mirror of
https://github.com/Nanaloveyuki/BitLogger.git
synced 2026-09-08 02:14:01 +00:00
✅ split sync tests by behavior boundary
This commit is contained in:
@@ -0,0 +1,335 @@
|
||||
///|
|
||||
test "callback sink receives record" {
|
||||
let captured_target : Ref[String] = Ref("")
|
||||
let captured_message : Ref[String] = Ref("")
|
||||
let logger = Logger::new(
|
||||
callback_sink(fn(rec) {
|
||||
captured_target.val = rec.target
|
||||
captured_message.val = rec.message
|
||||
}),
|
||||
min_level=Level::Info,
|
||||
target="callback",
|
||||
)
|
||||
logger.info("hello")
|
||||
inspect(captured_target.val, content="callback")
|
||||
inspect(captured_message.val, content="hello")
|
||||
}
|
||||
|
||||
///|
|
||||
test "split sink routes records by predicate" {
|
||||
let left_messages : Ref[Array[String]] = Ref([])
|
||||
let right_messages : Ref[Array[String]] = Ref([])
|
||||
let logger = Logger::new(
|
||||
split_sink(
|
||||
callback_sink(fn(rec) { left_messages.val.push(rec.message) }),
|
||||
callback_sink(fn(rec) { right_messages.val.push(rec.message) }),
|
||||
fn(rec) { rec.target == "audit" },
|
||||
),
|
||||
min_level=Level::Info,
|
||||
target="main",
|
||||
)
|
||||
logger.info("drop to right")
|
||||
logger.log(Level::Info, "keep on left", target="audit")
|
||||
inspect(left_messages.val.length(), content="1")
|
||||
inspect(left_messages.val[0], content="keep on left")
|
||||
inspect(right_messages.val.length(), content="1")
|
||||
inspect(right_messages.val[0], content="drop to right")
|
||||
}
|
||||
|
||||
///|
|
||||
test "split_by_level routes warn and error separately" {
|
||||
let high_messages : Ref[Array[String]] = Ref([])
|
||||
let low_messages : Ref[Array[String]] = Ref([])
|
||||
let logger = Logger::new(
|
||||
split_by_level(
|
||||
callback_sink(fn(rec) {
|
||||
high_messages.val.push(rec.level.label() + ":" + rec.message)
|
||||
}),
|
||||
callback_sink(fn(rec) {
|
||||
low_messages.val.push(rec.level.label() + ":" + rec.message)
|
||||
}),
|
||||
min_level=Level::Warn,
|
||||
),
|
||||
min_level=Level::Trace,
|
||||
target="split",
|
||||
)
|
||||
logger.info("info")
|
||||
logger.warn("warn")
|
||||
logger.error("error")
|
||||
inspect(high_messages.val.length(), content="2")
|
||||
inspect(high_messages.val[0], content="WARN:warn")
|
||||
inspect(high_messages.val[1], content="ERROR:error")
|
||||
inspect(low_messages.val.length(), content="1")
|
||||
inspect(low_messages.val[0], content="INFO:info")
|
||||
}
|
||||
|
||||
///|
|
||||
test "buffered sink flushes manually" {
|
||||
let flushed_messages : Ref[Array[String]] = Ref([])
|
||||
let sink = buffered_sink(
|
||||
callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }),
|
||||
flush_limit=10,
|
||||
)
|
||||
let logger = Logger::new(sink, min_level=Level::Info, target="buffered")
|
||||
logger.info("one")
|
||||
logger.info("two")
|
||||
inspect(sink.pending_count(), content="2")
|
||||
inspect(flushed_messages.val.length(), content="0")
|
||||
sink.flush()
|
||||
inspect(sink.pending_count(), content="0")
|
||||
inspect(flushed_messages.val.length(), content="2")
|
||||
inspect(flushed_messages.val[0], content="one")
|
||||
inspect(flushed_messages.val[1], content="two")
|
||||
}
|
||||
|
||||
///|
|
||||
test "buffered sink flushes automatically at limit" {
|
||||
let flushed_messages : Ref[Array[String]] = Ref([])
|
||||
let sink = buffered_sink(
|
||||
callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }),
|
||||
flush_limit=2,
|
||||
)
|
||||
let logger = Logger::new(sink, min_level=Level::Info, target="buffered")
|
||||
logger.info("one")
|
||||
inspect(sink.pending_count(), content="1")
|
||||
logger.info("two")
|
||||
inspect(sink.pending_count(), content="0")
|
||||
inspect(flushed_messages.val.length(), content="2")
|
||||
inspect(flushed_messages.val[0], content="one")
|
||||
inspect(flushed_messages.val[1], content="two")
|
||||
}
|
||||
|
||||
///|
|
||||
test "filter sink only forwards matching records" {
|
||||
let flushed_messages : Ref[Array[String]] = Ref([])
|
||||
let sink = filter_sink(
|
||||
callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }),
|
||||
fn(rec) { rec.target == "kept" },
|
||||
)
|
||||
let kept = Logger::new(sink, min_level=Level::Info, target="kept")
|
||||
let dropped = Logger::new(sink, min_level=Level::Info, target="dropped")
|
||||
kept.info("one")
|
||||
dropped.info("two")
|
||||
kept.info("three")
|
||||
inspect(flushed_messages.val.length(), content="2")
|
||||
inspect(flushed_messages.val[0], content="one")
|
||||
inspect(flushed_messages.val[1], content="three")
|
||||
}
|
||||
|
||||
///|
|
||||
test "logger with_filter composes naturally" {
|
||||
let flushed_messages : Ref[Array[String]] = Ref([])
|
||||
let logger = Logger::new(
|
||||
callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }),
|
||||
min_level=Level::Info,
|
||||
target="app",
|
||||
).with_filter(fn(rec) { rec.target == "app.worker" })
|
||||
logger.info("drop at app")
|
||||
logger.child("worker").info("keep at worker")
|
||||
inspect(flushed_messages.val.length(), content="1")
|
||||
inspect(flushed_messages.val[0], content="keep at worker")
|
||||
}
|
||||
|
||||
///|
|
||||
test "filter helpers support target level and message composition" {
|
||||
let flushed_messages : Ref[Array[String]] = Ref([])
|
||||
let logger = Logger::new(
|
||||
callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }),
|
||||
min_level=Level::Trace,
|
||||
target="service",
|
||||
).with_filter(
|
||||
all_of([
|
||||
target_has_prefix("service"),
|
||||
level_at_least(Level::Info),
|
||||
message_contains("visible"),
|
||||
]),
|
||||
)
|
||||
logger.debug("visible debug")
|
||||
logger.info("hidden info")
|
||||
logger.child("api").info("visible info")
|
||||
inspect(flushed_messages.val.length(), content="1")
|
||||
inspect(flushed_messages.val[0], content="visible info")
|
||||
}
|
||||
|
||||
///|
|
||||
test "field helpers can match and negate records" {
|
||||
let flushed_messages : Ref[Array[String]] = Ref([])
|
||||
let logger = Logger::new(
|
||||
callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }),
|
||||
min_level=Level::Info,
|
||||
target="fields",
|
||||
).with_filter(
|
||||
all_of([
|
||||
has_field("request_id"),
|
||||
field_equals("kind", "audit"),
|
||||
not_(target_is("fields.drop")),
|
||||
]),
|
||||
)
|
||||
logger.info("missing field")
|
||||
logger.info("wrong kind", fields=[
|
||||
field("request_id", "1"),
|
||||
field("kind", "trace"),
|
||||
])
|
||||
logger
|
||||
.child("drop")
|
||||
.info("blocked target", fields=[
|
||||
field("request_id", "2"),
|
||||
field("kind", "audit"),
|
||||
])
|
||||
logger.info("kept", fields=[field("request_id", "3"), field("kind", "audit")])
|
||||
inspect(flushed_messages.val.length(), content="1")
|
||||
inspect(flushed_messages.val[0], content="kept")
|
||||
}
|
||||
|
||||
///|
|
||||
test "any_of helper accepts multiple predicates" {
|
||||
let flushed_messages : Ref[Array[String]] = Ref([])
|
||||
let logger = Logger::new(
|
||||
callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }),
|
||||
min_level=Level::Info,
|
||||
target="multi",
|
||||
).with_filter(
|
||||
any_of([target_is("multi.keep"), field_equals("force", "true")]),
|
||||
)
|
||||
logger.info("drop")
|
||||
logger.child("keep").info("keep by target")
|
||||
logger.info("keep by field", fields=[field("force", "true")])
|
||||
inspect(flushed_messages.val.length(), content="2")
|
||||
inspect(flushed_messages.val[0], content="keep by target")
|
||||
inspect(flushed_messages.val[1], content="keep by field")
|
||||
}
|
||||
|
||||
///|
|
||||
test "patch sink can rewrite message target and fields" {
|
||||
let captured_target : Ref[String] = Ref("")
|
||||
let captured_message : Ref[String] = Ref("")
|
||||
let captured_fields : Ref[Array[Field]] = Ref([])
|
||||
let logger = Logger::new(
|
||||
callback_sink(fn(rec) {
|
||||
captured_target.val = rec.target
|
||||
captured_message.val = rec.message
|
||||
captured_fields.val = rec.fields
|
||||
}),
|
||||
min_level=Level::Info,
|
||||
target="auth",
|
||||
).with_patch(
|
||||
compose_patches([
|
||||
set_target("audit.auth"),
|
||||
prefix_message("[safe] "),
|
||||
redact_field("token"),
|
||||
append_fields([field("service", "bitlogger")]),
|
||||
]),
|
||||
)
|
||||
logger.info("login", fields=[field("token", "secret"), field("user", "alice")])
|
||||
inspect(captured_target.val, content="audit.auth")
|
||||
inspect(captured_message.val, content="[safe] login")
|
||||
inspect(captured_fields.val.length(), content="3")
|
||||
inspect(captured_fields.val[0].key, content="token")
|
||||
inspect(captured_fields.val[0].value, content="***")
|
||||
inspect(captured_fields.val[1].key, content="user")
|
||||
inspect(captured_fields.val[1].value, content="alice")
|
||||
inspect(captured_fields.val[2].key, content="service")
|
||||
inspect(captured_fields.val[2].value, content="bitlogger")
|
||||
}
|
||||
|
||||
///|
|
||||
test "patch helpers can redact multiple fields" {
|
||||
let captured_fields : Ref[Array[Field]] = Ref([])
|
||||
let logger = Logger::new(
|
||||
callback_sink(fn(rec) { captured_fields.val = rec.fields }),
|
||||
min_level=Level::Info,
|
||||
target="audit",
|
||||
).with_patch(redact_fields(["token", "password"], placeholder="[redacted]"))
|
||||
logger.info("credentials", fields=[
|
||||
field("token", "abc"),
|
||||
field("password", "123"),
|
||||
field("user", "alice"),
|
||||
])
|
||||
inspect(captured_fields.val.length(), content="3")
|
||||
inspect(captured_fields.val[0].value, content="[redacted]")
|
||||
inspect(captured_fields.val[1].value, content="[redacted]")
|
||||
inspect(captured_fields.val[2].value, content="alice")
|
||||
}
|
||||
|
||||
///|
|
||||
test "queued sink drains in order" {
|
||||
let flushed_messages : Ref[Array[String]] = Ref([])
|
||||
let sink = queued_sink(
|
||||
callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }),
|
||||
)
|
||||
let logger = Logger::new(sink, min_level=Level::Info, target="queue")
|
||||
logger.info("one")
|
||||
logger.info("two")
|
||||
logger.info("three")
|
||||
inspect(sink.pending_count(), content="3")
|
||||
inspect(sink.dropped_count(), content="0")
|
||||
inspect(sink.drain(max_items=2), content="2")
|
||||
inspect(sink.pending_count(), content="1")
|
||||
inspect(flushed_messages.val.length(), content="2")
|
||||
inspect(flushed_messages.val[0], content="one")
|
||||
inspect(flushed_messages.val[1], content="two")
|
||||
inspect(sink.flush(), content="1")
|
||||
inspect(sink.pending_count(), content="0")
|
||||
inspect(flushed_messages.val[2], content="three")
|
||||
}
|
||||
|
||||
///|
|
||||
test "queued sink can drop newest when full" {
|
||||
let flushed_messages : Ref[Array[String]] = Ref([])
|
||||
let sink = queued_sink(
|
||||
callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }),
|
||||
max_pending=2,
|
||||
overflow=QueueOverflowPolicy::DropNewest,
|
||||
)
|
||||
let logger = Logger::new(sink, min_level=Level::Info, target="queue")
|
||||
logger.info("one")
|
||||
logger.info("two")
|
||||
logger.info("three")
|
||||
inspect(sink.pending_count(), content="2")
|
||||
inspect(sink.dropped_count(), content="1")
|
||||
inspect(sink.flush(), content="2")
|
||||
inspect(flushed_messages.val.length(), content="2")
|
||||
inspect(flushed_messages.val[0], content="one")
|
||||
inspect(flushed_messages.val[1], content="two")
|
||||
}
|
||||
|
||||
///|
|
||||
test "queued sink can drop oldest when full" {
|
||||
let flushed_messages : Ref[Array[String]] = Ref([])
|
||||
let sink = queued_sink(
|
||||
callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }),
|
||||
max_pending=2,
|
||||
overflow=QueueOverflowPolicy::DropOldest,
|
||||
)
|
||||
let logger = Logger::new(sink, min_level=Level::Info, target="queue")
|
||||
logger.info("one")
|
||||
logger.info("two")
|
||||
logger.info("three")
|
||||
inspect(sink.pending_count(), content="2")
|
||||
inspect(sink.dropped_count(), content="1")
|
||||
inspect(sink.flush(), content="2")
|
||||
inspect(flushed_messages.val.length(), content="2")
|
||||
inspect(flushed_messages.val[0], content="two")
|
||||
inspect(flushed_messages.val[1], content="three")
|
||||
}
|
||||
|
||||
///|
|
||||
test "logger with_queue preserves chaining ergonomics" {
|
||||
let flushed_messages : Ref[Array[String]] = Ref([])
|
||||
let logger = Logger::new(
|
||||
callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }),
|
||||
min_level=Level::Info,
|
||||
target="service",
|
||||
)
|
||||
.with_patch(prefix_message("[queued] "))
|
||||
.with_queue(max_pending=2, overflow=QueueOverflowPolicy::DropOldest)
|
||||
logger.info("one")
|
||||
logger.child("api").info("two")
|
||||
logger.info("three")
|
||||
inspect(logger.sink.pending_count(), content="2")
|
||||
inspect(logger.sink.dropped_count(), content="1")
|
||||
inspect(logger.sink.flush(), content="2")
|
||||
inspect(flushed_messages.val.length(), content="2")
|
||||
inspect(flushed_messages.val[0], content="[queued] two")
|
||||
inspect(flushed_messages.val[1], content="[queued] three")
|
||||
}
|
||||
Reference in New Issue
Block a user