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.Discovery via JAR Scanning
When the server starts — or when you run/hrtk scan — the TestDiscoveryEngine iterates every loaded JavaPlugin, opens its JAR file, and scans every .class entry for HRTK annotations.
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.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:
| Command | Purpose |
|---|---|
/hrtk run | Run all tests (or filter by plugin/suite/tag) |
/hrtk bench | Run benchmarks only |
/hrtk scan | Re-discover tests from loaded plugins |
/hrtk list | List all discovered tests |
/hrtk results | Show results from the last run |
/hrtk export | Export results to JSON |
/hrtk watch | Auto-rerun tests when a plugin reloads |
Threading Model
HRTK uses two execution paths depending on the test type:Standard tests (no world required)
Standard tests (no world required)
Tests that do not require a world context (pure logic, codec, basic assertions) are submitted to
CompletableFuture.runAsync() and awaited with the configured timeout. They execute on the common ForkJoinPool, keeping the server’s main threads free.World-bound tests (@EcsTest, @WorldTest, @FlowTest)
World-bound tests (@EcsTest, @WorldTest, @FlowTest)
Tests that interact with the ECS store or world state are scheduled onto the world thread via
world.execute(). This ensures all ECS operations happen on the correct thread, respecting Hytale’s single-threaded-per-world model. The test runner awaits completion via a CompletableFuture with the configured timeout.Crash Protection
A core design goal of HRTK is that a test must never crash the server. The test executor wraps every invocation in acatch (Throwable) block — not just Exception, but Throwable. This catches:
AssertionFailedException— test failures, reported asFAILEDRuntimeException,NullPointerException, etc. — unexpected errors, reported asERROREDStackOverflowError,OutOfMemoryError— JVM-level errors, reported asERRORED
What Happens at a Glance
Discovery
On server start,
TestDiscoveryEngine scans all plugin JARs for HRTK annotations and builds the PluginTestRegistry.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.