Attributes
All attributes live in sparkles.test_runner.attributes and are plain marker types — a test annotated with them remains an ordinary unittest block for any other runner. Import them unconditionally (not under version (unittest)): unittest UDAs are resolved even in builds that do not compile the unittest bodies.
@betterC
The test is -betterC-compatible (no GC, exceptions, TypeInfo, druntime).
- Runs normally under
dub test. --better-cextracts it into a standalone druntime-free program.- The body may only use the module's public symbols. Its own module is compiled in by default, so ordinary functions work; other modules are opted in with
--include-import. @betterC(selfContained: true)keeps the module out entirely, limiting the body to templates/CTFE-able code — which is what lets a module that cannot compile under-betterCstill host such a test.
@ctfe
The test runs at compile time instead of runtime.
- Evaluated through CTFE by a runner-generated probe compiled with
-o- -unittest(semantic analysis only) after-i/-efiltering — so filters control which tests execute, and--help/--listwork even when an@ctfetest would fail. - Reported as
⚙ … (compile time)on success,✗ … (compile time)plus the compiler's CTFE error trail on failure; never executed at runtime. - The body must be CTFE-able; needs a D compiler on
PATH(or$DC/--compiler) at run time. - Named after (and forward-compatible with) DMD 2.113's
@__ctfefunction attribute.
@wasm
The test is WebAssembly-compatible.
- Runs normally under
dub test. --wasmcross-compiles it towasm32with LDC and runs it undernode/deno/bun/wasmtime.- All
@betterCconstraints apply, and with a stock LDC the module's import chain must avoid druntime headers that do not supportwasm32.
@benchmark / @benchmark(iterations: N)
The test is a benchmark.
- Skipped by normal runs (counted in the summary); measured by
--bench. iterationspins the count;0(default) auto-scales. Batched timing (benchIter/whole-body) pins the per-sample iteration count; a per-callbenchCaseruns exactly N timed calls, one sample each.- The whole body is the measured unit by default. From
sparkles.test_runner.bench:benchItermeasures a sub-section,benchCaseemits many rows from one test (a matrix, withMetricthroughput columns), andblackBoxis the optimizer barrier. --perfadds hardware-counter columns to the--benchtable: Linuxperf_event(IPC, instructions/iter, cache/branch miss rates) or macOSproc_pid_rusagefixed counters (IPC + instructions/iter, process-wide).
@workload / @workload(reps: N)
The test is a workload, measured under the window model — the counterpart to @benchmark's per-iteration statistics.
- Skipped by normal runs (counted in the summary); measured by
--benchin a single pass: the body runs once (repstimes for the measured window content;0is treated as1), and the runner reports each open counter source's deltas across the window plus a wall-clock decomposition (on-CPU user/kernel from rusage, runqueue wait from schedstat, and a clampedotherresidual — locks/sleeps are never attributed to a fabricated cause). The body is never re-run for counting, so expensive or non-idempotent workloads are safe. - The whole body is the window by default. From
sparkles.test_runner.workload,workloadWindow(dg)/workloadWindow(name, dg)measures only the closure (× reps); each call is one row in theworkloadstable. Outside--benchthe closure runs exactly once, inertly. --perf/--syscalls/--metricsopen the same sources for windows; the table shows a fixed summary column set per source (full totals in--bench-json'swindowsarray).- On Linux the decomposition is thread-scoped (the driving thread); rusage's user/kernel split is tick-sampled, so windows should be long (tens of milliseconds up) for the split to be signal rather than quantization.
@workload(regime: CacheRegime.cold)(orwarm) sets the page-cache regimeworkloadFiles(paths...)establishes and verifies for the files the body names — see the how-to's regime section.
See Measure workloads.
Combining
Attributes compose freely — e.g. @betterC @wasm opts one test into both extra environments; @("name") string UDAs keep naming the test. The one exclusion: @benchmark and @workload are different measurement models for the same body, and combining them is a discovery-time error.