1# testing - a small test runner.2#3# A test file declares its cases and hands them to Main, which runs them all4# and exits non zero if any of them failed:5#6# testing = import("testing")7# cmp = import("cmp")8#9# testing.Main([10# ["Equal on lists", fn(t) {11# t.AssertEq(cmp.Equal([1, 2], [1, 2]), true)12# }],13# ["Compare", fn(t) {14# t.AssertEq(cmp.Compare(2, 3), -1)15# }],16# ])17#18# There are no exceptions, so a failed assertion records the failure and the19# case keeps going: to stop it, return right after t.Fatal.2021cmp = import("cmp")22time = import("time")2324# newT builds the object a test case is given.25newT = fn(name) {26 t = new()27 t.Name = name28 t.failed = false29 t.skipped = false30 t.messages = []3132 # Error records a failure and lets the case carry on.33 t.Error = fn(msg) {34 t.failed = true35 t.messages = append(t.messages, string(msg))36 }3738 # Fatal records a failure meant to end the case. Nothing can interrupt a39 # function from the outside, so return right after calling it.40 t.Fatal = fn(msg) {41 t.Error(msg)42 return null43 }4445 # Assert fails the case if cond isn't true.46 t.Assert = fn(cond, msg) {47 if cond != true {48 t.Error(if msg == null { "assertion failed" } else { msg })49 return false50 }51 return true52 }5354 # AssertEq fails the case unless got and want hold the same value,55 # compared structurally.56 t.AssertEq = fn(got, want) {57 if !cmp.Equal(got, want) {58 t.Error("got {got}, want {want}")59 return false60 }61 return true62 }6364 # AssertNe is AssertEq the other way round.65 t.AssertNe = fn(got, want) {66 if cmp.Equal(got, want) {67 t.Error("got {got}, want anything else")68 return false69 }70 return true71 }7273 # AssertError fails the case unless x is an error.74 t.AssertError = fn(x) {75 if type(x) != "error" {76 t.Error("got {type(x)} {x}, want an error")77 return false78 }79 return true80 }8182 # Skip marks the case as not run.83 t.Skip = fn(msg) {84 t.skipped = true85 t.messages = append(t.messages, if msg == null { "skipped" } else { string(msg) })86 return null87 }8889 t.Failed = fn() { t.failed }9091 return t92}9394# Run runs one case and returns its t, already reported.95Run = fn(name, f) {96 t = newT(name)97 start = time.Mono()98 f(t)99 elapsed = time.Since(start)100101 if t.skipped {102 println("--- SKIP: {name}")103 } else if t.failed {104 println("--- FAIL: {name} ({elapsed}ms)")105 } else {106 println("--- PASS: {name} ({elapsed}ms)")107 }108109 for i = 0; i < len(t.messages); ++i {110 println(" {t.messages[i]}")111 }112113 return t114}115116# Main runs every [name, function] pair and exits: 0 when they all pass, 1 as117# soon as one of them doesn't.118Main = fn(cases) {119 failed = 0120 skipped = 0121 start = time.Mono()122123 for i = 0; i < len(cases); ++i {124 t = Run(cases[i][0], cases[i][1])125 if t.failed {126 ++failed127 }128 if t.skipped {129 ++skipped130 }131 }132133 elapsed = time.Since(start)134 total = len(cases)135 passed = total - failed - skipped136137 if failed > 0 {138 println("FAIL {failed} failed, {passed} passed of {total} ({elapsed}ms)")139 exit(1)140 }141142 println("ok {passed} passed of {total} ({elapsed}ms)")143 exit(0)144}