Pudu programming language
Menu
Package

@chrismichaelps / pudu-lang-log

Structured event logging for Pudu: message templates, enrichment, filtering, formatting, and sinks

0.1.0Apache-2.01

InstallClose

ClefTest.pudu

Pudu84 lines5.1 KB

GitHub ↗
1/** @Test.Domain.Clef.Suite — compact JSON lines read back into events */2module PuduLangLog.Domain.ClefTest34import Std.Io as Io5import Std.Option as Option6import Std.Test as Test7import PuduLangLog.Domain.Clef as Clef8import PuduLangLog as Log9import PuduLangLog.Value as Value1011/// The event a line describes, or none.12fn read(text: Str) -> Option[Log.Event] {13  match Clef.line(text) {14    case Ok(event) => Some(event)15    case Err(_) => None16  }17}1819/// Why a line cannot be read, or `read` when it can.20fn refusal(text: Str) -> Str {21  match Clef.line(text) {22    case Ok(_) => "read"23    case Err(problem) => problem24  }25}2627/// The failure of the event a line describes.28fn failureIn(text: Str) -> Option[Log.Failure] { Option.andThen(read(text), |event: Log.Event| event.failure) }2930/// Runs the suite.31fn main() -> Int {32  let minimal = read("\{\"@t\":\"2026-09-28T14:05:09.1230000Z\",\"@mt\":\"Hello \{Name\}\",\"Name\":\"Ada\"\}")33  let offset = read("\{\"@t\":\"2026-09-28T16:05:09.123+02:00\",\"@mt\":\"x\"\}")34  let behind = read("\{\"@t\":\"2026-09-28T09:35:09.123-04:30\",\"@mt\":\"x\"\}")35  let rendered = read("\{\"@t\":\"2026-09-28T14:05:09Z\",\"@m\":\"Set \{a\} to 1\",\"@i\":\"abc\"\}")36  let bare = read("\{\"@t\":\"2026-09-28T14:05:09Z\"\}")37  let values = read("\{\"@t\":\"2026-09-28T14:05:09Z\",\"@mt\":\"v\",\"@r\":[\"1\"],\"n\":null,\"b\":true,\"i\":-4,\"d\":1.50,\"s\":\"x\",\"l\":[1,\"a\"],\"o\":\{\"A\":1,\"$type\":\"User\"\},\"u\":\{\"B\":2\},\"@@odd\":1\}")38  let checks = Test.suite("Domain.Clef", &[39      Test.equals("the timestamp is read in UTC", &Option.map(minimal, |event: Log.Event| event.timestamp), &Some(Log.Timestamp{millis: 1790604309123, offset: 0})),40      Test.equals("a positive offset is kept in minutes", &Option.map(offset, |event: Log.Event| event.timestamp), &Some(Log.Timestamp{millis: 1790604309123, offset: 120})),41      Test.equals("a negative offset is kept in minutes", &Option.map(behind, |event: Log.Event| event.timestamp), &Some(Log.Timestamp{millis: 1790604309123, offset: -270})),42      Test.equals("the template and its properties", &Option.map(minimal, |event: Log.Event| (event.template.text, event.properties, event.level)), &Some(("Hello \{Name\}", [Value.property("Name", Value.text("Ada"))], Log.Information))),43      Test.equals("a rendered message becomes an escaped template", &Option.map(rendered, |event: Log.Event| (event.template.text, event.template.holes.length(), event.properties.length())), &Some(("Set \{\{a\}\} to 1", 0, 0))),44      Test.equals("no template reads as empty", &Option.map(bare, |event: Log.Event| event.template.text), &Some("")),45      Test.equals("values of every kind", &Option.map(values, |event: Log.Event| event.properties), &Some([46            Value.property("n", Value.nothing()),47            Value.property("b", Value.bool(true)),48            Value.property("i", Value.int(-4)),49            Value.property("d", Value.decimal(1.50d)),50            Value.property("s", Value.text("x")),51            Value.property("l", Value.list([Value.int(1), Value.text("a")])),52            Value.property("o", Value.structure("User", [("A", Value.int(1))])),53            Value.property("u", Value.object([("B", Value.int(2))])),54            Value.property("@odd", Value.int(1))55          ])),56      Test.equals("levels, trace, and span", &Option.map(read("\{\"@t\":\"2026-09-28T14:05:09Z\",\"@l\":\"warning\",\"@tr\":\"t1\",\"@sp\":\"s1\"\}"), |event: Log.Event| (event.level, event.traceId, event.spanId)), &Some((Log.Warning, Some("t1"), Some("s1")))),57      Test.equals("a failure with a kind and message", &failureIn("\{\"@t\":\"2026-09-28T14:05:09Z\",\"@x\":\"IoError: disk: full\"\}"), &Some(Log.Failure{kind: "IoError", message: "disk: full", trace: [], causes: []})),58      Test.equals("a failure with only a kind", &failureIn("\{\"@t\":\"2026-09-28T14:05:09Z\",\"@x\":\"Timeout\"\}"), &Some(Log.Failure{kind: "Timeout", message: "", trace: [], causes: []})),59      Test.equals("a failure with message lines, a trace, and a chain of causes", &failureIn("\{\"@t\":\"2026-09-28T14:05:09Z\",\"@x\":\"Outer: first\\nsecond\\n   at a\\n   at b\\n ---> Middle: m\\n   at c\\n ---> Inner: i\"\}"), &Some(Log.Failure{kind: "Outer", message: "first\nsecond", trace: ["a", "b"], causes: [Log.Failure{kind: "Middle", message: "m", trace: ["c"], causes: [Log.Failure{kind: "Inner", message: "i", trace: [], causes: []}]}]})),60      Test.equals("refusals", &[61          refusal("not json"),62          refusal("[1]"),63          refusal("\{\"@mt\":\"x\"\}"),64          refusal("\{\"@t\":\"yesterday\"\}"),65          refusal("\{\"@t\":\"2026-09-28T14:05:09Z\",\"@l\":\"Loud\"\}"),66          refusal("\{\"@t\":\"2026-09-28T14:05:09Z\",\"@mt\":3\}"),67          refusal("\{\"@t\":1\}")68        ], &[69          "The line is not JSON: expected a value at 0",70          "The event is not a JSON object",71          "The event has no `@t` timestamp",72          "The timestamp `yesterday` is not RFC 3339",73          "Unknown level `Loud`",74          "The `@mt` member is not text",75          "The `@t` member is not text"76        ])77    ])78  let ran = Test.run(&checks)79  for failure in Test.failuresOf(&ran) {80    let _reported = Io.writeErrorLine(failure)81  }82  Test.report(&ran)83}84