
RollingTest.pudu
Pudu79 lines10.1 KB
1/** @Test.Domain.Rolling.Suite — rolling periods, names, retention, and retry timing */2module PuduLangLog.Domain.RollingTest34import Std.Io as Io5import Std.Test as Test6import PuduLangLog.Domain.Rolling as Rolling7import PuduLangLog.Domain.Schedule as Schedule8import PuduLangLog.Domain.Sources as Sources9import PuduLangLog as Log101112const EAST: Log.Timestamp = Log.Timestamp{millis: 1790604309123, offset: 120}131415fn rolled(name: Str, period: Option[Int], sequence: Option[Int]) -> Rolling.Rolled { Rolling.Rolled{name: name, period: period, sequence: sequence} }161718fn failedAt(start: Schedule.State, moments: Array[Int]) -> Schedule.Verdict {19 var verdict = Schedule.Verdict{state: start, dropBatch: false, dropQueue: false}20 for moment in moments { verdict = Schedule.failed(&verdict.state, moment) }21 verdict22}232425fn main() -> Int {26 let intervals = [Log.Infinite, Log.Year, Log.Month, Log.Day, Log.Hour, Log.Minute]27 let files = [rolled("log-20260926.txt", Some(1790380800000), None), rolled("log-20260928.txt", Some(1790553600000), None), rolled("log-20260927.txt", Some(1790467200000), None), rolled("log-20260928_001.txt", Some(1790553600000), Some(1)), rolled("log-20260928_002.txt", Some(1790553600000), Some(2))]28 let schedule = Schedule.create(2000, 600000)29 let checks = Test.suite("Domain.Rolling", &[30 Test.equals("period formats", &intervals.map(|interval: Log.RollingInterval| Rolling.periodFormat(interval)), &["", "yyyy", "yyyyMM", "yyyyMMdd", "yyyyMMddHH", "yyyyMMddHHmm"]),31 Test.equals("checkpoints use the local clock", &intervals.map(|interval: Log.RollingInterval| Rolling.checkpoint(interval, &EAST)), &[None, Some(1767225600000), Some(1788220800000), Some(1790553600000), Some(1790611200000), Some(1790611500000)]),32 Test.equals("next checkpoints", &intervals.map(|interval: Log.RollingInterval| Rolling.next(interval, &EAST)), &[None, Some(1798761600000), Some(1790812800000), Some(1790640000000), Some(1790614800000), Some(1790611560000)]),33 Test.equals("the next month after December is January", &Rolling.next(Log.Month, &Log.Timestamp{millis: 1798761540000, offset: 0}), &Some(1798761600000)),34 Test.equals("file names carry the period before the extension", &intervals.map(|interval: Log.RollingInterval| Rolling.fileName("logs/app.txt", interval, &EAST, None)), &["logs/app.txt", "logs/app2026.txt", "logs/app202609.txt", "logs/app20260928.txt", "logs/app2026092816.txt", "logs/app202609281605.txt"]),35 Test.equals("sequence numbers", &(Rolling.fileName("log-.txt", Log.Day, &EAST, Some(1)), Rolling.fileName("log-.txt", Log.Day, &EAST, Some(1234))), &("log-20260928_001.txt", "log-20260928_1234.txt")),36 Test.equals("a path without an extension", &Rolling.fileName("logs/app", Log.Day, &EAST, Some(2)), &"logs/app20260928_002"),37 Test.equals("a dotted directory keeps its dot", &Rolling.fileName("my.logs/app", Log.Hour, &EAST, None), &"my.logs/app2026092816"),38 Test.equals("names read back", &(Rolling.matches("logs/log-.txt", Log.Day, "log-20260928.txt"), Rolling.matches("logs/log-.txt", Log.Day, "log-20260928_007.txt")), &(Some(rolled("log-20260928.txt", Some(1790553600000), None)), Some(rolled("log-20260928_007.txt", Some(1790553600000), Some(7))))),39 Test.equals("an unrolled name reads back without a period", &Rolling.matches("app.log", Log.Infinite, "app_003.log"), &Some(rolled("app_003.log", None, Some(3)))),40 Test.equals("names that are not rolled files", &[Rolling.matches("log-.txt", Log.Day, "log-2026092.txt"), Rolling.matches("log-.txt", Log.Day, "log-20260928.json"), Rolling.matches("log-.txt", Log.Day, "other-20260928.txt"), Rolling.matches("log-.txt", Log.Day, "log-20260928_01.txt"), Rolling.matches("log-.txt", Log.Day, "log-20260928-001.txt"), Rolling.matches("log-.txt", Log.Day, "log-2026092a.txt"), Rolling.matches("log-.txt", Log.Day, "log-20260928_0x1.txt")], &[None, None, None, None, None, None, None]),41 Test.equals("every interval reads its period back", &[Rolling.matches("app.log", Log.Month, "app202609.log"), Rolling.matches("app.log", Log.Hour, "app2026092816.log"), Rolling.matches("app.log", Log.Minute, "app202609281605.log")], &[Some(rolled("app202609.log", Some(1788220800000), None)), Some(rolled("app2026092816.log", Some(1790611200000), None)), Some(rolled("app202609281605.log", Some(1790611500000), None))]),42 Test.equals("the first and last days of a year", &(Rolling.matches("a.log", Log.Day, "a20260101.log"), Rolling.matches("a.log", Log.Day, "a20261231.log")), &(Some(rolled("a20260101.log", Some(1767225600000), None)), Some(rolled("a20261231.log", Some(1798675200000), None)))),43 Test.equals("the last day of February", &(Rolling.matches("a.log", Log.Day, "a20260228.log"), Rolling.matches("a.log", Log.Day, "a20240229.log"), Rolling.matches("a.log", Log.Day, "a20260229.log")), &(Some(rolled("a20260228.log", Some(1772236800000), None)), Some(rolled("a20240229.log", Some(1709164800000), None)), Some(rolled("a20260229.log", None, None)))),44 Test.equals("the last minute of a day", &Rolling.matches("a.log", Log.Minute, "a202609282359.log"), &Some(rolled("a202609282359.log", Some(1790639940000), None))),45 Test.equals("each field out of range alone", &[Rolling.matches("a.log", Log.Month, "a202613.log"), Rolling.matches("a.log", Log.Month, "a202600.log"), Rolling.matches("a.log", Log.Day, "a20260900.log"), Rolling.matches("a.log", Log.Hour, "a2026092824.log"), Rolling.matches("a.log", Log.Minute, "a202609281660.log")], &[Some(rolled("a202613.log", None, None)), Some(rolled("a202600.log", None, None)), Some(rolled("a20260900.log", None, None)), Some(rolled("a2026092824.log", None, None)), Some(rolled("a202609281660.log", None, None))]),46 Test.equals("the unrolled file itself matches", &Rolling.matches("logs/app.log", Log.Infinite, "app.log"), &Some(rolled("app.log", None, None))),47 Test.equals("a different stem with a matching tail does not match", &Rolling.matches("log-.txt", Log.Day, "xog-20260928.txt"), &None),48 Test.equals("a different extension of the same length does not match", &Rolling.matches("log-.txt", Log.Day, "log-20260928_001xtxt"), &None),49 Test.equals("a name shorter than the stem and extension does not match", &Rolling.matches("ab.txt", Log.Infinite, "ab"), &None),50 Test.equals("impossible dates have no period", &(Rolling.matches("log-.txt", Log.Day, "log-20260231.txt"), Rolling.matches("log-.txt", Log.Minute, "log-202609282460.txt")), &(Some(rolled("log-20260231.txt", None, None)), Some(rolled("log-202609282460.txt", None, None)))),51 Test.equals("the latest sequence of a period", &(Rolling.latestSequence(&files, Some(1790553600000)), Rolling.latestSequence(&files, Some(1790467200000))), &(Some(2), None)),52 Test.equals("retention keeps the count, the current file included", &Rolling.retired(&files, "log-20260928_002.txt", Some(3), None), &["log-20260927.txt", "log-20260926.txt"]),53 Test.equals("a count of one keeps only the current file", &Rolling.retired(&files, "LOG-20260928_002.TXT", Some(1), None).length(), &4),54 Test.equals("no limits keep everything", &Rolling.retired(&files, "x", None, None), &[]),55 Test.equals("retention by age removes older periods", &Rolling.retired(&files, "log-20260928_002.txt", None, Some(1790467200000)), &["log-20260926.txt"]),56 Test.equals("a period exactly at the age limit is kept", &Rolling.retired(&[rolled("a", Some(10), None)], "x", None, Some(10)), &[]),57 Test.equals("a sequence is newer than no sequence", &Rolling.retired(&[rolled("b_000", Some(5), Some(0)), rolled("b", Some(5), None)], "x", Some(2), None), &["b"]),58 Test.equals("a higher sequence is newer", &Rolling.retired(&[rolled("b_002", Some(5), Some(2)), rolled("b_010", Some(5), Some(10))], "x", Some(2), None), &["b_002"]),59 Test.equals("equal periods and sequences keep the later name", &(Rolling.retired(&[rolled("a", Some(5), None), rolled("b", Some(5), None)], "x", Some(2), None), Rolling.retired(&[rolled("b", Some(5), None), rolled("a", Some(5), None)], "x", Some(2), None)), &(["a"], ["a"])),60 Test.equals("files without periods are oldest", &Rolling.retired(&[rolled("old", None, None), rolled("new", Some(5), None)], "x", Some(2), None), &["old"]),61 Test.equals("buffering time until repeated failures", &(Schedule.interval(&schedule), Schedule.interval(&failedAt(schedule, [0]).state)), &(2000, 2000)),62 Test.equals("backoff doubles from five seconds to a minute", &[2, 3, 4, 5, 100].map(|failures: Int| Schedule.interval(&Schedule.State{..schedule, failures: failures})), &[10000, 20000, 40000, 60000, 60000]),63 Test.equals("a long buffering time is never shortened", &Schedule.interval(&Schedule.State{..Schedule.create(90000, 600000), failures: 3}), &90000),64 Test.equals("a failure within the retry limit keeps the batch", &failedAt(schedule, [1000]), &Schedule.Verdict{state: Schedule.State{..schedule, failures: 1, firstFailure: Some(1000)}, dropBatch: false, dropQueue: false}),65 Test.equals("a retry past the limit drops the batch", &failedAt(Schedule.create(2000, 3000), [0, 2500]).dropBatch, &true),66 Test.equals("a retry exactly at the limit drops the batch", &failedAt(Schedule.create(2000, 2000), [0]).dropBatch, &true),67 Test.equals("ten dropped batches drop the queue", &(failedAt(Schedule.create(2000, 0), [0, 0, 0, 0, 0, 0, 0, 0, 0]).dropQueue, failedAt(Schedule.create(2000, 0), [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]).dropQueue), &(false, true)),68 Test.equals("success resets the schedule", &Schedule.succeeded(&failedAt(schedule, [0, 1, 2]).state), &schedule),69 Test.that("a prefix covers itself and what lies beneath it", Sources.covers("App.Web", "App.Web") && Sources.covers("App.Web", "App.Web.Api") && Sources.covers("app.web", "App.Web.Api")),70 Test.that("a prefix does not cover a longer word", !Sources.covers("App.Web", "App.Website") && !Sources.covers("App.Web", "App") && !Sources.covers("App.Web", "Other")),71 Test.that("beneath keeps case", Sources.beneath("App", "App.X") && !Sources.beneath("app", "App.X"))72 ])73 let ran = Test.run(&checks)74 for failure in Test.failuresOf(&ran) {75 let _reported = Io.writeErrorLine(failure)76 }77 Test.report(&ran)78}79