Pyromaniac

Pyromaniac PRM: JUnitXML

Pyromaniac PRM: JUnitXML

RISC OS PyromaniacJUnitXML

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

JUnitXML_CreateSWI &5AC00
R0= Flags
Bit(s)NameMeaning
0JUnitXML_Create_FilenameGiven If set, R1 points to a filename string
1-31Reserved, must be zero
R1= Filename pointer (if bit 0 of flags set)
R0= Handle ID (positive) or error code (negative)
Interrupts are undefined
Fast interrupts are undefined
Processor is in undefined mode
Not defined

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"

JUnitXML_TestSuiteSWI &5AC01
Performs operations on test suites
R0=

Reason code:

ReasonAction
0Creates a new test suite within the handle
1Closes the current test suite, writing final statistics
2Updates metadata on the current open test suite
3Sets a property on the current test suite
R1= Handle ID
R2 - R5= Dependent on reason code
R0= Preserved
Interrupts are undefined
Fast interrupts are undefined
Processor is in undefined mode
Not defined

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.

JUnitXML_TestSuite 0CreateSWI &5AC01
Creates a new test suite within the handle
R0= Flags
Bit(s)NameMeaning
0-3Operation 0 (Create)
4PackageSupplied If set, R4 points to a package name
5-6TSFormat Timestamp format: 0=Current, 1=RISC OS, 2=Unix, 3=ISO 8601
7-31Reserved, must be zero
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
Interrupts are undefined
Fast interrupts are undefined
Processor is in undefined mode
Not defined

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:

ValueMeaning
0JUNIT_TS_CURRENT — R5 ignored, uses current system time
1JUNIT_TS_RISCOS — R5 points to a 5-byte RISC OS time quin (centiseconds since 1900-01-01)
2JUNIT_TS_UNIX — R5 contains Unix epoch time (seconds since 1970-01-01)
3JUNIT_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"
   

JUnitXML_TestSuite 1CloseSWI &5AC01
Closes the current test suite, writing final statistics
R0= Flags
Bit(s)NameMeaning
0-3Operation 1 (Close)
4-6Reserved, must be zero
7DurationPresent If set, R2 contains the suite duration in centiseconds
8-31Reserved, must be zero
R1= Handle ID
R2= Duration in centiseconds (if bit 7 of R0 set, else ignored)
R0= Preserved
Interrupts are undefined
Fast interrupts are undefined
Processor is in undefined mode
Not defined

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
   

JUnitXML_TestSuite 2UpdateSWI &5AC01
Updates metadata on the current open test suite
R0= Flags
Bit(s)NameMeaning
0-3Operation 2 (Update)
4JUnitXML_TestSuite_HostnameSupplied If set, R2 points to a hostname string
5JUnitXML_TestSuite_UpdateName If set, R3 points to a new suite name string
6JUnitXML_TestSuite_UpdatePackage If set, R4 points to a new package name string
7-31Reserved, must be zero
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
Interrupts are undefined
Fast interrupts are undefined
Processor is in undefined mode
Not defined

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"
   

JUnitXML_TestSuite 3PropertySWI &5AC01
Sets a property on the current test suite
R0= Flags
Bit(s)NameMeaning
0-3Operation 3 (Property)
4-31Reserved, must be zero
R1= Handle ID
R2= Property name string pointer
R3= Property value string pointer
R0= Preserved
Interrupts are undefined
Fast interrupts are undefined
Processor is in undefined mode
Not defined

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"
   

JUnitXML_TestCaseSWI &5AC02
Performs operations on test cases
R0=

Reason code:

ReasonAction
0Creates a new test case within the current suite
1Closes the current test case, writing it to the output
2Reserved for future use; not yet implemented
R1= Handle ID
R2 - R6= Dependent on reason code
R0= Preserved
Interrupts are undefined
Fast interrupts are undefined
Processor is in undefined mode
Not defined

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.

JUnitXML_TestCase 0CreateSWI &5AC02
Creates a new test case within the current suite
R0= Flags
Bit(s)NameMeaning
0-3Operation 0 (Create)
4-7Status Test status code (see below)
8ErrorBlock If set, R5 points to a RISC OS error block rather than a string
9-31Reserved, must be zero
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
Interrupts are undefined
Fast interrupts are undefined
Processor is in undefined mode
Not defined

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:

