hare

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

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