Define explicit ownership for custom appenders and formatters
Add SharedLogState for synchronized value data and factories for independently owned appender state and captured formatter closures. Reject unsafe configuration state, update the test collector to value snapshots, and document the breaking APIs in version 3.0.0. Expand ORC ownership and sanitizer coverage and reject stale test executables.
This commit is contained in:
1 parent
851caef706
commit
b2d0e9dade
11 files changed
+808
-110
No files matched your search
@@ -0,0 +1,170 @@
|
||||
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
|
||||
Reference in new issue
Block a user