|
| 1 | +structure Expect = |
| 2 | +struct |
| 3 | + datatype expectation = Pass | Fail of string * string |
| 4 | + |
| 5 | + local |
| 6 | + fun failEq b a = |
| 7 | + Fail ("Expected: " ^ b, "Got: " ^ a) |
| 8 | + |
| 9 | + fun failExn b a = |
| 10 | + Fail ("Expected: " ^ b, "Raised: " ^ a) |
| 11 | + |
| 12 | + fun exnName (e: exn): string = General.exnName e |
| 13 | + in |
| 14 | + fun truthy a = |
| 15 | + if a |
| 16 | + then Pass |
| 17 | + else failEq "true" "false" |
| 18 | + |
| 19 | + fun falsy a = |
| 20 | + if a |
| 21 | + then failEq "false" "true" |
| 22 | + else Pass |
| 23 | + |
| 24 | + fun equalTo b a = |
| 25 | + if a = b |
| 26 | + then Pass |
| 27 | + else failEq (PolyML.makestring b) (PolyML.makestring a) |
| 28 | + |
| 29 | + fun nearTo delta b a = |
| 30 | + if Real.abs (a - b) <= delta * Real.abs a orelse |
| 31 | + Real.abs (a - b) <= delta * Real.abs b |
| 32 | + then Pass |
| 33 | + else failEq (Real.toString b ^ " +/- " ^ Real.toString delta) (Real.toString a) |
| 34 | + |
| 35 | + fun anyError f = |
| 36 | + ( |
| 37 | + f (); |
| 38 | + failExn "an exception" "Nothing" |
| 39 | + ) handle _ => Pass |
| 40 | + |
| 41 | + fun error e f = |
| 42 | + ( |
| 43 | + f (); |
| 44 | + failExn (exnName e) "Nothing" |
| 45 | + ) handle e' => if exnMessage e' = exnMessage e |
| 46 | + then Pass |
| 47 | + else failExn (exnMessage e) (exnMessage e') |
| 48 | + end |
| 49 | +end |
| 50 | + |
| 51 | +structure TermColor = |
| 52 | +struct |
| 53 | + datatype color = Red | Green | Yellow | Normal |
| 54 | + |
| 55 | + fun f Red = "\027[31m" |
| 56 | + | f Green = "\027[32m" |
| 57 | + | f Yellow = "\027[33m" |
| 58 | + | f Normal = "\027[0m" |
| 59 | + |
| 60 | + fun colorize color s = (f color) ^ s ^ (f Normal) |
| 61 | + |
| 62 | + val redit = colorize Red |
| 63 | + |
| 64 | + val greenit = colorize Green |
| 65 | + |
| 66 | + val yellowit = colorize Yellow |
| 67 | +end |
| 68 | + |
| 69 | +structure Test = |
| 70 | +struct |
| 71 | + datatype testnode = TestGroup of string * testnode list |
| 72 | + | Test of string * (unit -> Expect.expectation) |
| 73 | + |
| 74 | + local |
| 75 | + datatype evaluation = Success of string |
| 76 | + | Failure of string * string * string |
| 77 | + | Error of string * string |
| 78 | + |
| 79 | + fun indent n s = (implode (List.tabulate (n, fn _ => #" "))) ^ s |
| 80 | + |
| 81 | + fun fmt indentlvl ev = |
| 82 | + let |
| 83 | + val check = TermColor.greenit "\226\156\148 " (* ✔ *) |
| 84 | + val cross = TermColor.redit "\226\156\150 " (* ✖ *) |
| 85 | + val indentlvl = indentlvl * 2 |
| 86 | + in |
| 87 | + case ev of |
| 88 | + Success descr => indent indentlvl (check ^ descr) |
| 89 | + | Failure (descr, exp, got) => |
| 90 | + String.concatWith "\n" [indent indentlvl (cross ^ descr), |
| 91 | + indent (indentlvl + 2) exp, |
| 92 | + indent (indentlvl + 2) got] |
| 93 | + | Error (descr, reason) => |
| 94 | + String.concatWith "\n" [indent indentlvl (cross ^ descr), |
| 95 | + indent (indentlvl + 2) (TermColor.redit reason)] |
| 96 | + end |
| 97 | + |
| 98 | + fun eval (TestGroup _) = raise Fail "Only a 'Test' can be evaluated" |
| 99 | + | eval (Test (descr, thunk)) = |
| 100 | + ( |
| 101 | + case thunk () of |
| 102 | + Expect.Pass => ((1, 0, 0), Success descr) |
| 103 | + | Expect.Fail (s, s') => ((0, 1, 0), Failure (descr, s, s')) |
| 104 | + ) |
| 105 | + handle e => ((0, 0, 1), Error (descr, "Unexpected error: " ^ exnMessage e)) |
| 106 | + |
| 107 | + fun flatten depth testnode = |
| 108 | + let |
| 109 | + fun sum (x, y, z) (a, b, c) = (x + a, y + b, z + c) |
| 110 | + |
| 111 | + fun aux (t, (counter, acc)) = |
| 112 | + let |
| 113 | + val (counter', texts) = flatten (depth + 1) t |
| 114 | + in |
| 115 | + (sum counter' counter, texts :: acc) |
| 116 | + end |
| 117 | + in |
| 118 | + case testnode of |
| 119 | + TestGroup (descr, ts) => |
| 120 | + let |
| 121 | + val (counter, texts) = foldr aux ((0, 0, 0), []) ts |
| 122 | + in |
| 123 | + (counter, (indent (depth * 2) descr) :: List.concat texts) |
| 124 | + end |
| 125 | + | Test _ => |
| 126 | + let |
| 127 | + val (counter, evaluation) = eval testnode |
| 128 | + in |
| 129 | + (counter, [fmt depth evaluation]) |
| 130 | + end |
| 131 | + end |
| 132 | + |
| 133 | + fun println s = print (s ^ "\n") |
| 134 | + in |
| 135 | + fun run suite = |
| 136 | + let |
| 137 | + val ((succeeded, failed, errored), texts) = flatten 0 suite |
| 138 | + |
| 139 | + val summary = String.concatWith ", " [ |
| 140 | + TermColor.greenit ((Int.toString succeeded) ^ " passed"), |
| 141 | + TermColor.redit ((Int.toString failed) ^ " failed"), |
| 142 | + TermColor.redit ((Int.toString errored) ^ " errored"), |
| 143 | + (Int.toString (succeeded + failed + errored)) ^ " total" |
| 144 | + ] |
| 145 | + |
| 146 | + val status = if failed = 0 andalso errored = 0 |
| 147 | + then OS.Process.success |
| 148 | + else OS.Process.failure |
| 149 | + |
| 150 | + in |
| 151 | + List.app println texts; |
| 152 | + println ""; |
| 153 | + println ("Tests: " ^ summary); |
| 154 | + OS.Process.exit status |
| 155 | + end |
| 156 | + end |
| 157 | +end |
| 158 | + |
| 159 | +fun describe description tests = Test.TestGroup (description, tests) |
| 160 | +fun test description thunk = Test.Test (description, thunk) |
0 commit comments