Every Axion container image includes a built-in test harness at /report/status. It runs 110+ automated assertions that validate the container is correctly built, compiled, configured, and ready to serve. The harness is the quality gate between a successful docker build and a production deployment.
| URL |
Format |
Purpose |
/report/status |
HTML |
Visual dashboard for operators |
/report/status?format=png |
PNG |
Playwright screenshot for reports |
/report/status?format=json |
JSON |
Machine-readable test results for CI/CD |
# Start the container
docker run -d --name axion-test -p 9400:9400 \
-v /path/to/config.yaml:/axion/config.yaml:ro \
-v /path/to/data.config:/axion/data.config:ro \
axion-homechoice:report
# Wait for startup
until curl -sf http://localhost:9400/report/status > /dev/null; do sleep 2; done
# Run harness
curl -s http://localhost:9400/report/status?format=json | jq '.tests.summary'
#!/bin/bash
result=$(curl -s http://host:9400/report/status?format=json)
unexpected=$(echo "$result" | jq '.tests.summary.unexpected_failures')
effective=$(echo "$result" | jq '.tests.summary.effective_pass_rate')
echo "Effective pass rate: ${effective}%"
echo "Unexpected failures: ${unexpected}"
if [ "$unexpected" -gt 0 ]; then
echo "FAILED — unexpected failures:"
echo "$result" | jq -r '
.tests.assertions[]
| select(.pass == false and .expected_fail == "")
| " \(.test): \(.detail)"
'
exit 1
fi
echo "PASSED"
{
"package": "homechoice",
"python": "3.12.13",
"modules": { "total_so": 808, "total_py": 0, ... },
"connectivity": { "MariaDB": { "reachable": true, ... }, ... },
"factory_imports": [ { "factory": "factory.core", "module": "ObjData", "status": "pass" }, ... ],
...
"tests": {
"assertions": [
{
"test": "python_version",
"pass": true,
"detail": "3.12.13",
"expected_fail": ""
},
{
"test": "connectivity_redis",
"pass": false,
"detail": "Connection refused",
"expected_fail": "Requires Redis pod (Helm chart)"
}
],
"summary": {
"total": 110,
"passed": 104,
"expected_failures": 6,
"unexpected_failures": 0,
"pass_rate": 94,
"effective_pass_rate": 100
}
}
}
| Test |
What it validates |
Failure means |
python_version |
Python starts with 3.12 |
Wrong base image |
package_set |
AXION_PACKAGE resolves to a value |
Config or env var missing |
is_docker |
DOCKER_AXION env detected |
Not running in container |
compiled_100pct |
Zero .py files remain in factories |
ObjCompile failure |
no_leftover_py |
No uncompiled source files |
Cython worker missed files |
config_trimmed |
Config has 4 or fewer sections |
trim_config didn't run |
venv_size_reasonable |
Venv between 0 and 5GB |
Bloated or missing venv |
| Test |
What it validates |
Failure means |
volume_local.documents |
Dir exists and is writable |
Mount missing |
volume_data.documents |
Dir exists and is writable |
Mount missing |
volume_archive.documents |
Dir exists and is writable |
Mount missing |
Tests that every compiled .so module can be imported by the Python runtime. Catches:
- Missing dependencies at runtime
- Cython compilation that produced invalid
.so files
- Broken import chains between modules
Modules tested per factory:
| Factory |
Modules |
factory.core |
ObjData, Objects, ObjWorkflow, ObjReport, ObjAcl, ObjPlatform, ConfigIni, ObjEncryption, ObjMonitor, ObjImage, ObjDocument, ObjNotify, ObjCalculation, ObjScorecard, ObjService, ObjChannel |
factory.core/extend.delegate |
ObjDebug, ObjSystem, ObjPatch, ObjResource, ObjSerialization |
factory.core/extend.monitor |
ObjMonitorCollector, ObjMonitorPing, ObjMonitorPersist, ObjMonitorReport, ObjMonitorSecurity |
factory.core/extend.data |
ObjDataBranding, ObjDataCache, ObjDataRemote, ObjDataEvent, ObjDataDDL, ObjDataKey, ObjDataQueryExec, ObjDataSql |
factory.web |
WebServer, WebForm, WebFormElement |
factory.pages |
WebPageForm, WebPageReport, WebPageAuth, WebPageFlow, WebPageGo, WebPagePWA |
factory.report |
ObjReportStatus |
factory.service |
ObjServiceHeartbeat |
factory.text |
ObjTextDownloadXlsx, ObjTextReportpdf |
factory.export |
ObjDataExportCsv, ObjDataExportParquet |
factory.import |
ObjDataImportCSV |
factory.field |
ObjFieldText, ObjFieldBoolean, ObjFieldCurrency |
factory.formlayout |
ObjLayoutDesignForm |
factory.workflow |
ObjWorkflowNode, ObjWorkflowService, ObjWorkflowScheduler, ObjWorkflowCalc |
factory.webhook |
ObjHookWorkflow |
factory.sms |
ObjSmsGrapevine |
factory.conversation |
ObjConversation |
factory.learn |
ObjMLDatasets |
Pings each external service. Six are expected to fail in standalone container mode — they require Helm chart infrastructure pods or external services.
| Test |
Expected in standalone |
Resolves when |
connectivity_mariadb |
PASS |
DB reachable from container |
connectivity_redis |
EXPECTED FAIL |
Redis pod deployed (Helm chart) |
connectivity_mongodb |
EXPECTED FAIL |
MongoDB pod deployed (Helm chart) |
connectivity_mqtt |
PASS |
MQTT broker reachable |
connectivity_rabbitmq |
EXPECTED FAIL |
RabbitMQ pod deployed (Helm chart) |
connectivity_influxdb |
EXPECTED FAIL |
InfluxDB configured (external) |
connectivity_smtp |
PASS |
Brevo SMTP reachable |
connectivity_imap |
PASS |
IMAP server reachable |
connectivity_aws_s3 |
PASS |
AWS accessible |
connectivity_cloudflare |
PASS |
Cloudflare API accessible |
connectivity_docker_registry |
EXPECTED FAIL |
Registry deployed (Ticket 3) |
connectivity_keycloak |
EXPECTED FAIL |
Keycloak configured (external) |
¶ 5. DNS and Time (2 tests)
| Test |
What it validates |
dns_google_com |
Container can resolve external DNS |
dns_technocore_co_za |
Container can resolve internal DNS |
| Test |
What it validates |
Failure means |
pem_private_key |
Private PEM file exists in data.config |
Volume not mounted |
pem_public_key |
Public PEM file exists in data.config |
Volume not mounted |
config_yaml_exists |
config.yaml present |
ConfigMap not mounted |
config_package_matches |
config.yaml package: matches AXION_PACKAGE |
Wrong config mounted |
config_has_database |
Package section has database entry |
Incomplete config |
env_axion_package_set |
AXION_PACKAGE env var present |
Dockerfile or deploy issue |
env_docker_axion_set |
DOCKER_AXION env var present |
Not a Docker container |
| Test |
What it validates |
tini_pid1 |
PID 1 is tini (proper signal handling) |
process_running |
Server process has a PID |
| Test |
What it validates |
webserver_routes_registered |
WebServer.app has >10 routes (forms, reports, webhooks, etc.) |
| Test |
What it validates |
Failure means |
core_no_source_py |
No .py files in factory.core (all compiled to .so) |
ObjCompile missed files |
no_orphan_c_files |
No .c intermediates left in factories |
Cleanup step failed |
| Test |
What it validates |
Failure means |
no_git_dir |
No .git/ in image |
.dockerignore not applied |
no_env_file |
No .env file in image |
Secrets leaked into build |
no_data_code_dir |
No data.code/ (PEM source) in image |
Should be runtime mount |
no_leaked_package_config |
Config has only expected sections |
Other packages' credentials present |
| Test |
What it validates |
Failure means |
memory_under_1gb |
Process RSS < 1GB |
Memory leak or bloat |
no_zombie_processes |
Zero zombie child processes |
tini not reaping |
fd_count_reasonable |
Open file descriptors < 100 |
FD leak |
disk_usage_reasonable |
/axion < 5GB |
Build artefacts not cleaned |
| Metric |
Description |
total |
Total number of assertions |
passed |
Tests that passed |
expected_failures |
Tests that failed but are tagged as expected for this deployment stage |
unexpected_failures |
Tests that failed unexpectedly — these are real issues |
pass_rate |
passed / total * 100 |
effective_pass_rate |
(passed + expected_failures) / total * 100 — the metric to gate on |
| Stage |
Total |
Passed |
Expected Fail |
Unexpected |
Effective |
| Standalone container |
110 |
104 |
6 |
0 |
100% |
| With Redis + MongoDB |
110 |
106 |
4 |
0 |
100% |
| With Helm chart (full) |
110 |
108 |
2 |
0 |
100% |
| Full production |
110 |
110 |
0 |
0 |
100% |
Tests are defined in factory.report/package.system/ObjReportStatus.py in the _build_test_harness() method. To add a test:
check(
"test_name", # Unique test identifier
condition, # bool — True = pass
"detail string", # Optional context shown on failure
expected_fail="reason" # If set, failure is expected
)
For connectivity tests against infra services, add the service key to the infra_services dict to automatically tag as expected failure in standalone mode.
factory.report/package.system/ObjReportStatus.py — implementation
factory.report/package.system/ObjReportStatus.md — module documentation
resource.notes/howto/howto_docker_build.md — build guide
factory.deploy/ObjBuild.py — build pipeline (runs before harness)