import std/[atomics, locks, options, tables, unittest] import ../src/namespaced_logging type UnsafeState = ref object value: int NestedUnsafeState = object values: seq[UnsafeState] VariantUnsafeState = object case hasReference: bool of true: reference: UnsafeState of false: value: int EmbeddedLock = object lock: Lock Slot = enum firstSlot, secondSlot proc unsafeCallback(state: var UnsafeState, msg: LogMessage) {.gcsafe, noconv.} = discard proc nestedCallback(state: var NestedUnsafeState, msg: LogMessage) {.gcsafe, noconv.} = discard proc makePrefix(prefix: string): StatefulLogFormatterFunc = result = proc(msg: LogMessage): string {.gcsafe.} = prefix & msg.msg static: doAssert not compiles(initSharedLogState(UnsafeState())) doAssert not compiles(initSharedLogState(NestedUnsafeState())) doAssert not compiles(initSharedLogState(VariantUnsafeState())) doAssert not compiles(initSharedLogState(some(UnsafeState()))) doAssert not compiles(initCustomLogAppender(UnsafeState(), unsafeCallback)) doAssert not compiles(initCustomLogAppender(NestedUnsafeState(), nestedCallback)) doAssert not compiles(initConsoleLogAppender(formatter = makePrefix("unsafe"))) doAssert not compiles(initSharedLogState(makePrefix("unsafe"))) doAssert not compiles(initSharedLogState(default(Lock))) doAssert not compiles(initSharedLogState(default(Atomic[int]))) doAssert not compiles(initSharedLogState(default(EmbeddedLock))) doAssert compiles(initSharedLogState(default(array[Slot, string]))) doAssert compiles(initSharedLogState(initTable[string, seq[string]]())) proc counterFormatter(prefix: string): StatefulLogFormatterFunc = var count = 0 result = proc(msg: LogMessage): string {.gcsafe.} = inc count prefix & $count & ": " & msg.msg type ValueCounter = object count: int output: SharedLogState[seq[int]] LocalCounter = ref object count: int output: SharedLogState[seq[int]] proc initCounter(output: SharedLogState[seq[int]]): LocalCounter = LocalCounter(output: output) proc recordCounter(state: var LocalCounter, msg: LogMessage) {.gcsafe, noconv.} = inc state.count let count = state.count state.output.withState(proc(values: var seq[int]) = values.add(count)) proc recordCounter(state: var ValueCounter, msg: LogMessage) {.gcsafe, noconv.} = inc state.count let count = state.count state.output.withState(proc(values: var seq[int]) = values.add(count)) proc ignoreError(err: ref Exception, msg: string) {.gcsafe, nimcall.} = discard suite "Custom state and formatter ownership": test "shared value snapshots are independent and updates are explicit": let state = initSharedLogState(@[@["original"]]) var first = state.snapshot() first[0][0] = "changed" check state.snapshot() == @[@["original"]] state.withState(proc(value: var seq[seq[string]]) = value[0].add("second")) check state.snapshot() == @[@["original", "second"]] test "callback exceptions release the shared state lock": let state = initSharedLogState(0) expect ValueError: state.withState(proc(value: var int) = inc value raise newException(ValueError, "injected failure")) check state.snapshot() == 1 state.withState(proc(value: var int) = inc value) check state.snapshot() == 2 test "a callback can release its caller's last handle": var state = initSharedLogState(@["alive"]) state.withState(proc(value: var seq[string]) = state = default(typeof(state)) value.add("still alive") check value == @["alive", "still alive"]) expect ValueError: discard state.snapshot() test "empty and moved handles reject access": var original = initSharedLogState(42) let moved = move(original) check moved.snapshot() == 42 expect ValueError: discard original.snapshot() expect ValueError: original.withState(proc(value: var int) = discard) test "appender factories retain local state and reset it on clone": let output = initSharedLogState(newSeq[int]()) var service = initLogService() service.addAppender(initFactoryCustomLogAppender(output, initCounter, recordCounter)) let first = service.threadLocalRef().getLogger("first") let second = service.threadLocalRef().getLogger("second") first.info("one") first.info("two") second.info("one") check output.snapshot() == @[1, 2, 1] test "value appender state is mutable and independently copied": let output = initSharedLogState(newSeq[int]()) var service = initLogService() service.addAppender(initCustomLogAppender(ValueCounter(output: output), recordCounter)) let first = service.threadLocalRef().getLogger("first") let second = service.threadLocalRef().getLogger("second") first.info("one") first.info("two") second.info("one") check output.snapshot() == @[1, 2, 1] test "a failed appender factory is retried on the next message": let calls = initSharedLogState(0) proc factory(calls: SharedLogState[int]): UnsafeState = calls.withState(proc(count: var int) = inc count) if calls.snapshot() == 1: raise newException(ValueError, "injected failure") UnsafeState() var service = initLogService(errorHandler = ignoreError) service.addAppender(initFactoryCustomLogAppender(calls, factory, unsafeCallback)) let logger = service.threadLocalRef().getLogger("worker") logger.info("first attempt") logger.info("retry") logger.info("reuse") check calls.snapshot() == 2 test "formatter clones create independent captured state": let original = initLogFormatter("count ", counterFormatter) let message = LogMessage(msg: "hello") check original.format(message) == "count 1: hello" let copied = clone(original) check original.format(message) == "count 2: hello" check copied.format(message) == "count 1: hello" test "every built-in appender clones its formatter configuration": let formatter = initLogFormatter("count ", counterFormatter) let message = LogMessage(msg: "hello") let console = initConsoleLogAppender(formatter) let file = initFileLogAppender("unused.log", formatter) let standard = initStdLoggingAppender(formatter = formatter) check console.formatter.format(message) == "count 1: hello" check file.formatter.format(message) == "count 1: hello" check standard.formatter.format(message) == "count 1: hello" check console.formatter.format(message) == "count 2: hello" check ConsoleLogAppender(clone(console)).formatter.format(message) == "count 1: hello" check FileLogAppender(clone(file)).formatter.format(message) == "count 1: hello" check StdLoggingAppender(clone(standard)).formatter.format(message) == "count 1: hello" test "a failed formatter factory is retried": let calls = initSharedLogState(0) proc factory(calls: SharedLogState[int]): StatefulLogFormatterFunc = calls.withState(proc(count: var int) = inc count) if calls.snapshot() == 1: raise newException(ValueError, "injected failure") makePrefix("ready: ") let formatter = initLogFormatter(calls, factory) expect ValueError: discard formatter.format(LogMessage(msg: "hello")) check formatter.format(LogMessage(msg: "hello")) == "ready: hello" check formatter.format(LogMessage(msg: "again")) == "ready: again" check calls.snapshot() == 2