Testing
NativeAOT-Patcher has two complementary testing layers: unit tests that validate the build-time toolchain (patcher, scanner, analyzer) and kernel integration tests that run compiled kernel images inside QEMU and report results over a binary UART protocol.
Unit Tests
Unit tests live in tests/Cosmos.Tests.* projects and are run with the standard .NET test runner. They do not require QEMU or any special infrastructure.
Running Unit Tests
dotnet test
Test Projects
- Cosmos.Tests.Build.Asm – Verifies the assembly build task runs via Yasm.
Test1
- Cosmos.Tests.Build.Analyzer.Patcher – Validates that code does not contain plug architecture errors.
Test_AnalyzeAccessedMemberTest_MethodNotImplementedTest_StaticConstructorTooManyParametersTest_StaticConstructorNotImplemented
- Cosmos.Tests.Scanner – Validates that all required plugs are detected correctly.
LoadPlugMethods_ShouldReturnPublicStaticMethodsLoadPlugMethods_ShouldReturnEmpty_WhenNoMethodsExistLoadPlugMethods_ShouldContainAddMethod_WhenPluggedLoadPlugs_ShouldFindPluggedClassesLoadPlugs_ShouldIgnoreClassesWithoutPlugAttributeLoadPlugs_ShouldHandleOptionalPlugsFindPluggedAssemblies_ShouldReturnMatchingAssemblies
- Cosmos.Tests.Patcher – Ensures that plugs are applied successfully to target methods and types.
PatchAssembly_ShouldSkipWhenNoMatchingPlugsPatchObjectWithAThis_ShouldPlugInstanceCorrectlyPatchConstructor_ShouldPlugCtorCorrectlyPatchProperty_ShouldPlugPropertyPatchType_ShouldReplaceAllMethodsCorrectlyPatchType_ShouldPlugAssemblyAddMethod_BehaviorBeforeAndAfterPlug
- Cosmos.Tests.NativeWrapper – Contains runtime assets; no unit tests.
- Cosmos.Tests.NativeLibrary – Provides native code used in tests; no unit tests.
Kernel Integration Tests
Kernel integration tests compile a real NativeAOT kernel, boot it in QEMU, and communicate results back to the host over a binary UART protocol. These tests exercise the full build and runtime pipeline.
Test Suites
| Suite | Tests | Description |
|---|---|---|
| HelloWorld | 3 | Basic arithmetic, boolean logic, integer comparison |
| Memory | 85 | Boxing/unboxing, memory allocation, collections, memory copy, GC |
HelloWorld Tests
Test_BasicArithmetic– Addition (2+2=4)Test_BooleanLogic– True/False assertionsTest_IntegerComparison– Equality and comparison operators
Memory Tests
Boxing/Unboxing (11 tests):
Boxing_Char,Boxing_Int32,Boxing_Byte,Boxing_LongBoxing_Nullable,Boxing_Interface,Boxing_CustomStructBoxing_ArrayCopy,Boxing_Enum,Boxing_ValueTuple,Boxing_NullInterface
Memory Allocation (8 tests):
Memory_CharArray,Memory_StringAllocation,Memory_IntArrayMemory_StringConcat,Memory_StringBuilderMemory_ZeroLengthArray,Memory_EmptyString,Memory_LargeAllocation
Generic Collections – List (14 tests):
Collections_ListInt,Collections_ListString,Collections_ListByteCollections_ListLong,Collections_ListStructCollections_ListContains,Collections_ListIndexOf,Collections_ListRemoveAtCollections_ListInsert,Collections_ListRemove,Collections_ListClearCollections_ListToArray,Collections_ListForeach,Collections_ListEmpty
Generic Collections – Dictionary (9 tests):
Collections_DictCustomComparer,Collections_DictAddGet,Collections_DictIndexerCollections_DictContains,Collections_DictRemove,Collections_DictClearCollections_DictTryGetValue,Collections_DictKeysValues,Collections_DictEmpty
Generic Collections – IEnumerable (1 test):
Collections_IEnumerable
Memory Copy / SIMD (15 tests):
MemCopy_8Bytes,MemCopy_16Bytes,MemCopy_24Bytes,MemCopy_32BytesMemCopy_48Bytes,MemCopy_64Bytes,MemCopy_80Bytes,MemCopy_128BytesMemCopy_256Bytes,MemCopy_264BytesMemSet_64Bytes,MemMove_Overlap,MemMove_Overlap_DestBeforeSrcMemCopy_0Bytes,MemCopy_1Byte
Array.Copy (5 tests):
ArrayCopy_IntArray,ArrayCopy_ByteArray,ArrayCopy_LargeArrayArrayCopy_ZeroLength,ArrayCopy_Overlap
Garbage Collection (22 tests):
GC_IsEnabled,GC_GetStats,GC_CollectBasic,GC_StatsIncrementGC_ExactCollectionCount,GC_ObjectSurvival,GC_StringSurvivalGC_ArraySurvival,GC_ListSurvival,GC_UnreachableExactCountGC_ObjectGraphSurvival,GC_MixedTypeSurvival,GC_AllocAfterCollectGC_WeakReference,GC_LargeAllocCollect,GC_StructArraySurvivalGC_DictSurvival,GC_PageAccounting,GC_DependentHandleGC_DependentHandleCleanup,GC_HandleStoreIntegrity,GC_PinnedHeapReuse
Running Kernel Tests
From VS Code
Using Tasks (recommended):
- Press
Ctrl+Shift+P→ "Tasks: Run Task" - Select one of:
- Run Test: HelloWorld (x64) – Console + XML output
- Run Test: HelloWorld (x64, Console Only) – Console output only
- Run Test: HelloWorld (ARM64) – ARM64 test with XML output
- Dev Test: HelloWorld (x64) – Developer mode with verbose output
Debug test runner:
- Open the Run & Debug panel (
Ctrl+Shift+D) - Select a configuration:
- Debug Test Runner (HelloWorld x64)
- Debug Test Runner (HelloWorld ARM64)
- Press
F5
From the Command Line
# Run test with XML output
dotnet run --project tests/Cosmos.TestRunner.Engine/Cosmos.TestRunner.Engine.csproj -- \
tests/Kernels/Cosmos.Kernel.Tests.HelloWorld \
x64 \
60 \
test-results.xml
# Run test with console output only
dotnet run --project tests/Cosmos.TestRunner.Engine/Cosmos.TestRunner.Engine.csproj -- \
tests/Kernels/Cosmos.Kernel.Tests.HelloWorld \
x64 \
60
Arguments:
- Kernel project path (absolute or relative)
- Architecture:
x64orarm64 - Timeout in seconds
- (Optional) XML output path (JUnit format)
- (Optional) Mode:
ciordev
Recommended timeouts:
| Suite | x64 | ARM64 |
|---|---|---|
| HelloWorld | 60 s | 90 s |
| Memory | 180 s | 300 s |
Output Formats
Console (colored)
================================================================================
Starting test suite: HelloWorld Basic Tests
Architecture: x64
Time: 2025-11-05 04:06:48
================================================================================
[1] Test_BasicArithmetic: PASSED (15ms)
[2] Test_BooleanLogic: PASSED (12ms)
[3] Test_IntegerComparison: PASSED (10ms)
================================================================================
Suite: HelloWorld Basic Tests
Total tests: 3 | Passed: 3 | Failed: 0 | Skipped: 0 | Duration: 0.04s
================================================================================
ALL TESTS PASSED
================================================================================
XML (JUnit format)
<?xml version="1.0" encoding="utf-16"?>
<testsuites name="HelloWorld Basic Tests" tests="3" failures="0" skipped="0" time="0.037">
<testsuite name="HelloWorld Basic Tests" tests="3" failures="0" skipped="0" time="0.037">
<properties>
<property name="architecture" value="x64" />
</properties>
<testcase name="Test_BasicArithmetic" classname="HelloWorld Basic Tests" time="0.015" />
<testcase name="Test_BooleanLogic" classname="HelloWorld Basic Tests" time="0.012" />
<testcase name="Test_IntegerComparison" classname="HelloWorld Basic Tests" time="0.010" />
</testsuite>
</testsuites>
Exit Codes
| Code | Meaning |
|---|---|
| 0 | All tests passed (skipped tests are acceptable) |
| 1 | Tests failed or execution error |
| 137 | Timeout (SIGKILL) |
UART Debug Protocol
Test kernels communicate results back to the host engine over a binary protocol embedded in the QEMU serial (UART) stream. The protocol is defined in tests/Cosmos.TestRunner.Protocol/ and is ported from the CosmosOS debug connector.
Framing
Every message is prefixed with a 4-byte magic signature, followed by the command byte and a 2-byte little-endian payload length:
[Magic: 4 bytes][Command: 1 byte][Length: 2 bytes LE][Payload: N bytes]
- Magic:
0x07 0x08 0x74 0x19(i.e.0x19740807in little-endian,SerialSignatureinConsts.cs) - Command: one of the
Ds2Vsconstants (see table below) - Length: number of payload bytes that follow
After the final TestSuiteEnd message the kernel also sends an 8-byte termination marker (0xDE 0xAD 0xBE 0xEF 0xCA 0xFE 0xBA 0xBE) so the engine can kill QEMU immediately without waiting for the full timeout.
Commands (Kernel → Host, Ds2Vs)
Test-runner-specific commands occupy the range 100–106. The original CosmosOS debug commands (0–25) are also defined but are not used by the test runner.
| Command | Value | Payload format | Description |
|---|---|---|---|
TestSuiteStart |
100 | [ExpectedTests: 2 LE][SuiteName: UTF-8] |
Sent once when the test suite begins |
TestStart |
101 | [TestNumber: 2 LE][TestName: UTF-8] |
Sent before each test executes |
TestPass |
102 | [TestNumber: 2 LE][DurationMs: 4 LE] |
Sent when a test passes |
TestFail |
103 | [TestNumber: 2 LE][ErrorMessage: UTF-8] |
Sent when an assertion fails |
TestSkip |
104 | [TestNumber: 2 LE][SkipReason: UTF-8] |
Sent when a test is explicitly skipped |
TestSuiteEnd |
105 | [Total: 2 LE][Passed: 2 LE][Failed: 2 LE] |
Sent once when the test suite ends |
ArchitectureInfo |
106 | [ArchId: 1][CpuCount: 1] |
Sent on kernel startup (arch IDs: 1=x86, 2=x64, 3=ARM32, 4=ARM64) |
Message Flow
A typical session looks like this:
→ TestSuiteStart (suiteName="HelloWorld Basic Tests", expectedTests=3)
→ TestStart (testNumber=1, testName="Test_BasicArithmetic")
→ TestPass (testNumber=1, durationMs=15)
→ TestStart (testNumber=2, testName="Test_BooleanLogic")
→ TestPass (testNumber=2, durationMs=12)
→ TestStart (testNumber=3, testName="Test_IntegerComparison")
→ TestPass (testNumber=3, durationMs=10)
→ TestSuiteEnd (total=3, passed=3, failed=0)
→ [0xDE 0xAD 0xBE 0xEF 0xCA 0xFE 0xBA 0xBE] ← termination marker
Parser
tests/Cosmos.TestRunner.Engine/Protocol/UartMessageParser.cs reads the raw UART log captured by QEMU (uart-output.log), scans byte-by-byte for the magic signature, validates the command byte and length, then dispatches to the appropriate parse helper.
Corruption detection: the TestSuiteEnd payload is validated by checking total == passed + failed. If this invariant does not hold (e.g. due to a timer-interrupt interleave corrupting UART bytes), the end message is ignored and results fall back to the individually tracked counters.
Host → Kernel Commands (Vs2Ds)
The test runner currently does not send commands to the kernel. The Vs2Ds class (Noop=0, Continue=4, Ping=17) is inherited from the CosmosOS debug connector and reserved for future use.
Project Structure
tests/
├── Cosmos.TestRunner.Engine/ # Host-side test runner
│ ├── Engine.cs # Main orchestration
│ ├── Engine.Build.cs # NativeAOT build pipeline
│ ├── Program.cs # CLI entry point
│ ├── TestConfiguration.cs # Configuration
│ ├── TestResults.cs # Result model
│ ├── Hosts/ # QEMU host implementations
│ │ ├── IQemuHost.cs
│ │ ├── QemuX64Host.cs
│ │ └── QemuARM64Host.cs
│ ├── OutputHandlers/ # Result output formats
│ │ ├── OutputHandlerBase.cs
│ │ ├── OutputHandlerConsole.cs # Colored terminal output
│ │ ├── OutputHandlerXml.cs # JUnit XML output
│ │ └── MultiplexingOutputHandler.cs
│ └── Protocol/
│ └── UartMessageParser.cs # Binary message parser
├── Cosmos.TestRunner.Framework/ # In-kernel test framework
│ ├── TestRunner.cs # Start / Run / Skip / Finish
│ └── Assert.cs # Assertion helpers
├── Cosmos.TestRunner.Protocol/ # Shared protocol definitions
│ ├── Consts.cs # Magic signature and constants
│ └── Messages.cs # Typed message classes
├── Cosmos.Tests.Build.Asm/ # Unit tests – Yasm build task
├── Cosmos.Tests.Build.Analyzer.Patcher/ # Unit tests – plug analyzer
├── Cosmos.Tests.Scanner/ # Unit tests – plug scanner
├── Cosmos.Tests.Patcher/ # Unit tests – IL patcher
├── Cosmos.Tests.NativeWrapper/ # Runtime assets (no tests)
├── Cosmos.Tests.NativeLibrary/ # Native code for tests (no tests)
└── Kernels/ # Kernel test projects
├── Cosmos.Kernel.Tests.HelloWorld/
│ ├── Kernel.cs
│ └── Bootloader/limine.conf
└── Cosmos.Kernel.Tests.Memory/
├── Kernel.cs
└── Bootloader/limine.conf
Writing a Test Kernel
Minimal Example
Test kernels inherit from Cosmos.Kernel.System.Kernel and run all tests inside BeforeRun(). Use the TR alias for TestRunner and Assert for assertions.
using Cosmos.Kernel.Core.IO;
using Cosmos.TestRunner.Framework;
using Sys = Cosmos.Kernel.System;
using TR = Cosmos.TestRunner.Framework.TestRunner;
namespace Cosmos.Kernel.Tests.MyTests;
public class Kernel : Sys.Kernel
{
protected override void BeforeRun()
{
// Initialize test suite (expectedTests must equal the total number of TR.Run + TR.Skip calls)
TR.Start("My Test Suite", expectedTests: 3);
TR.Run("Test_Addition", () =>
{
int result = 2 + 2;
Assert.Equal(4, result);
});
TR.Run("Test_StringOps", () =>
{
string str = "Hello";
Assert.Equal("Hello", str);
Assert.NotNull(str);
});
// Mark unsupported operations as skipped rather than letting them crash
// TR.Skip also counts toward expectedTests
TR.Skip("Test_Unsupported", "Feature not implemented");
TR.Finish();
Serial.WriteString("[Tests Complete - System Halting]\n");
Stop();
}
protected override void Run()
{
// Tests completed in BeforeRun, nothing to do here
}
protected override void AfterRun()
{
Cosmos.Kernel.Kernel.Halt();
}
}
Kernel Lifecycle
The Sys.Kernel base class drives a fixed lifecycle:
OnBoot()– system initialization (called automatically, rarely overridden)BeforeRun()– run all tests here, then callStop()Run()– called in a loop untilStop()is invoked; leave empty for test kernelsAfterRun()– called once after the loop exits; callCosmos.Kernel.Kernel.Halt()here
Available Assertions
// Equality – typed overloads (int, uint, long, byte, bool, string, byte[], int[])
Assert.Equal(expected, actual);
Assert.Equal<T>(expected, actual); // Generic overload (requires IEquatable<T>)
// Null checks
Assert.Null(obj);
Assert.NotNull(obj);
// Boolean
Assert.True(condition);
Assert.True(condition, "message");
Assert.False(condition);
// Manual failure
Assert.Fail("Custom error message");
Note:
Assertuses static failure state (no exceptions) for NativeAOT compatibility. Only the first failure per test is recorded; subsequent assertions in the sameTR.Runblock are still evaluated.
Test Status
| Status | When |
|---|---|
| Passed | Test completed without assertion failures |
| Failed | Assertion set the failure state inside TR.Run |
| Skipped | Test explicitly marked via TR.Skip(name, reason) |
Adding a New Test Suite
- Create a kernel project under
tests/Kernels/Cosmos.Kernel.Tests.{Name}/ - Copy
.csprojandBootloader/limine.conffrom an existing suite; update the ELF path inlimine.conf - Implement tests using
TestRunner.Framework - Add a CI job in
.github/workflows/kernel-tests.yml:- Copy an existing
*-testsjob, rename it, and update the kernel path - Add a corresponding
{name}-resultsjob for PR comments - Add the new job to the
test-summarydependencies
- Copy an existing
- Add VS Code tasks in
.vscode/tasks.json
CI Integration
The CI workflow (.github/workflows/kernel-tests.yml) runs kernel integration tests on both x64 and ARM64.
Jobs:
helloworld-tests– Matrix build for x64/arm64helloworld-results– Combined PR commentmemory-tests– Matrix build for x64/arm64memory-results– Combined PR commenttest-summary– Final status summary
Triggers:
- Push to
main - Pull requests (any branch)
- Manual dispatch with architecture selection
PR Comments: Each test suite posts a comment with separate rows for x64 and arm64, showing test counts, duration, and links to artifacts.
Artifacts (30-day retention):
test-results-{suite}-{arch}.xml– JUnit XML resultsuart-log-{suite}-{arch}– Full UART output{Suite}-Test-ISO-{arch}– Bootable kernel ISO + ELF
Example CI Step
- name: Run Cosmos Tests
run: |
dotnet run --project tests/Cosmos.TestRunner.Engine/Cosmos.TestRunner.Engine.csproj -- \
tests/Kernels/Cosmos.Kernel.Tests.HelloWorld \
x64 \
120 \
test-results.xml \
ci
- name: Publish Test Results
uses: dorny/test-reporter@v1
if: always()
with:
name: Cosmos Tests
path: test-results.xml
reporter: java-junit
Performance Reference
| Stage | x64 | ARM64 |
|---|---|---|
| Kernel build | ~60 s | ~70 s |
| HelloWorld execution | 2–5 s | 5–10 s |
| Memory execution | 60–120 s | 120–240 s |
Troubleshooting
Timeout
- Increase the timeout argument
- Check
uart-output.logfor boot issues - Verify QEMU is installed:
qemu-system-x86_64 --version
Build Failures
- Run
.devcontainer/postCreateCommand.shto rebuild the framework - Restore NuGet packages:
dotnet restore - Verify .NET 9 SDK is installed
ARM64 Issues
- Ensure UEFI firmware is present:
/usr/share/qemu-efi-aarch64/QEMU_EFI.fd - Install on Ubuntu:
sudo apt install qemu-efi-aarch64 - Use a longer timeout (90 s+ for HelloWorld, 180 s+ for Memory)
#UD Exceptions (Invalid Opcode)
Some operations may trigger Invalid Opcode faults depending on the runtime state. Use TR.Skip() to mark them instead of letting the kernel crash.