module test_suite_runner; import std::io, std::math, std::os, std::collections; import libc; Path compiler_path; int test_count; int skip_count; int success_count; Path start_cwd; Path test_dir; bool print_to_file; String stdlib; fn void main(String[] args) { // Grab the name for `usage` String appname = args[0]; if (args.len < 3) usage(appname); // Do we want the print to file variant print_to_file = !io::stdout().isatty(); // Retain our current path. start_cwd = path::tcwd()!!; // Create our test path, note that this prevents us from doing tests in parallell test_dir = start_cwd.tappend("_c3test_")!!; defer (void)path::rmtree(test_dir); // Find the compiler Path! path = start_cwd.tappend(args[1]); if (catch path) arg_error_exit(appname, "Invalid compiler path: %s", args[1]); // Is it a valid file? if (!path::is_file(path)) { error_exit("Error: Invalid path to compiler: %s (%s relative to %s)", path.path_string, args[1], start_cwd); } // Ok we're done. compiler_path = path; bool only_skipped = false; for (int i = 3; i < args.len; i++) { String arg = args[i]; switch (arg) { case "--no-terminal": print_to_file = true; case "-s": case "--skipped": only_skipped = true; case "--stdlib": if (i == args.len - 1 || args[i + 1].starts_with("-")) { arg_error_exit(appname, "Expected --stdlib to be followed by the path to the alternative standard library."); } stdlib = args[i + 1]; i++; if (!os::native_is_dir(stdlib)) { error_exit(appname, "Stdlib directory '%s' cannot be found.", stdlib); } stdlib = start_cwd.tappend(stdlib).str_view()!!; default: arg_error_exit(appname, "Unknown option '%s'.", args[i]); } } if (only_skipped && args[3] != "-s" && args[3] != "--skipped") usage(appname); // Get the directory or file to test Path! file = args[2].to_tpath(); if (catch file) arg_error_exit(appname, "Invalid path: '%s'.", args[2]); // Now just run all tests recursively. switch { case path::is_file(file): test_file(file); case path::is_dir(file): test_path(file)!!; default: error_exit("Error: Path wasn't to directory or file: %s", file); } // Print the test result io::printfn("Found %d tests: %.1f%% (%d / %d) passed (%d skipped).", test_count, (100.0 * success_count) / math::max(1, test_count - skip_count), success_count, test_count - skip_count, skip_count); libc::exit(success_count == test_count - skip_count ? 0 : 1); } struct Error { int line; String text; } <* Information about a given file that we're running *> struct RunFile { String name; bool is_output; union { struct { File file; List {Error} warnings; List {Error} errors; int line_offset; } List {String} expected_lines; } } fn void RunFile.add_line(&self, String line) { if (self.is_output) { line = line.trim(); if (line == "") return; self.expected_lines.push(line); } else { io::fprintn(&self.file, line)!!; } } fn RunFile*! create_input_file(String filename) { File file = file::open_path(test_dir.tappend(filename), "wb")!; RunFile *run_file = mem::temp_new(RunFile, { .name = filename, .file = file, .is_output = false }); return run_file; } fn RunFile*! create_output_file(String filename) { RunFile *run_file = mem::temp_new(RunFile, { .name = filename, .is_output = true }); return run_file; } fn void RunFile.close(&self) { if (!self.is_output) (void)self.file.close(); } struct RunSettings { bool safe; bool debuginfo; bool no_deprecation; List{String} opts; String arch; List{RunFile*} input_files; List{RunFile*} output_files; RunFile* current_file; } <* Check a line from stderr returned by the compiler *> fn bool check_line(RunSettings* settings, String type, String file, String line_str, String col, String message) => @pool() { // Convert the line number int line = line_str.to_int()!!; // Get the base name String basename = file.file_tbasename()!!; // Loop through our input files (usually only 1!) foreach (f : settings.input_files) { if (f.name != basename) continue; List {Error}* list; switch (type) { case "Error": list = &f.errors; break; case "Warning": list = &f.warnings; break; default: error_exit("FAILED - Unknown error type '%s'", type); } // See if we match the message foreach (i, err : *list) { if (line != err.line) continue; if (!message.contains(err.text)) return false; // Match, remove it! list.remove_at(i); return true; } } // No match return false; } <* Parse all stderr output *> fn bool parse_result(DString out, RunSettings settings) { // Note that this is a bit inefficient bool success = true; int errors = 0; foreach (line : out.str_view().tsplit("\n")) { // Skip empty lines if (!line) continue; // Split the output from the compiler using `|` String[] parts = line.tsplit("|", 5); // We should have 5 parts, otherwise just print an error for this whole thing. if (parts.len != 5) { io::printn("FAILED - Unexpected response from compiler:"); io::printn("Output: ----------------------------------------------------------"); io::print(out); io::printn("------------------------------------------------------------------"); return false; } // Check the line if (!check_line(&settings, ...parts[:5])) { // Print error if there is no match. if (success) { io::printn("FAILED\n\n Unexpected compilation errors:"); io::printn(" ------------------------------"); } io::printf(" %d. %s at %s:%s: ", ++errors, parts[0], parts[1].file_tbasename()!!, parts[2]); io::printfn(`"%s"`, parts[4]); success = false; } } // Now let's check for missing errors: int not_found_errors, not_found_warnings; foreach (file : settings.input_files) { if (file.errors.len()) { if (success) io::printn("FAILED - Missing errors"); if (!not_found_errors) { io::printn(); io::printn(" Errors that never occurred:"); io::printn(" ---------------------------"); } success = false; foreach (i, &item : file.errors) { io::printfn(` %d. %s:%d expected: "%s"`, ++not_found_errors, file.name, item.line, item.text); } } } // Missing warnings foreach (file : settings.input_files) { if (file.warnings.len()) { if (success) io::printn("FAILED - Missing warnings"); success = false; if (!not_found_warnings) { if (!success) io::printn(); io::printn(" Warnings that never occurred:"); io::printn(" -----------------------------"); } foreach (i, &item : file.warnings) { io::printn(file.name); io::printn("Ok"); io::printn(item.text); io::printfn(` %d. %s:%d expected: "%s"`, ++not_found_errors, file.name, item.line, item.text); } } } if (!success) io::printn(); return success; } <* Parse trailing directives #warning and #error *> fn void parse_trailing_directive(int line_number, String line, RunSettings* settings, bool is_single) { if (settings.current_file.is_output) return; usz index = line.rindex_of("// #")!! + 4; line = line[index..].trim(); switch { case line.starts_with("warning:"): line = line[8..].trim(); settings.current_file.warnings.push({ line_number, line }); case line.starts_with("error:"): line = line[6..].trim(); settings.current_file.errors.push({ line_number, line }); default: error_exit("FAILED - Unknown trailing directive '%s'", line); } } <* Parse header directives, #error, #safe, #debuginfo, #opt, #target, #deprecation, #file and #expect *> fn void parse_header_directive(int* line_no, String line, RunSettings* settings, bool is_single) { line = line[4..].trim(); switch { case line.starts_with("error:"): line = line[6..].trim(); if (settings.current_file.is_output) return; settings.current_file.errors.push({ *line_no, line }); case line.starts_with("safe:"): if (settings.current_file.is_output) return; settings.safe = line[5..].trim() == "yes"; case line.starts_with("debuginfo:"): if (settings.current_file.is_output) return; settings.debuginfo = line[10..].trim() == "yes"; case line.starts_with("opt:"): if (settings.current_file.is_output) return; settings.opts.push(line[4..].trim()); case line.starts_with("target:"): if (settings.current_file.is_output) return; settings.arch = line[7..].trim(); case line.starts_with("deprecation:"): if (settings.current_file.is_output) return; settings.no_deprecation = line[12..].trim() == "no"; case line.starts_with("file:"): if (is_single) error_exit("FAILED - 'file' directive only allowed with .c3t"); settings.current_file.close(); line = line[5..].trim(); RunFile* file = settings.current_file = create_input_file(line)!!; *line_no = 1; settings.input_files.push(file); settings.current_file = file; case line.starts_with("expect:"): if (is_single) error_exit("FAILED - 'expect' directive only allowed with .c3t"); line = line[7..].trim(); settings.current_file.close(); RunFile* file = settings.current_file = create_output_file(line)!!; settings.output_files.push(file); default: io::printfn("FAILED - Unknown header directive '%s'", line); libc::exit(1); } } <* Test a file *> fn void test_file(Path file_path) { test_count++; if (print_to_file) { io::printf("- %d/%d %s: ", test_count, test_count - success_count - 1, file_path); } else { io::printf("- %d/%d Compiling: %s ", test_count, test_count - success_count - 1, file_path); } (void)io::stdout().flush(); static int count = 1; bool single; (void)path::rmtree(test_dir); if (@catch(path::mkdir(test_dir))) { error_exit("FAILED - Failed to create temp test directory '%s'.", test_dir); } switch (file_path.extension() ?? "") { case "c3": single = true; case "c3t": single = false; default: error_exit("FAILED - Unexpected file name '%s', expected a file with a '.c3' or '.c3t' suffix.", file_path.str_view()); } File! f = file::open_path(file_path, "rb"); if (catch f) { error_exit("FAILED - Failed to open '%s'.", file_path); } defer (void)f.close(); RunSettings settings; settings.current_file = create_input_file(file_path.basename()[..^(single ? 4 : 5)].tconcat(".c3"))!!; settings.input_files.push(settings.current_file); int line_no = 1; while (try line = io::treadline(&f)) { if (line.starts_with("// #") || line.starts_with("/* #")) { parse_header_directive(&line_no, line, &settings, single); continue; } else if (line.contains("// #")) { parse_trailing_directive(line_no, line, &settings, single); } settings.current_file.add_line(line); line_no++; } settings.current_file.close(); // Construct the compile line List{String} cmdline; cmdline.push(compiler_path.str_view()); cmdline.push("compile-only"); cmdline.push("--test"); if (stdlib) { cmdline.push("--stdlib"); cmdline.push(stdlib); cmdline.push("--enable-new-generics"); } cmdline.push("--llvm-out"); cmdline.push("."); foreach (file : settings.input_files) { cmdline.push(file.name); } if (!single) cmdline.push("--emit-llvm"); cmdline.push(settings.debuginfo ? "-g" : "-g0"); if (settings.arch) { cmdline.push("--target"); cmdline.push(settings.arch); } cmdline.push("-O0"); if (settings.no_deprecation) cmdline.push("--silence-deprecation"); cmdline.push(settings.safe ? "--safe=yes" : "--safe=no"); foreach (opt : settings.opts) { cmdline.push(opt); } path::chdir(test_dir)!!; // Start process SubProcess compilation = process::create(cmdline.array_view(), { .search_user_path, .no_window, .inherit_environment })!!; defer compilation.destroy(); CInt result = compilation.join()!!; DString out; io::copy_to(&&compilation.stderr(), &out)!!; if (result != 0 && result != 1) { (void)io::copy_to(&&compilation.stdout(), &out); io::printfn("FAILED - Error(%s): ", result, out); return; } if (!parse_result(out, settings)) return; foreach (file : settings.output_files) { if (!file::exists(file.name)) { io::printfn("FAILED - Did not compile file %s.", file.name); return; } File file_ll = file::open(file.name, "rb")!!; defer (void)file_ll.close(); String! next = file.expected_lines.pop_first(); while (try line = io::treadline(&file_ll) && try value = next) { line = line.trim(); if (line == "") continue; if (line.contains(value)) { next = file.expected_lines.pop_first(); } } if (try next) { io::printfn(`FAILED - %s did not contain: "%s"`, file.name, next); io::printfn("\n\n\n---------------------------------------------------> %s\n\n", file.name); (void)file_ll.seek(0); (void)io::printn((String)io::read_fully(tmem(), &file_ll)); io::printfn("<---------------------------------------------------- %s\n", file_path); return; } } if (print_to_file) { io::print("Passed."); io::printn(); } else { for (int i = 0; i < 200; i++) io::print("\b \b"); } success_count++; } fn void! test_path(Path file_path) { (void)path::chdir(start_cwd); foreach (file : path::ls(tmem(), file_path)!!) { @pool() { (void)path::chdir(start_cwd); file = file_path.tappend(file.str_view())!; switch { case path::is_dir(file): test_path(file)!; case path::is_file(file): switch (file.extension() ?? "") { case "c3": case "c3t": test_file(file); } default: io::printfn("Skip %s", file); // Ignore } }; } } fn void usage(String appname) @noreturn { io::printfn("Usage: %s [options]", appname); io::printn(); io::printn("Options:"); io::printn(" -s, --skipped only run skipped tests"); io::printn(" --stdlib override the path to stdlib"); libc::exit(0); } fn void arg_error_exit(String appname, String fmt, args...) @noreturn { io::printfn(fmt, ...args); usage(appname); } fn void error_exit(String fmt, args...) @noreturn { io::printfn(fmt, ...args); libc::exit(1); }