Introduction
The JUnitXML module provides an interface for generating JUnit-compatible XML test result files on RISC OS systems. It allows test frameworks and test runners to record test results in a standardised format that can be consumed by continuous integration systems, test reporting tools, and other software development utilities.
The module implements a SWI-based interface for creating test suites, recording test cases with their outcomes (success, failure, error, or skipped), generating XML output conforming to the JUnit XML schema, and retrieving aggregate result counts.
Key features include:
- Support for multiple test suites within a single output file
- Flexible timestamp handling (current time, RISC OS time, Unix time, or ISO 8601 strings)
- Package and class name support for organised test hierarchies
- Custom properties for test suites
- Detailed failure and error reporting with type and message
- Duration tracking for test suites and individual test cases
- Result retrieval for obtaining aggregate pass, failure, error, and skip counts
Technical Details
The JUnitXML module maintains internal state for each open handle, tracking test suites and test cases as they are created. The module uses a handle-based API where each handle represents an independent XML output stream.
Memory Management
The module allocates memory dynamically for:
- Handle structures
- Test suite structures and their properties
- Test case structures
- String data (names, IDs, messages, timestamps)
All memory is freed when a handle is closed or when the module is terminated. The module maintains a linked list of handles to allow multiple concurrent XML output streams.
Timestamp Handling
The module supports multiple timestamp formats for test suite creation:
- Current time: Automatically uses the current system time
- RISC OS time: Accepts a RISC OS 5-byte quin (centiseconds since 1900-01-01)
- Unix time: Accepts a Unix epoch timestamp (seconds since 1970-01-01)
- ISO 8601: Accepts a pre-formatted ISO 8601 timestamp string
RISC OS time values are converted to ISO 8601 format using the Territory_ConvertDateAndTime SWI for local time conversion, with manual adjustment for UTC epoch differences.
XML Output
The module generates JUnit-compatible XML output with the following structure:
- XML declaration with UTF-8 encoding
- Root testsuites element with aggregate statistics
- Individual testsuite elements with properties and test cases
- Test case elements with optional failure or error child elements
The XML is written incrementally as test cases are closed, reducing memory usage for large test suites.
SWI Calls
| R0 | = |
Flags
| ||||||||||||
| R1 | = | Filename pointer (if bit 0 of flags set) | ||||||||||||
| R0 | = | Handle ID (positive) or error code (negative) |
JUnitXML_Create creates a new JUnit XML output handle. The handle can be used to create test suites and record test results.
Flags:
- Bit 0 (JUnitXML_Create_FilenameGiven): If set, R1 points to a filename string where the XML output will be written. If clear, the module will generate a default filename.
The function returns a positive handle ID on success, or a negative error code on failure.
SWI "JUnitXML_Create"
| R0 | = |
Reason code: |
| R1 | = | Handle ID |
| R2 - R5 | = | Dependent on reason code |
| R0 | = | Preserved |
JUnitXML_TestSuite performs operations on test suites within a given handle. The operation is determined by bits 0-3 of the flags word in R0. R1 always contains the handle ID.
| R0 | = |
Flags
| ||||||||||||||||||||
| R1 | = | Handle ID | ||||||||||||||||||||
| R2 | = | ID string pointer (or NULL to assign a numeric ID automatically) | ||||||||||||||||||||
| R3 | = | Name string pointer | ||||||||||||||||||||
| R4 | = | Package string pointer (if bit 4 of R0 set, else ignored) | ||||||||||||||||||||
| R5 | = | Timestamp value (interpretation depends on bits 5-6 of R0) | ||||||||||||||||||||
| R0 | = | Preserved |
Creates a new test suite within the handle. The suite is identified by the ID string in R2; if R2 is NULL a numeric ID is assigned automatically, starting from 1 and incrementing for each suite or test case within the handle or suite respectively.
If bit 4 (PackageSupplied) is set, R4 must point to a null-terminated package name string which will be recorded in the XML output.
The timestamp format for the suite start time is selected by bits 5-6 (TSFormat) of R0:
| Value | Meaning |
|---|---|
| 0 | JUNIT_TS_CURRENT — R5 ignored, uses current system time |
| 1 | JUNIT_TS_RISCOS — R5 points to a 5-byte RISC OS time quin (centiseconds since 1900-01-01) |
| 2 | JUNIT_TS_UNIX — R5 contains Unix epoch time (seconds since 1970-01-01) |
| 3 | JUNIT_TS_ISO8601 — R5 points to a pre-formatted ISO 8601 string |
Creating a test suite with current timestamp:
SYS "JUnitXML_TestSuite", 0, jx%, "suite1", "My Test Suite"
| R0 | = |
Flags
| ||||||||||||||||||||
| R1 | = | Handle ID | ||||||||||||||||||||
| R2 | = | Duration in centiseconds (if bit 7 of R0 set, else ignored) | ||||||||||||||||||||
| R0 | = | Preserved |
Closes the current open test suite, calculating and writing final aggregate statistics to the output.
If bit 7 (DurationPresent) is set, R2 gives the elapsed duration of the suite in centiseconds, which is recorded in the XML output. If clear, the duration is omitted.
Closing a test suite with duration of one second:
SYS "JUnitXML_TestSuite", 1 OR (1<<7), jx%, 100
| R0 | = |
Flags
| ||||||||||||||||||||||||
| R1 | = | Handle ID | ||||||||||||||||||||||||
| R2 | = | Hostname string pointer (if bit 4 of R0 set), otherwise ignored | ||||||||||||||||||||||||
| R3 | = | New suite name string pointer (if bit 5 of R0 set), otherwise ignored | ||||||||||||||||||||||||
| R4 | = | New package name string pointer (if bit 6 of R0 set), otherwise ignored | ||||||||||||||||||||||||
| R0 | = | Preserved |
Updates metadata on the currently open test suite for the given handle. This call allows the hostname, suite name, and package name to be set or changed after the suite has been created with reason 0.
Only the fields for which the corresponding flag bit is set are updated; fields for which the bit is clear are left unchanged.
This call must be made before the first property or test case is added to the suite, since the suite header is written to the XML output lazily just before those items. Calling Update after a property or test case has been added returns Error_JUnitXML_BadSuiteOp.
Setting the hostname on the current suite:
SYS "JUnitXML_TestSuite", 2 + (1<<4), jx%, "my-machine"
| R0 | = |
Flags
| ||||||||||||
| R1 | = | Handle ID | ||||||||||||
| R2 | = | Property name string pointer | ||||||||||||
| R3 | = | Property value string pointer | ||||||||||||
| R0 | = | Preserved |
Sets a named property on the currently open test suite. Properties are written to the XML output as child elements of the testsuite element.
Both the property name (R2) and property value (R3) must be non-NULL null-terminated strings.
Setting a property on a test suite:
SYS "JUnitXML_TestSuite", 3, jx%, "host", "RISC OS"
| R0 | = |
Reason code:
| ||||||||
| R1 | = | Handle ID | ||||||||
| R2 - R6 | = | Dependent on reason code |
| R0 | = | Preserved |
JUnitXML_TestCase performs operations on test cases within the current suite. The operation is determined by bits 0-3 of the flags word in R0. R1 always contains the handle ID.
| R0 | = |
Flags
| ||||||||||||||||||||
| R1 | = | Handle ID | ||||||||||||||||||||
| R2 | = | ID string pointer (or NULL to assign a numeric ID automatically) | ||||||||||||||||||||
| R3 | = | Classname string pointer | ||||||||||||||||||||
| R4 | = | Test name string pointer | ||||||||||||||||||||
| R5 | = | Failure type string pointer, or RISC OS error block pointer if bit 8 set (or NULL) | ||||||||||||||||||||
| R6 | = | Failure message string pointer (or NULL) | ||||||||||||||||||||
| R0 | = | Preserved |
Creates a new test case within the currently open suite. The test case is identified by the ID string in R2; if R2 is NULL a numeric ID is assigned automatically, starting from 1 and incrementing for each suite or test case within the handle or suite respectively.
The test status is given by bits 4-7 (Status) of R0:
| Value | Meaning |
|---|---|
| 0 | JUNIT_STATUS_NONE — no status specified |
| 1 | JUNIT_STATUS_SUCCESS — test passed |
| 2 | JUNIT_STATUS_FAILURE — test failed (assertion failure) |
| 3 | JUNIT_STATUS_ERROR — test errored (unexpected error) |
| 4 | JUNIT_STATUS_SKIPPED — test was skipped |
For failure and error statuses, R5 and R6 provide optional failure details. If bit 8 (ErrorBlock) is set in R0, R5 points to a RISC OS error block from which the error number and message are extracted; otherwise R5 points to a plain null-terminated string giving the failure type. R6 points to a null-terminated string giving an optional failure message.
Creating a successful test case:
SYS "JUnitXML_TestCase", 1<<4, jx%, "test1", "Calculator", "testAdd", 0, 0
Creating a failed test case:
SYS "JUnitXML_TestCase", 2<<4, jx%, "test2", "Calculator", "testDivide", "AssertionError", "Expected 5 but got 3"
| R0 | = |
Flags
| ||||||||||||
| R1 | = | Handle ID | ||||||||||||
| R2 | = | Duration in centiseconds | ||||||||||||
| R0 | = | Preserved |
Closes the current test case and writes it to the XML output. R2 gives the elapsed duration of the test case in centiseconds.
Closing a test case with duration of half a second:
SYS "JUnitXML_TestCase", 1, jx%, 50
| R0 | = |
Flags
| ||||||||||||
| R1 | = | Handle ID | ||||||||||||
This reason code is reserved for future use. It is not currently implemented; calling it always returns Error_JUnitXML_BadCaseOp.
| R0 | = |
Flags
| ||||||||||||
| R1 | = | Filename pointer (if bit 0 set) | ||||||||||||
| R0 | = | Preserved |
JUnitXML_Close closes a JUnit XML handle, writing the final XML structure and freeing resources. The handle is removed from the internal handle list.
Flags:
- Bit 0 (JUnitXML_Close_FilenameGiven): If set, R1 points to an output filename. This overrides the filename specified during handle creation.
The function closes the most recently created handle if multiple handles exist. For precise control, ensure handles are closed in reverse order of creation.
Closing a handle with the original filename:
SYS "JUnitXML_Close", 0
Closing a handle with a different filename:
SYS "JUnitXML_Close", 1, "$.results.junit/xml"
| R0 | = |
Flags
| ||||||
| R1 | = | Handle ID | ||||||
| R0 | = | Number of tests present |
| R1 | = | Number of passes |
| R2 | = | Number of failures |
| R3 | = | Number of errors |
| R4 | = | Number of skips |
JUnitXML_Result returns aggregate result counts for all test suites recorded within the given handle. The counts are summed across all suites, including suites that are still open.
R0 (flags) must be zero on entry; all flag bits are reserved for future use.
This SWI may be called at any point while the handle is valid — before or after closing individual suites. It must not be called after JUnitXML_Close has been used to close the handle, as the handle will no longer be valid.
The returned counts are:
- R0: Total number of test cases recorded across all suites
- R1: Number of passing tests (total minus failures, errors, and skips)
- R2: Number of tests that reported a failure status
- R3: Number of tests that reported an error status
- R4: Number of tests that were skipped
Reading result counts after running tests:
SYS "JUnitXML_Result", 0, jx% TO tests%, passes%, failures%, errors%, skips%
PRINT "Tests: "; tests%
PRINT "Passes: "; passes%
PRINT "Failures: "; failures%
PRINT "Errors: "; errors%
PRINT "Skipped: "; skips%
IF failures% + errors% > 0 THEN PRINT "FAILED" ELSE PRINT "PASSED"
Error Messages
The JUnitXML module returns error blocks in the standard RISC OS format. The error numbers are module-specific with base &822A00, and are returned as negative values from SWI calls or via the carry flag.
Failed to create a new JUnit XML handle. This may be due to memory exhaustion or invalid parameters.
Failed to create a test suite within the handle. This may occur if the handle is invalid or if memory allocation fails.
Failed to close a test suite. This may occur if the suite is not properly formed or if writing to the output file fails.
Failed to set a property on a test suite. This may occur if the property name or value is invalid, or if memory allocation fails.
An unknown operation code was specified for JUnitXML_TestSuite. The supported operations are 0 (Create), 1 (Close), and 3 (Property).
Failed to create a test case. This may occur if the handle or current suite is invalid, or if memory allocation fails.
Failed to close a test case. This may occur if writing to the output file fails.
An unknown operation code was specified for JUnitXML_TestCase. The supported operations are 0 (Create) and 1 (Close).
Failed to close a JUnit XML handle. This may occur if writing the final XML structure fails or if the handle is invalid.
No handle is available to close. All handles may have already been closed, or the module may not have been initialised.
Module initialisation failed. The internal state manager could not be initialised.
Usage
The JUnitXML module is loaded automatically when a program links against it, or it can be loaded manually using the RMEnsure command.
Loading the Module
To load the module manually:
- *RMEnsure JUnitXML 0.01 RMLoad JUnitXML
The module will be loaded from the usual module search path. Once loaded, the SWI calls described above can be used to create and manage JUnit XML output.
Typical Usage Pattern
A typical usage pattern for generating JUnit XML output:
- Call JUnitXML_Create to obtain a handle
- Call JUnitXML_TestSuite with operation Create to start a test suite
For each test case:
- Call JUnitXML_TestCase with operation Create
- Call JUnitXML_TestCase with operation Close
- Call JUnitXML_TestSuite with operation Close to finish the test suite
- Call JUnitXML_Close to close the handle and write the final XML
Killing the Module
To kill the module:
- *RMKill JUnitXML
All open handles will be closed and their resources freed when the module is killed.
Examples
Complete Example in C
This example shows how to create a simple JUnit XML file with one test suite containing three test cases:
#include <stdio.h>
#include "swis.h"
#include "kernel.h"
#define JUnitXML_Create_FilenameGiven (1 << 0)
#define JUnitXML_TestSuite_OpCreate 0
#define JUnitXML_TestSuite_OpClose 1
#define JUnitXML_TestCase_OpCreate 0
#define JUnitXML_TestCase_OpClose 1
#define JUNIT_STATUS_SUCCESS 1
#define JUNIT_STATUS_FAILURE 2
int main(void)
{
_kernel_swi_regs r;
_kernel_oserror *err;
int handle;
/* Create a handle with output filename */
r.r[0] = JUnitXML_Create_FilenameGiven;
r.r[1] = (int)"TestResults.xml";
err = _kernel_swi(0x5AC00, &r, &r); /* JUnitXML_Create */
if (err) {
printf("Failed to create handle: %s\\n", err->errmess);
return 1;
}
handle = r.r[0];
/* Create a test suite */
r.r[0] = JUnitXML_TestSuite_OpCreate; /* Create, current time */
r.r[1] = handle;
r.r[2] = (int)"suite1";
r.r[3] = (int)"MyTestSuite";
err = _kernel_swi(0x5AC01, &r, &r); /* JUnitXML_TestSuite */
if (err) {
printf("Failed to create suite: %s\\n", err->errmess);
return 1;
}
/* Create test case 1: success */
r.r[0] = JUnitXML_TestCase_OpCreate | (JUNIT_STATUS_SUCCESS << 4);
r.r[1] = handle;
r.r[2] = (int)"test1";
r.r[3] = (int)"MyTestClass";
r.r[4] = (int)"test_addition";
r.r[5] = 0;
r.r[6] = 0;
err = _kernel_swi(0x5AC02, &r, &r); /* JUnitXML_TestCase */
if (err) goto error;
/* Close test case 1 */
r.r[0] = JUnitXML_TestCase_OpClose;
r.r[1] = handle;
r.r[2] = 25; /* 25 centiseconds */
err = _kernel_swi(0x5AC02, &r, &r);
if (err) goto error;
/* Create test case 2: failure */
r.r[0] = JUnitXML_TestCase_OpCreate | (JUNIT_STATUS_FAILURE << 4);
r.r[1] = handle;
r.r[2] = (int)"test2";
r.r[3] = (int)"MyTestClass";
r.r[4] = (int)"test_subtraction";
r.r[5] = (int)"AssertionError";
r.r[6] = (int)"Expected 3 but got 4";
err = _kernel_swi(0x5AC02, &r, &r);
if (err) goto error;
/* Close test case 2 */
r.r[0] = JUnitXML_TestCase_OpClose;
r.r[1] = handle;
r.r[2] = 30;
err = _kernel_swi(0x5AC02, &r, &r);
if (err) goto error;
/* Close the test suite */
r.r[0] = JUnitXML_TestSuite_OpClose | (1 << 7); /* Duration present */
r.r[1] = handle;
r.r[2] = 100; /* 100 centiseconds total */
err = _kernel_swi(0x5AC01, &r, &r);
if (err) goto error;
/* Close the handle */
r.r[0] = 0;
err = _kernel_swi(0x5AC03, &r, &r); /* JUnitXML_Close */
if (err) goto error;
printf("JUnit XML file created successfully\\n");
return 0;
error:
printf("Error: %s\\n", err ? err->errmess : "Unknown");
return 1;
}
Example Output
The above C code would generate XML similar to:
<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
<testsuite name="MyTestSuite" package=""
id="suite1" tests="2" failures="1" errors="0" skipped="0"
time="1.00" timestamp="2026-03-27T10:30:00Z">
<testcase id="test1" classname="MyTestClass" name="test_addition"
time="0.25"/>
<testcase id="test2" classname="MyTestClass" name="test_subtraction"
time="0.30">
<failure type="AssertionError">Expected 3 but got 4</failure>
</testcase>
</testsuite>
</testsuites>