hare

[hare] The Hare programming language
git clone https://git.torresjrjr.com/hare.git
Log | Files | Refs | README | LICENSE

+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 };