Files
nim-namespaced-logging/tests
jdb 46245b3f1e Create log timestamps in UTC to avoid local-time races
Use getTime().utc in both message constructors, document the timezone change, and cover concurrent construction and structured formatting. Restore ordinary logging calls in the parallel writer fixture.

Validation: 32 unit tests and 29 regression tests pass. Timestamp sanitizer checks pass under UTC and non-UTC timezones. The full suite retains the known allocator reports in factory-state and allocator.
2026-09-29 09:03:31 -05:00
..

Regression tests

These tests assert the intended behavior. Known bugs are ordinary failures, not expected failures: the tasks should become green as the defects are fixed.

Run from the repository root:

Command Coverage
nimble test All 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, factory state, formatter closures, collector snapshots, timestamps, the Nim allocator, and writer lifecycle, plus an independent-service control

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:

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; nimble test deliberately retains the default allocator and exposes these reports rather than suppressing them.

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. Allocator reports remain for subsequent work.

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.