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.
146 lines
8.7 KiB
Markdown
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.
|