Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.hrtk.frotty27.com/llms.txt

Use this file to discover all available pages before exploring further.

HRTK is a runtime testing framework purpose-built for Hytale server plugins. Unlike traditional test frameworks that run before deployment, HRTK executes tests inside a live server where the ECS, worlds, and plugin systems are fully operational. This page explains the key architectural decisions that make that possible.

Two-Artifact Design

HRTK ships as two separate JARs with distinct roles:

hrtk-api

Compile-only dependency. Contains annotations (@HytaleTest, @HytaleSuite, etc.), assertion classes (HytaleAssert, EcsAssert, WorldAssert), context interfaces (TestContext, EcsTestContext), and mock types. Your mod compiles against this JAR, but it is never loaded at runtime as a standalone plugin.

hrtk-server

Server plugin. Loaded by the Hytale server as a regular JavaPlugin. Contains the discovery engine, test runner, context implementations, isolation managers, the /hrtk command tree, and the result exporter. This is the only JAR that needs to be in your server’s plugins/ folder.
This separation means your mod’s production JAR carries zero runtime overhead from HRTK. The API classes are erased at compile time; the annotations survive in the bytecode for the server plugin to discover.

Discovery via JAR Scanning

When the server starts - or when you run /hrtk scan - the discovery engine looks through every loaded plugin’s JAR file to find classes with HRTK annotations like @HytaleTest.
TestDiscoveryEngine.scanAll()
  -> PluginManager.get().getPlugins()
    -> For each JavaPlugin:
       -> Open JAR via plugin.getFile()
       -> Walk .class entries (skip inner classes)
       -> Load class via plugin's ClassLoader
       -> Check for @HytaleTest, @EcsTest, @WorldTest, @Benchmark, etc.
       -> Build TestClassInfo + TestMethodInfo
       -> Register in PluginTestRegistry
Inner classes (entries containing $) are skipped during scanning. If you need helper classes for your tests, use top-level or static nested classes annotated with @HytaleSuite.
The registry groups discovered tests by plugin name, then by suite. Each TestClassInfo captures lifecycle hooks (@BeforeAll, @AfterAll, @BeforeEach, @AfterEach), isolation strategy, class-level tags, and the ordered list of test methods.

Command-Triggered Execution

Tests never run automatically. They are triggered through the /hrtk command tree:
CommandPurpose
/hrtk runRun all tests (or filter by plugin/suite/tag)
/hrtk benchRun benchmarks only
/hrtk scanRe-discover tests from loaded plugins
/hrtk listList all discovered tests
/hrtk resultsShow results from the last run
/hrtk exportExport results to JSON
/hrtk watchAuto-rerun tests when a plugin reloads
See Commands for the full reference.

Threading Model

HRTK uses two execution paths depending on the test type:
Tests that don’t need a world (pure logic, codec checks, basic assertions) run on a background thread. They don’t block the server’s main threads.
Tests that interact with entities or world state run on the world thread - the same thread that processes the game loop. This ensures all entity operations happen safely. The test runner waits for completion with the configured timeout.
Because world-bound tests run on the world thread, a test that hangs would stall the game loop. The timeout (default 30 seconds) prevents this - if a test does not finish in time, it is marked TIMED_OUT and the runner moves on.

Crash Protection

A core design goal of HRTK is that a test must never crash the server. The test executor catches every possible error, not just regular exceptions. This catches:
  • Assertion failures - your test’s assertions failed, reported as FAILED
  • Runtime errors (NullPointerException, etc.) - unexpected bugs, reported as ERRORED
  • JVM-level errors (StackOverflowError, OutOfMemoryError) - severe issues, reported as ERRORED
try {
    methodInfo.getMethod().invoke(suiteInstance, params);
} catch (Throwable t) {
    // A test must NEVER crash the server.
    Throwable cause = unwrapCause(t);
    if (cause instanceof AssertionFailedException) {
        return TestResult(... TestStatus.FAILED ...);
    } else {
        return TestResult(... TestStatus.ERRORED ...);
    }
}
Even if your test accidentally triggers an infinite recursion or a native memory error, the server keeps running. The test is simply marked as errored and the runner continues to the next test.

What Happens at a Glance

1

Discovery

On server start, TestDiscoveryEngine scans all plugin JARs for HRTK annotations and builds the PluginTestRegistry.
2

Trigger

An operator runs /hrtk run (with optional filters for plugin, suite, tag, or method).
3

Execution

The TestRunner iterates matching suites. For each suite, SuiteExecutor instantiates the test class, applies isolation, runs lifecycle hooks, and delegates to TestExecutor for each method.
4

Results

Results are collected by ResultCollector, formatted by ResultFormatter for console output, and optionally exported to JSON via ResultExporter.
Next, learn how to write your first test.