
FileTest.pudu
Pudu123 lines6.3 KB
1/** @Test.Sinks.File.Suite — appending, rolling, limits, retention, and buffering */2module PuduLangLog.Sinks.FileTest34import Std.Concurrent as Concurrent5import Std.Env as Env6import Std.Fs as Fs7import Std.Io as Io8import Std.List as List9import Std.Result as Result10import Std.Sync as Sync11import Std.Test as Test12import Std.Time as Time13import PuduLangLog.Event as Event14import PuduLangLog as Log15import PuduLangLog.Sinks.File as File161718const DAY: Int = 86400000192021const MORNING: Int = 1790589600000222324fn event(day: Int, message: Str) -> Log.Event {25 Event.create(Log.Timestamp{millis: MORNING + day * DAY, offset: 0}, Log.Information, message, [])26}272829fn lines(path: Str) -> File.Options { File.Options{..File.defaults(path), formatter: fn(written: &Log.Event) -> Str { Event.message(written) + "\n" }} }303132fn run(options: File.Options, events: Array[Log.Event]) -> Array[Result[(), Str]] {33 let sink = File.sink(options)34 let results = events.map(|written: Log.Event| (sink.emit)(&written))35 (sink.close)()36 results37}383940fn namesIn(directory: Str) -> Array[Str] {41 List.sortBy(&Result.unwrapOr(Io.list(directory), []), fn(left: Str, right: Str) -> Bool { left < right })42}434445fn read(path: Str) -> Str { Result.unwrapOr(Io.read(path), "") }464748fn main() -> Int {49 let root = Io.join(Env.temporaryDirectory(), "pudu-lang-log-file-" + show(Time.toMillis(&Time.currentInstant())))50 let _cleared = Fs.removeTree(root)5152 let plain = Io.join(root, "plain/nested/app.log")53 let plainResults = run(lines(plain), [event(0, "one"), event(0, "two")])5455 let daily = Io.join(root, "daily")56 let _daily = run(File.Options{..lines(Io.join(daily, "app-.log")), rollingInterval: Log.Day}, [event(0, "first"), event(1, "second"), event(1, "third")])5758 let limited = Io.join(root, "limited/app.log")59 let _limited = run(File.Options{..lines(limited), fileSizeLimitBytes: Some(10)}, [event(0, "aaaaaaa"), event(0, "bbbbbbb"), event(0, "ccccccc")])6061 let sized = Io.join(root, "sized")62 let _sized = run(File.Options{..lines(Io.join(sized, "app.log")), fileSizeLimitBytes: Some(10), rollOnFileSizeLimit: true}, [event(0, "aaaaaaaaaa"), event(0, "bbbbbbbbbb"), event(0, "cccccccccc")])6364 let kept = Io.join(root, "kept")65 let deleted = Sync.cell(0)66 let _kept = run(File.Options{..lines(Io.join(kept, "app-.log")), rollingInterval: Log.Day, retainedFileCountLimit: Some(2), onDeleting: Some(fn(_path: Str) -> () {67 if let Ok(count) = Sync.get(&deleted) { let _stored = Sync.set(&deleted, count + 1) }68 }) }, [event(0, "a"), event(1, "b"), event(2, "c"), event(3, "d")])6970 let aged = Io.join(root, "aged")71 let _aged = run(File.Options{..lines(Io.join(aged, "app-.log")), rollingInterval: Log.Day, retainedFileCountLimit: None, retainedFileTimeLimit: Some(2 * DAY)}, [event(0, "a"), event(1, "b"), event(2, "c"), event(3, "d")])7273 let headed = Io.join(root, "headed/app-.log")74 let _headed = run(File.Options{..lines(headed), rollingInterval: Log.Day, onOpened: Some(fn(_path: Str) -> Option[Str] { Some("# header\n") })}, [event(0, "a"), event(0, "b")])75 let _reopened = run(File.Options{..lines(headed), rollingInterval: Log.Day, onOpened: Some(fn(_path: Str) -> Option[Str] { Some("# header\n") })}, [event(0, "c")])7677 let resumed = Io.join(root, "resumed")78 let _made = Io.makeDirectory(resumed)79 let _seeded = Io.write(Io.join(resumed, "app-20260928_003.log"), "old\n")80 let _resumed = run(File.Options{..lines(Io.join(resumed, "app-.log")), rollingInterval: Log.Day}, [event(0, "new")])8182 let buffered = Io.join(root, "buffered/app.log")83 let bufferedSink = File.sink(File.Options{..lines(buffered), buffered: true})84 let _first = (bufferedSink.emit)(&event(0, "held"))85 let beforeFlush = Io.exists(buffered)86 (bufferedSink.flush)()87 let afterFlush = read(buffered)88 (bufferedSink.close)()89 let afterClose = (bufferedSink.emit)(&event(0, "late"))9091 let timed = Io.join(root, "timed/app.log")92 let timedSink = File.sink(File.Options{..lines(timed), buffered: true, flushInterval: Some(100)})93 let _timed = (timedSink.emit)(&event(0, "soon"))94 let _waited = Concurrent.sleep(400)95 let timedText = read(timed)96 (timedSink.close)()9798 let checks = Test.suite("Sinks.File", &[99 Test.equals("events are appended in order", &read(plain), &"one\ntwo\n"),100 Test.equals("every write succeeds", &plainResults, &[Ok(()), Ok(())]),101 Test.equals("a file per day", &namesIn(daily), &["app-20260928.log", "app-20260929.log"]),102 Test.equals("each day holds its events", &(read(Io.join(daily, "app-20260928.log")), read(Io.join(daily, "app-20260929.log"))), &("first\n", "second\nthird\n")),103 Test.equals("a full file takes no more events", &read(limited), &"aaaaaaa\nbbbbbbb\n"),104 Test.equals("a full file gives way to the next sequence", &namesIn(sized), &["app.log", "app_001.log", "app_002.log"]),105 Test.equals("each sequence holds one event", &read(Io.join(sized, "app_002.log")), &"cccccccccc\n"),106 Test.equals("retention keeps the count", &namesIn(kept), &["app-20260930.log", "app-20261001.log"]),107 Test.equals("the deleting hook sees each removal", &Sync.get(&deleted), &Ok(2)),108 Test.equals("retention by age removes periods that started before the limit", &namesIn(aged), &["app-20260930.log", "app-20261001.log"]),109 Test.equals("a header starts only a new file", &read(Io.join(root, "headed/app-20260928.log")), &"# header\na\nb\nc\n"),110 Test.equals("the latest sequence on disk is resumed", &(namesIn(resumed), read(Io.join(resumed, "app-20260928_003.log"))), &(["app-20260928_003.log"], "old\nnew\n")),111 Test.that("buffered text waits for a flush", !beforeFlush),112 Test.equals("a flush writes buffered text", &afterFlush, &"held\n"),113 Test.equals("a closed sink refuses events", &afterClose, &Err("the file sink is closed")),114 Test.equals("a flushing thread writes on its own", &timedText, &"soon\n")115 ])116 let _removed = Fs.removeTree(root)117 let ran = Test.run(&checks)118 for failure in Test.failuresOf(&ran) {119 let _reported = Io.writeErrorLine(failure)120 }121 Test.report(&ran)122}123