Files
jdb 5a2418f438 Make known Nim allocator race scenarios opt-in
Skip factory-state and allocator by default with explicit skip messages. Preserve both scenarios and enable them with NSL_TEST_RUNTIME_ALLOCATOR=1. Document the deferred runtime issue and remaining validation limits.

Validation: nimble test passes 32 unit tests, 29 regression tests, and 11 sanitizer scenarios, with two skips. Opt-in execution still reports both known allocator failures.
2026-09-29 09:11:59 -05:00

146 lines
8.7 KiB
Markdown

# Regression tests
These tests assert the intended behavior. Enabled tests treat bugs as ordinary
failures. Two scenarios that expose a known Nim allocator race are temporarily
disabled by default; they remain available for explicit runs as described below.
Run from the repository root:
| Command | Coverage |
| --- | --- |
| `nimble test` | All enabled unit, regression, and ThreadSanitizer tests under ORC with threads enabled |
| `nimble testUnit` | Embedded unit tests, including writer submission/shutdown coordination |
| `nimble testRegression` | Custom-state/formatter ownership, handle lifetime, configuration freshness, refresh failure recovery, appender-state cleanup, and writer shutdown |
| `nimble testThreadSafety` | Separate ThreadSanitizer processes for service ownership, last-owner teardown, reconfiguration, shared state, formatter closures, collector snapshots, timestamps, and writer lifecycle, plus an independent-service control; factory-state and allocator scenarios are opt-in |
`factory-state` and `allocator` are reported as **SKIP** in normal runs because
they expose the default-allocator race in Nim 2.2.10. Resolving that runtime
issue is deferred. Their implementations and assertions remain intact, and
their test code is still compiled. To enable both scenarios:
```sh
NSL_TEST_RUNTIME_ALLOCATOR=1 nimble testThreadSafety
# Or include them in the full run:
NSL_TEST_RUNTIME_ALLOCATOR=1 nimble test
```
When enabled, their failures make the command fail normally. Skipping these
scenarios does not resolve the runtime limitation or establish that factory
state is fully validated under the default allocator.
`nimble test` attempts every suite even if an earlier suite fails to compile or
run, summarizes the failing suites, and exits unsuccessfully if any suite fails.
The subset commands use the same build and execution procedures as the full run.
Old executables are removed before compilation, and a new executable must exist
before a suite runs. This prevents stale binaries from hiding compilation errors
when a nested Nimble command fails to propagate its exit status.
Executables are generated in ignored `tests/bin/`. The shutdown child is built
with `-d:multithreaded`; `--threads:on` alone does not enable background writers.
The ownership tests do not need background I/O. The embedded writer lifecycle
tests call the internal writer functions directly; their sanitizer build is
`tests/bin/writer_lifecycle`.
The full run and the sanitizer subset require Linux, Bash, GNU `timeout`, and a
C compiler and runtime supporting `-fsanitize=thread`. The sanitizer instruments
the generated Nim C code, including the ORC runtime. A sanitizer report, timeout,
crash, or runtime startup failure causes a nonzero result; consult the diagnostic
to distinguish these.
Each scenario runs separately so one failure does not hide another. To inspect
one scenario after building:
```sh
TSAN_OPTIONS=halt_on_error=1:exitcode=66 timeout 30s tests/bin/thread_safety service
TSAN_OPTIONS=halt_on_error=1:exitcode=66 timeout 30s tests/bin/writer_lifecycle 'Background writer lifecycle::*'
```
The worker start barrier ensures argument setup finishes before the concurrent
operations begin. In `service-lifetime`, workers first acquire local services;
the parent then releases the original handle before allowing them to proceed.
Thread arguments contain borrowed pointers so no hidden owning argument keeps
the service alive. Atomic construction/destruction counters verify that every
tracked appender is released exactly once after the workers join.
`service-reconfigure` exercises concurrent updates and local cache refresh.
`shared-state-lifetime` drops the original state handle before concurrent
mutations and verifies one final destruction with all updates present.
`formatter-lifetime` drops an already-used original formatter and checks both
independent closure counters and exact cleanup on worker threads. `factory-state`
uses per-thread cyclic reference graphs, explicitly runs the ORC collector, and
checks that each worker maintains its own counter. `collector` concurrently
records messages containing JSON and exceptions and mutates returned snapshots.
These ownership scenarios use explicit timestamps where messages are needed.
The separate `timestamp` case exercises both `initLogMessage()` overloads and
structured JSON formatting concurrently. Generated timestamps use UTC, avoiding
the libc local-time conversion race previously reached via `std/times.now()`.
The timestamp regressions verify the captured instant, UTC timezone, and `Z`
suffix in structured output.
`allocator` calls the standalone reproduction in `runtime_allocator.nim`, which
imports no logging code. It exercises sequence growth and clearing under a lock
alongside thread-local closure allocations. On Nim 2.2.10 it reproduces a report
in `system/alloc.nim`'s `compensateCounters` / shared-free-list path. The same
runtime report can occur in `factory-state` and other allocation-heavy cases.
The ten ownership scenarios passed debug and release diagnostic builds with
`-d:useMalloc`. Normal runs retain the default allocator and skip the two
scenarios above. Sanitizer reports in any enabled scenario still fail the run;
there are no sanitizer suppressions.
The original custom-state and formatter scenarios retain parent owners until all
workers join. They only read application data; their concurrent writes are the
implicit ownership operations caused by cloning and destruction. The independent
case creates a separate service inside each worker and should pass. Custom-state
and formatter cloning is serialized with a lock, matching configuration refresh;
the copied appenders are used and destroyed outside that lock.
`ownership.nim`, imported by `regressions.nim`, checks independent snapshots,
exception recovery, factory and clone behavior, and the built-in formatter paths.
Compile-time checks reject direct and nested refs, captured formatter arguments,
and embedded synchronization primitives. The formerly unsafe custom-state and
formatter scenarios now exercise the supported shared-handle and factory APIs;
the unsafe forms are tested as compilation failures.
The in-process regression tests are deterministic. The lifetime suite checks
copying, self-assignment, replacement, moving, container copies, and exporting a
handle from a local service. `isEnabled` tests verify that namespace thresholds
remain cached until an explicit refresh, a logging call, or `getLogger`, while
the root fallback is read from shared state. This behavior is intentional so
guards can avoid configuration refresh work.
Refresh tests verify that repeated clone failures
preserve the old appenders and thresholds, release partial replacements, and
allow a later retry without another configuration change. A successful refresh
releases the replaced cache and installs both appenders and thresholds.
Shutdown tests launch
a fresh process for each scenario and check all 10,000 numbered messages after
normal exit, using isolated temporary paths and a 15-second deadline. Scenarios
cover stdout, stderr, files, empty formatted messages, multiple file destinations,
and four joined producers. They verify message content and order within each
producer and destination, using the real exit hook without sleeps or explicit
flushes. File batches must preserve blank records and finish their final batch
before the writer stops.
The embedded lifecycle tests start producers, release them to submit more
records while the file writer shuts down, and verify every accepted record.
They also check repeated shutdown, rejection of sends after shutdown, and
waking the console writer after an empty message. The sanitizer task runs these
tests as a separate process even when another scenario fails.
Nim 2.2.10's `Channel.close()` destroys its internal mutex while holding it,
which the lifecycle sanitizer test exposed after the writers had joined.
Shutdown therefore drains and joins the writers, rejects further submissions,
and leaves the process-wide channels allocated until process exit. It does not
call the faulty runtime teardown or suppress its diagnostics.
The suite provides targeted regressions; it does not exhaust thread scheduling
or ORC cycle-collector interleavings.
The ownership scenarios, lifetime suite, and appender-state cleanup regression
exercise the new ownership contracts. The known allocator issue remains
unresolved, with its failing scenarios temporarily disabled by default.
The standalone `ownership.nim` regression suite passed AddressSanitizer and
LeakSanitizer with `--mm:orc -d:useMalloc`. An additional worker-thread build found
no invalid accesses in the shared-state, factory-state, formatter-lifetime, and collector
scenarios. LeakSanitizer reported per-thread ORC cycle-registration buffers
allocated by the Nim runtime; this is not a clean leak-check result.