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