ValueMeaning
0JUNIT_STATUS_NONE — no status specified
1JUNIT_STATUS_SUCCESS — test passed
2JUNIT_STATUS_FAILURE — test failed (assertion failure)
3JUNIT_STATUS_ERROR — test errored (unexpected error)
4JUNIT_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"
   

JUnitXML_TestCase 1CloseSWI &5AC02
Closes the current test case, writing it to the output
R0= Flags
Bit(s)NameMeaning
0-3Operation 1 (Close)
4-31Reserved, must be zero
R1= Handle ID
R2= Duration in centiseconds
R0= Preserved
Interrupts are undefined
Fast interrupts are undefined
Processor is in undefined mode
Not defined

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
   

JUnitXML_TestCase 2UpdateSWI &5AC02
Reserved for future use; not yet implemented
R0= Flags
Bit(s)NameMeaning
0-3Operation 2 (Update)
4-31Reserved, must be zero
R1= Handle ID
None
Interrupts are undefined
Fast interrupts are undefined
Processor is in undefined mode
Not defined

This reason code is reserved for future use. It is not currently implemented; calling it always returns Error_JUnitXML_BadCaseOp.

JUnitXML_CloseSWI &5AC03
R0= Flags
Bit(s)NameMeaning
0JUnitXML_Close_FilenameGiven If set, R1 points to an output filename
1-31Reserved, must be zero
R1= Filename pointer (if bit 0 set)
R0= Preserved
Interrupts are undefined
Fast interrupts are undefined
Processor is in undefined mode
Not defined

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"
   

JUnitXML_ResultSWI &5AC04
R0= Flags
Bit(s)Meaning
0-31Reserved, must be zero
R1= Handle ID
R0= Number of tests present
R1= Number of passes
R2= Number of failures
R3= Number of errors
R4= Number of skips
Interrupts are undefined
Fast interrupts are undefined
Processor is in undefined mode
Not defined

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.

Error_JUnitXML_CreateFailedError &822A00
Failed to create JUnitXML handle

Failed to create a new JUnit XML handle. This may be due to memory exhaustion or invalid parameters.

Error_JUnitXML_CreateSuiteFailedError &822A01
Failed to create test suite

Failed to create a test suite within the handle. This may occur if the handle is invalid or if memory allocation fails.

Error_JUnitXML_CloseSuiteFailedError &822A02
Failed to close test suite

Failed to close a test suite. This may occur if the suite is not properly formed or if writing to the output file fails.

Error_JUnitXML_SetPropertyFailedError &822A03
Failed to set property

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.

Error_JUnitXML_BadSuiteOpError &822A04
Unknown TestSuite operation

An unknown operation code was specified for JUnitXML_TestSuite. The supported operations are 0 (Create), 1 (Close), and 3 (Property).

Error_JUnitXML_CreateCaseFailedError &822A05
Failed to create test case

Failed to create a test case. This may occur if the handle or current suite is invalid, or if memory allocation fails.

Error_JUnitXML_CloseCaseFailedError &822A06
Failed to close test case

Failed to close a test case. This may occur if writing to the output file fails.

Error_JUnitXML_BadCaseOpError &822A07
Unknown TestCase operation

An unknown operation code was specified for JUnitXML_TestCase. The supported operations are 0 (Create) and 1 (Close).

Error_JUnitXML_CloseFailedError &822A08
Failed to close JUnitXML handle

Failed to close a JUnit XML handle. This may occur if writing the final XML structure fails or if the handle is invalid.

Error_JUnitXML_NoHandleError &822A09
No JUnitXML handle to close

No handle is available to close. All handles may have already been closed, or the module may not have been initialised.

Error_JUnitXML_InitFailedError &822A0A
Failed to initialise JUnitXML state

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:

  1. Call JUnitXML_Create to obtain a handle
  2. Call JUnitXML_TestSuite with operation Create to start a test suite
  3. For each test case:

    • Call JUnitXML_TestCase with operation Create
    • Call JUnitXML_TestCase with operation Close
  4. Call JUnitXML_TestSuite with operation Close to finish the test suite
  5. 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>