import std/[os, osproc, strutils, tempfiles, unittest] import ../src/namespaced_logging type CloneControl = object failNext: bool FallibleAppender = ref object of LogAppender control: ptr CloneControl method clone(app: FallibleAppender): LogAppender {.gcsafe.} = if app.control.failNext: app.control.failNext = false raise newException(ValueError, "injected clone failure") FallibleAppender(control: app.control) type LifetimeProbeObj = object destroyed: ptr int LifetimeProbe = ref LifetimeProbeObj proc `=destroy`(probe: LifetimeProbeObj) = inc probe.destroyed[] proc ignoreMessage(state: LifetimeProbe, msg: LogMessage) {.gcsafe, noconv.} = discard proc watchedService(destroyed: ptr int): LogService = result = initLogService() result.addAppender(initCustomLogAppender( LifetimeProbe(destroyed: destroyed), ignoreMessage)) proc createAndReleaseService(destroyed: ptr int) = var service = watchedService(destroyed) service.setThreshold("worker", lvlInfo) # All owning handles leave scope here. No thread or cycle is involved. suite "ORC service handle lifetime": test "copy, self-assignment, replacement, and move retain exactly one lifetime": var firstDestroyed, replacedDestroyed = 0 block: var first = watchedService(addr firstDestroyed) var other = watchedService(addr replacedDestroyed) other = first check replacedDestroyed == 1 first = first first = LogService() check firstDestroyed == 0 var moved = move(other) expect ValueError: discard other.threadLocalRef() moved.setThreshold("worker", lvlInfo) let local = moved.threadLocalRef() moved = LogService() check firstDestroyed == 0 check local.getLogger("worker").isEnabled(lvlInfo) check firstDestroyed == 1 check replacedDestroyed == 1 test "handles embedded in copied containers retain the service": var destroyed = 0 block: var source = watchedService(addr destroyed) var handles = @[source, source] var copied = handles source = LogService() handles.setLen(0) check destroyed == 0 copied[0].setThreshold("worker", lvlWarn) let local = copied[1].threadLocalRef() check not local.getLogger("worker").isEnabled(lvlInfo) copied.setLen(0) check destroyed == 0 check destroyed == 1 test "an exported handle outlives the local service it came from": var destroyed = 0 block: var exported: LogService block: let local = watchedService(addr destroyed).threadLocalRef() exported = local.service check destroyed == 0 check exported.threadLocalRef().getLogger("worker").isEnabled(lvlInfo) check destroyed == 1 test "failed local construction releases its shared owner and partial cache": var destroyed = 0 var control: CloneControl block: var service = watchedService(addr destroyed) service.addAppender(FallibleAppender(control: addr control)) control.failNext = true expect ValueError: discard service.threadLocalRef() check destroyed == 0 check destroyed == 1 suite "ORC ownership and configuration regressions": test "isEnabled observes a namespace change before any log call": var service = initLogService() service.setThreshold("worker", lvlError) let local = service.threadLocalRef() let logger = local.getLogger("worker") check not logger.isEnabled(lvlInfo) # Simulate a change from another service handle without introducing a # reference-count race into this deterministic configuration test. service.setThreshold("worker", lvlInfo) check logger.isEnabled(lvlInfo) test "a failed refresh is retried without another configuration change": var control: CloneControl var service = initLogService() service.addAppender(FallibleAppender(control: addr control)) let local = service.threadLocalRef() discard local.getLogger("worker") service.setRootThreshold(lvlInfo) control.failNext = true expect ValueError: local.ensureFreshness() # clone now succeeds; the failed refresh must not have marked the # incomplete local configuration as current. local.ensureFreshness() check local.appenders().len == 1 test "releasing the service releases state owned by its appenders": var destroyed = 0 createAndReleaseService(addr destroyed) check destroyed == 1 suite "Background writer shutdown regressions": for destination in ["file", "console"]: test destination & " writer drains accepted messages at normal process exit": let directory = createTempDir("namespaced_logging_shutdown_", "") let outputPath = directory / "messages.log" defer: removeDir(directory) let child = startProcess(parentDir(getAppFilename()) / "writer_child", args = [destination, outputPath], options = {poParentStreams}) defer: close(child) let exitCode = child.waitForExit(15_000) if exitCode == -1: child.kill() discard child.waitForExit() check exitCode == 0 require fileExists(outputPath) let received = readFile(outputPath).splitLines() # Check every record and its order, not just file existence. splitLines # includes one final empty item because each record ends with a newline. check received.len == 10_001 for i in 0 ..< min(10_000, received.len - 1): check received[i] == "message " & $i