CI & JUnit reports¶
Horizon-QA CI runs are normal dedicated-server runs with the Horizon-QA mode set on the Minecraft server JVM:
--mcJvmArgs is provided by Retrofuturagradle (RFG). Do not pass -Dhorizonqa.mode=ci directly to Gradle; that sets the property on the Gradle daemon, where the Minecraft server cannot read it.
In horizonqa.mode=ci, Horizon-QA discovers tests, runs the selected batch automatically after the server is ready, writes reports, and exits the process with a deterministic status code. Local authoring should use horizonqa.mode=interactive or omit the mode property, because interactive is the default.
Use horizonqa.mode=ci -Dhorizonqa.autoRun=false when you want report files from a manually-started non-interactive batch without CI lifetime management:
./gradlew runServer --mcJvmArgs="-Dhorizonqa.mode=ci -Dhorizonqa.autoRun=false -Dhorizonqa.reportDir=build/horizonqa"
Manual reported batches use the same report formats as automatic CI and default to the same void world policy. Then run /horizonqa run <testId>, /horizonqa runall [namespace], or /horizonqa runfailed. The selected batch writes JUnit XML and status JSON when it finishes, but the server does not auto-run tests at startup and does not exit afterward. horizonqa.tests and horizonqa.allowNoTests only affect automatic execution; for manual reported batches, use the command arguments to choose tests.
Modes are presets. Override specific behavior when the workflow needs it:
# Use the configured or existing world instead of Horizon-QA's void world
./gradlew runServer --mcJvmArgs="-Dhorizonqa.mode=ci -Dhorizonqa.world=normal"
# Run automatically but keep the server up afterward
./gradlew runServer --mcJvmArgs="-Dhorizonqa.mode=ci -Dhorizonqa.stopServer=false"
# Place the test grid at Y=128
./gradlew runServer --mcJvmArgs="-Dhorizonqa.mode=ci -Dhorizonqa.autoRun=false -Dhorizonqa.world=normal -Dhorizonqa.gridOrigin=0,128,0"
# Manual reported batches with CI overrides
./gradlew runServer --mcJvmArgs="-Dhorizonqa.mode=ci -Dhorizonqa.autoRun=false -Dhorizonqa.reportDir=build/horizonqa"
Report files¶
By default reports are written in the server process working directory:
For CI, send them to a predictable artifact directory:
./gradlew runServer --mcJvmArgs="-Dhorizonqa.mode=ci -Dhorizonqa.reportDir=build/horizonqa"
./gradlew runServer --mcJvmArgs="-Dhorizonqa.mode=ci -Dhorizonqa.autoRun=false -Dhorizonqa.reportDir=build/horizonqa"
Report path flags:
| Property | Meaning |
|---|---|
horizonqa.reportDir | Directory containing TEST-horizonqa.xml and, by default, horizonqa-result.json |
horizonqa.reportFile | Exact JUnit XML output path; takes precedence over horizonqa.reportDir |
horizonqa.statusFile | Exact status JSON output path |
Relative paths resolve from the server process working directory. When horizonqa.reportDir is set and horizonqa.statusFile is not set, the status JSON is written to horizonqa-result.json in that same directory.
JUnit XML¶
TEST-horizonqa.xml uses a standard JUnit-style <testsuite>:
<testsuite name="horizonqa" tests="…" failures="…" errors="…" skipped="…" …>
<testcase name="methodName" classname="namespace:ClassName" time="…">
<!-- failure, error, skipped, and system-out elements as appropriate -->
</testcase>
</testsuite>
| Field | Meaning |
|---|---|
classname | Test id prefix, for example mymod:AssemblerTests |
name | Method name |
time | Duration in seconds (testTicks / 20) |
Required assertion failures and timeouts are emitted as <failure>. Infrastructure problems such as cleanup, template, configuration, selection, report-path, and reporting failures are emitted as <error>. Optional failures are emitted as <skipped> so JUnit publishers can show them without failing the suite aggregate.
When event recording is enabled, each <testcase> may include ordered [t=NNN] [category] summary lines in <system-out>. The server console also prints a compact failure tail.
Disable event recording only for performance investigations:
Status JSON schema¶
horizonqa-result.json is the compact automation surface. Schema version 1 has this top-level shape:
{
"schemaVersion": 1,
"status": "passed",
"exitCode": 0,
"configuration": {
"mode": "ci",
"rawMode": "ci",
"world": "void",
"rawWorld": null,
"autoRun": true,
"rawAutoRun": null,
"stopServer": true,
"rawStopServer": null,
"gridOrigin": "0,64,0",
"rawGridOrigin": null,
"tests": null,
"selectsAllTests": true,
"allowNoTests": false,
"eventsEnabled": true,
"reportFile": null,
"reportDir": "build/horizonqa",
"statusFile": null
},
"counts": {
"selectedTests": 1,
"passed": 1,
"failed": 0,
"timedOut": 0,
"incomplete": 0,
"requiredFailures": 0,
"optionalFailures": 0,
"issues": 0,
"diagnosticErrors": 0,
"junitFailures": 0,
"junitErrors": 0,
"junitSkipped": 0
},
"reports": {
"junit": "build/horizonqa/TEST-horizonqa.xml",
"status": "build/horizonqa/horizonqa-result.json"
},
"issues": [],
"tests": []
}
Each issues[] entry contains id, kind, source, name, message, fatalInCi, and optional details / stackTrace. Each tests[] entry contains id, classname, name, status, required, ticks, timeSeconds, optional blockedByIssueId, and optional failure details.
Status values are:
JSON status | Exit code | Meaning |
|---|---|---|
passed | 0 | No required failures and no infrastructure errors |
failed | 1 | At least one required test failed or timed out |
error | 2 | Infrastructure, configuration, selection, template, cleanup, reporting, report-path, or incomplete-run error |
Selectors¶
Use horizonqa.tests to limit automatic execution:
-Dhorizonqa.tests=mymod
-Dhorizonqa.tests=mymod:AssemblerTests.processesOneRecipe
-Dhorizonqa.tests=mymod,compatmod:BridgeTests.basic
Selector grammar:
selectors := selector ("," selector)*
selector := namespace | exact-test-id
namespace := token-without-colon
exact-test-id := namespace ":" class-and-method
Rules:
- unset or empty
horizonqa.testsselects all valid tests, - a namespace selector matches every valid test id that starts with
namespace:, - an exact selector must contain exactly one
:and match the full test id, - whitespace around comma-separated tokens is trimmed,
- empty tokens such as
a,,bare invalid, *is not supported; omit the property or set it to an empty value to run everything,- duplicate selections are de-duplicated while preserving discovery order.
For automatic execution, invalid selector syntax aborts before tests run and exits 2. A syntactically valid selector that matches no valid tests is reported as a CI infrastructure issue; if other selectors match valid tests, those tests still run and the final result still includes the selector issue.
If no valid tests are selected automatically, CI still writes TEST-horizonqa.xml and horizonqa-result.json. By default this exits 2. Set -Dhorizonqa.allowNoTests=true only for jobs where an empty selection is expected and there are no selector infrastructure issues. Manual reported batches ignore these selector properties and use /horizonqa run, /horizonqa runall [namespace], or /horizonqa runfailed arguments instead.
Optional tests¶
@GameTest(required = false) marks a test as optional. Optional tests still run, appear in JUnit XML, and appear in tests[] in the status JSON with required: false.
An optional failure or timeout:
- increments
counts.optionalFailures, - is represented as
<skipped>in JUnit XML, - does not make the process exit non-zero by itself.
Use optional tests for genuinely quarantined, experimental, or environment-specific coverage. Required tests should gate merges.
GitHub Actions handling¶
Always upload reports with if: always() so failed tests still leave artifacts. Publish JUnit XML from a later always() step, then let the original runServer exit code fail the job.
name: Horizon-QA
on:
pull_request:
push:
branches: [ master, main ]
jobs:
gametest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version-file: .java-version
- uses: gradle/actions/setup-gradle@v4
- name: Run Horizon-QA
run: >
./gradlew runServer
--mcJvmArgs="-Dhorizonqa.mode=ci -Dhorizonqa.reportDir=build/horizonqa"
- name: Upload Horizon-QA reports
if: always()
uses: actions/upload-artifact@v4
with:
name: horizonqa-reports
path: |
build/horizonqa/TEST-horizonqa.xml
build/horizonqa/horizonqa-result.json
If your workflow uses a JUnit publishing action, run it after the upload step with if: always() and point it at build/horizonqa/TEST-horizonqa.xml.
When CI fails¶
Work from the artifacts before relaunching anything: the <failure> message, the event trace in <system-out>, and issues[] in the status JSON usually identify the cause on their own. The triage workflow, including a failure-signature table and the in-game reproduction loop, is in Debugging failed tests.