+test.ha (8761B)
1 // SPDX-License-Identifier: MPL-2.0 2 // (c) Hare authors <https://harelang.org> 3 4 use ascii; 5 use bufio; 6 use encoding::hex; 7 use encoding::utf8; 8 use fmt; 9 use fnmatch; 10 use io; 11 use memio; 12 use os; 13 use rt; 14 use strings; 15 use time; 16 use unix::signal; 17 use unix::tty; 18 19 type test = struct { 20 name: str, 21 func: *fn() void, 22 }; 23 24 // RETURN and ABORT must be 0 and 1 respectively 25 type status = enum { 26 RETURN = 0, 27 ABORT = 1, 28 SKIP, 29 SEGV, 30 }; 31 32 type failure = struct { 33 test: str, 34 reason: rt::abort_reason, 35 }; 36 37 type skipped = struct { 38 test: str, 39 reason: str, 40 }; 41 42 type output = struct { 43 test: str, 44 stdout: str, 45 stderr: str, 46 }; 47 48 fn finish_output(output: *output) void = { 49 free(output.stdout); 50 free(output.stderr); 51 }; 52 53 type context = struct { 54 stdout: memio::stream, 55 stderr: memio::stream, 56 failures: []failure, 57 skipped: []skipped, 58 output: []output, 59 maxname: size, 60 total_time: time::duration, 61 default_round: uint, 62 cwd: str, 63 }; 64 65 fn finish_context(ctx: *context) void = { 66 io::close(&ctx.stdout)!; 67 io::close(&ctx.stderr)!; 68 free(ctx.failures); 69 free(ctx.skipped); 70 for (let out &.. ctx.output) { 71 finish_output(out); 72 }; 73 free(ctx.output); 74 free(ctx.cwd); 75 }; 76 77 fn colored() bool = { 78 return len(os::tryenv("NO_COLOR", "")) == 0 79 && tty::isatty(os::stdout_file); 80 }; 81 82 let jmpbuf = rt::jmpbuf { ... }; 83 84 const @symbol("__test_array_start") test_start: [*]test; 85 const @symbol("__test_array_end") test_end: [*]test; 86 87 export @symbol("__test_main") fn main() size = { 88 const ntest = (&test_end: uintptr - &test_start: uintptr): size / size(test); 89 const tests = test_start[..ntest]; 90 let enabled_tests: []test = []; 91 defer free(enabled_tests); 92 if (len(os::args) == 1) { 93 append(enabled_tests, tests...); 94 } else for (let i = 0z; i < ntest; i += 1) { 95 for (let arg .. os::args) { 96 if (fnmatch::fnmatch(arg, tests[i].name)) { 97 append(enabled_tests, tests[i]); 98 break; 99 }; 100 }; 101 }; 102 if (len(enabled_tests) == 0) { 103 fmt::println("No tests run")!; 104 return 0; 105 }; 106 107 let maxname = 0z; 108 for (let test .. enabled_tests) { 109 if (len(test.name) > maxname) { 110 maxname = len(test.name); 111 }; 112 }; 113 114 let ctx = context { 115 stdout = memio::dynamic(), 116 stderr = memio::dynamic(), 117 maxname = maxname, 118 default_round = rt::fegetround(), 119 cwd = strings::dup(os::getcwd()), 120 ... 121 }; 122 defer finish_context(&ctx); 123 124 fmt::printfln("Running {}/{} tests:\n", len(enabled_tests), ntest)!; 125 reset(&ctx); 126 for (let test .. enabled_tests) { 127 do_test(&ctx, test); 128 }; 129 fmt::println()!; 130 131 for (let skipped .. ctx.skipped) { 132 fmt::printfln("Skipped {}: {}", skipped.test, skipped.reason)!; 133 }; 134 if (len(ctx.skipped) > 0) { 135 fmt::println()!; 136 }; 137 138 if (len(ctx.failures) > 0) { 139 fmt::println("Failures:")!; 140 for (let failure .. ctx.failures) { 141 match (failure.reason.path) { 142 case null => 143 fmt::printfln("{}: {}", 144 failure.test, 145 failure.reason.msg)!; 146 case let path: *str => 147 fmt::printfln("{}: {}:{}:{}: {}", 148 failure.test, 149 *path, 150 failure.reason.line, 151 failure.reason.col, 152 failure.reason.msg)!; 153 }; 154 }; 155 fmt::println()!; 156 }; 157 158 for (let i = 0z; i < len(ctx.output); i += 1) { 159 if (ctx.output[i].stdout != "") { 160 fmt::println(ctx.output[i].test, "stdout:")!; 161 fmt::println(ctx.output[i].stdout)!; 162 }; 163 if (ctx.output[i].stderr != "") { 164 fmt::println(ctx.output[i].test, "stderr:")!; 165 fmt::println(ctx.output[i].stderr)!; 166 }; 167 if (i == len(ctx.output) - 1) { 168 fmt::println()!; 169 }; 170 }; 171 172 // XXX: revisit once time::format_duration is implemented 173 const total_cnt = len(enabled_tests); 174 const failed_cnt = len(ctx.failures); 175 const skipped_cnt = len(ctx.skipped); 176 const passed_cnt = total_cnt - failed_cnt - skipped_cnt; 177 const elapsed_whole = ctx.total_time / time::SECOND; 178 const elapsed_fraction = ctx.total_time % time::SECOND; 179 styled_print(if (passed_cnt > 0) 92 else 37, passed_cnt); 180 fmt::print(" passed; ")!; 181 styled_print(if (len(ctx.failures) > 0) 91 else 37, failed_cnt); 182 fmt::print(" failed; ")!; 183 if (len(ctx.skipped) > 0) { 184 fmt::print(len(ctx.skipped), "skipped; ")!; 185 }; 186 fmt::printfln("{} completed in {}.{:.9}s", total_cnt, 187 elapsed_whole, elapsed_fraction)!; 188 189 easter_egg(ctx.failures, enabled_tests); 190 191 return len(ctx.failures); 192 }; 193 194 fn reset(ctx: *context) void = { 195 rt::fesetround(ctx.default_round); 196 rt::feclearexcept(~0u); 197 signal::resetall(); 198 os::chdir(ctx.cwd)!; 199 want_abort = false; 200 }; 201 202 fn do_test(ctx: *context, test: test) void = { 203 signal::handle(signal::sig::SEGV, &handle_segv, signal::flag::NODEFER); 204 memio::reset(&ctx.stdout); 205 memio::reset(&ctx.stderr); 206 207 const start_time = time::now(time::clock::MONOTONIC); 208 const status = run_test(ctx, test); 209 const end_time = time::now(time::clock::MONOTONIC); 210 211 const failed = interpret_status(ctx, test.name, status); 212 const time_diff = time::diff(start_time, end_time); 213 assert(time_diff >= 0); 214 ctx.total_time += time_diff; 215 fmt::printfln(" in {}.{:.9}s", 216 time_diff / 1000000000, 217 time_diff % 1000000000)!; 218 219 const stdout = printable(memio::buffer(&ctx.stdout)); 220 const stderr = printable(memio::buffer(&ctx.stderr)); 221 if (failed && (stdout != "" || stderr != "")) { 222 append(ctx.output, output { 223 test = test.name, 224 stdout = stdout, 225 stderr = stderr, 226 }); 227 }; 228 229 reset(ctx); 230 }; 231 232 fn run_test(ctx: *context, test: test) status = { 233 fmt::print(test.name)!; 234 dots(ctx.maxname - len(test.name) + 3); 235 bufio::flush(os::stdout)!; // write test name before test runs 236 237 let orig_stdout = os::stdout; 238 let orig_stderr = os::stderr; 239 os::stdout = &ctx.stdout; 240 os::stderr = &ctx.stderr; 241 defer rt::jmp = null; 242 const n = rt::setjmp(&jmpbuf): status; 243 if (n == status::RETURN) { 244 rt::jmp = &jmpbuf; 245 test.func(); 246 }; 247 248 os::stdout = orig_stdout; 249 os::stderr = orig_stderr; 250 return n; 251 }; 252 253 fn printable(buf: []u8) str = { 254 match (strings::fromutf8(buf)) { 255 case let s: str => 256 let it = strings::iter(s); 257 for (true) match (strings::next(&it)) { 258 case done => 259 return strings::dup(s); 260 case let r: rune => 261 if (ascii::valid(r) && !ascii::isprint(r) 262 && r != '\t' && r != '\n') { 263 break; 264 }; 265 }; 266 case utf8::invalid => void; 267 }; 268 269 let s = memio::dynamic(); 270 hex::dump(&s, buf)!; 271 return memio::string(&s)!; 272 }; 273 274 fn dots(n: size) void = { 275 for (let i = 0z; i < n; i += 1) { 276 fmt::print(".")!; 277 }; 278 }; 279 280 // returns true if test failed, false if it passed or was skipped 281 fn interpret_status(ctx: *context, test: str, status: status) bool = { 282 switch (status) { 283 case status::RETURN => 284 if (want_abort) { 285 styled_print(91, "FAIL"); 286 append(ctx.failures, failure { 287 test = test, 288 reason = rt::abort_reason { 289 msg = "Expected test to abort", 290 ... 291 }, 292 }); 293 return true; 294 } else { 295 styled_print(92, "PASS"); 296 return false; 297 }; 298 case status::ABORT => 299 if (want_abort) { 300 styled_print(92, "PASS"); 301 return false; 302 } else { 303 styled_print(91, "FAIL"); 304 append(ctx.failures, failure { 305 test = test, 306 reason = rt::reason, 307 }); 308 return true; 309 }; 310 case status::SKIP => 311 styled_print(37, "SKIP"); 312 append(ctx.skipped, skipped { 313 test = test, 314 reason = rt::reason.msg, 315 }); 316 return false; 317 case status::SEGV => 318 styled_print(91, "FAIL"); 319 append(ctx.failures, failure { 320 test = test, 321 reason = rt::abort_reason { 322 msg = "Segmentation fault", 323 ... 324 }, 325 }); 326 return true; 327 }; 328 }; 329 330 fn styled_print(color: int, result: fmt::formattable) void = { 331 if (colored()) { 332 fmt::printf("\x1b[{}m" "{}" "\x1b[m", color, result)!; 333 } else { 334 fmt::print(result)!; 335 }; 336 }; 337 338 fn handle_segv( 339 sig: signal::sig, 340 info: *signal::siginfo, 341 ucontext: *opaque, 342 ) void = { 343 rt::longjmp(&jmpbuf, status::SEGV); 344 }; 345 346 fn easter_egg(fails: []failure, tests: []test) void = { 347 // norwegian deadbeef 348 let blob: [_]u8 = [ 349 0xe1, 0x41, 0xf2, 0x21, 0x3f, 0x9e, 0x2d, 0xfe, 0x3f, 0x9e, 350 0x22, 0xfc, 0x43, 0xc2, 0x2f, 0x82, 0x15, 0xd1, 0x62, 0xae, 351 0x6c, 0x9e, 0x71, 0xfe, 0x33, 0xc2, 0x71, 0xfe, 0x63, 0xb4, 352 0x2d, 0xfe, 0x3f, 0xe1, 0x52, 0xf2, 0x43, 0xc6, 0x2d, 0xf9, 353 0x3d, 0x90, 0x07, 0xfe, 0x33, 0x9c, 0x2d, 0xfe, 0x3f, 0x96, 354 0x2d, 0x8f, 0x3f, 0x9e, 0x64, 0xd4, 0x33, 0x9c, 0x21, 0xfe, 355 0x3f, 0x9e, 0x2d, 0x82, 0x40, 0x9e, 0x54, 0xf9, 0x15, 0x99, 356 0x30, 0xfe, 0x3f, 0x92, 0x2d, 0xfe, 0x31, 0x9e, 0x2d, 0xfe, 357 0x38, 0xb4, 0x2d, 0xf9, 0x22, 0x83, 0x52, 0xf9, 0x40, 0xe1, 358 0x30, 0xe3, 0x38, 0x9e, 0x2d, 0xd4, 359 ]; 360 let words = &blob: *[24]u32; 361 362 // doesn't currently work on big-endian, would need to re-find the 363 // constants and use a different blob there 364 if (words[0]: u8 != 0xe1) return; 365 366 words[0] ^= len(tests): u32; 367 368 let hash = 2166136261u32; 369 for (let i = 0z; i < size(u32); i += 1) { 370 hash = (hash ^ blob[i]) * 16777619; 371 }; 372 373 for (let i = 0z; i < len(words); i += 1) { 374 words[i] ^= hash; 375 }; 376 377 if (-len(fails): u32 == words[0]) { 378 io::write(os::stdout, blob[size(u32)..])!; 379 }; 380 };