|
| 1 | +// Jai, being a very young language, doesn't have a built-in testing framework. |
| 2 | +// This file provides basic testing functionality that can be used in exercises |
| 3 | +// to run the tests and print a report of which tests passed and which failed. |
| 4 | + |
| 5 | +// This macro is used to compare the expected and actual values in a test. |
| 6 | +// It works on Code values for the expected and actual values, which allows it to capture |
| 7 | +// the code of the test assertion for reporting purposes. |
| 8 | +assert_equal :: (expected: Code, actual: Code, call := #caller_code, loc := #caller_location) #expand { |
| 9 | + // The test name is derived from the name of the test procedure, which is the caller of this macro. |
| 10 | + test_name := #procedure_name(); |
| 11 | + |
| 12 | + // Convert the call to this macro into its original source code form for reporting purposes. |
| 13 | + test_code_builder: String_Builder; |
| 14 | + print_expression(*test_code_builder, compiler_get_nodes(call)); |
| 15 | + test_code := builder_to_string(*test_code_builder); |
| 16 | + |
| 17 | + expected_value := #insert expected; |
| 18 | + actual_value := #insert actual; |
| 19 | + |
| 20 | + if expected_value == actual_value { |
| 21 | + set_test_result(test_name, .PASSED, "", test_code); |
| 22 | + } else { |
| 23 | + message := sprint("Expected: %, actual: %", expected_value, actual_value); |
| 24 | + set_test_result(test_name, .FAILED, message, test_code); |
| 25 | + |
| 26 | + // Short-circuit test execution to avoid cascading failures |
| 27 | + `return; |
| 28 | + } |
| 29 | +} |
| 30 | + |
| 31 | +// This macro allows skipping a test. |
| 32 | +// A skipped test's code will _not_ be executed. |
| 33 | +skip :: (code: Code) #expand { |
| 34 | + // Do not skip the test if the '-run_skipped' build option was provided. |
| 35 | + // This is used by the test runner to forcibly run _all_ tests, including skipped tests. |
| 36 | + skip_test := !has_build_option("-run_skipped"); |
| 37 | + |
| 38 | + if skip_test { |
| 39 | + // Get the name of the test procedure being skipped for reporting purposes |
| 40 | + test_procedure_call := cast (*Code_Procedure_Call)compiler_get_nodes(code); |
| 41 | + test_procedure_identifier := cast (*Code_Ident)test_procedure_call.procedure_expression; |
| 42 | + test_name := test_procedure_identifier.name; |
| 43 | + set_test_result(test_name, .SKIPPED, "", ""); |
| 44 | + |
| 45 | + // As we're skipping the test, we don't insert the original test procedure call, which prevents |
| 46 | + // the test from being executed |
| 47 | + } else { |
| 48 | + // Don't skip the test, so insert the original test procedure call to execute the test as normal. |
| 49 | + #insert code; |
| 50 | + } |
| 51 | +} |
| 52 | + |
| 53 | +set_test_result :: inline (test_name: string, status: TestStatus, message: string, test_code: string) { |
| 54 | + test: TestResult = .{test_name, status, message, test_code}; |
| 55 | + |
| 56 | + // If there already is a test result for this test name, it means the test has multiple assertions. |
| 57 | + // As we short-circuit test execution on failure, the existing test result _must_ be a passing test, |
| 58 | + // so we can safely overwrite the existing test result |
| 59 | + if context.test_run.tests.count > 0 && context.test_run.tests[context.test_run.tests.count - 1].name == test_name { |
| 60 | + context.test_run.tests[context.test_run.tests.count - 1] = test; |
| 61 | + } else { |
| 62 | + array_add(*context.test_run.tests, test); |
| 63 | + } |
| 64 | + |
| 65 | + // If we have one failing test, the whole test run is considered failed |
| 66 | + if status == .FAILED context.test_run.status = .FAILED; |
| 67 | +} |
| 68 | + |
| 69 | +before_running_tests :: () { |
| 70 | + set_build_options_dc(.{do_output=false}); |
| 71 | + context.started_at = current_time_monotonic(); |
| 72 | +} |
| 73 | + |
| 74 | +after_running_tests :: () { |
| 75 | + if has_build_option("-json") { |
| 76 | + write_json_test_report(); |
| 77 | + } else { |
| 78 | + print_test_report(); |
| 79 | + } |
| 80 | + |
| 81 | + if context.test_run.status == .FAILED { |
| 82 | + exit(1); |
| 83 | + } else { |
| 84 | + exit(0); |
| 85 | + } |
| 86 | +} |
| 87 | + |
| 88 | +// Print the test results in the Exercism Test Runner output format: |
| 89 | +// https://exercism.org/docs/building/tooling/test-runners/interface#h-output-format |
| 90 | +write_json_test_report :: () { |
| 91 | + // As there is no built-in JSON support in Jai, we manually build a JSON string. |
| 92 | + json_builder: String_Builder; |
| 93 | + init_string_builder(*json_builder); |
| 94 | + |
| 95 | + append(*json_builder, "{\n"); |
| 96 | + append(*json_builder, " \"version\": 2,\n"); |
| 97 | + append(*json_builder, tprint(" \"status\": \"%\",\n", test_status_to_string(context.test_run.status))); |
| 98 | + append(*json_builder, " \"tests\": [\n"); |
| 99 | + |
| 100 | + for context.test_run.tests { |
| 101 | + append(*json_builder, " {\n"); |
| 102 | + append(*json_builder, tprint(" \"name\": \"%\",\n", json_escape_string(it.name))); |
| 103 | + append(*json_builder, tprint(" \"status\": \"%\",\n", test_status_to_string(it.status))); |
| 104 | + if it.message.count > 0 { |
| 105 | + append(*json_builder, tprint(" \"message\": \"%\",\n", json_escape_string(it.message))); |
| 106 | + } |
| 107 | + append(*json_builder, tprint(" \"test_code\": \"%\"\n", json_escape_string(it.test_code))); |
| 108 | + append(*json_builder, " }"); |
| 109 | + if it_index < context.test_run.tests.count - 1 { |
| 110 | + append(*json_builder, ","); |
| 111 | + } |
| 112 | + append(*json_builder, "\n"); |
| 113 | + } |
| 114 | + |
| 115 | + append(*json_builder, " ]\n"); |
| 116 | + append(*json_builder, "}\n"); |
| 117 | + |
| 118 | + json := builder_to_string(*json_builder); |
| 119 | + write_entire_file("results.json", json); |
| 120 | +} |
| 121 | + |
| 122 | +json_escape_string :: (str: string) -> string { |
| 123 | + escaped_builder: String_Builder; |
| 124 | + init_string_builder(*escaped_builder); |
| 125 | + |
| 126 | + for str { |
| 127 | + if it == { |
| 128 | + case #char "\""; append(*escaped_builder, "\\\""); |
| 129 | + case #char "\\"; append(*escaped_builder, "\\\\"); |
| 130 | + case #char "\n"; append(*escaped_builder, "\\n"); |
| 131 | + case #char "\r"; append(*escaped_builder, "\\r"); |
| 132 | + case #char "\t"; append(*escaped_builder, "\\t"); |
| 133 | + case; append(*escaped_builder, it); |
| 134 | + } |
| 135 | + } |
| 136 | + return builder_to_string(*escaped_builder); |
| 137 | +} |
| 138 | + |
| 139 | +test_status_to_string :: inline (status: TestStatus) -> string { |
| 140 | + if status == { |
| 141 | + case .PASSED; |
| 142 | + return "pass"; |
| 143 | + case .FAILED; |
| 144 | + return "fail"; |
| 145 | + case .SKIPPED; |
| 146 | + return "skip"; |
| 147 | + case; |
| 148 | + return "error"; |
| 149 | + }; |
| 150 | +} |
| 151 | + |
| 152 | +print_test_report :: () { |
| 153 | + failed_tests, passed_tests, skipped_tests := 0; |
| 154 | + duration := to_float64_seconds(current_time_monotonic() - context.started_at); |
| 155 | + |
| 156 | + for test: context.test_run.tests { |
| 157 | + if test.status == { |
| 158 | + case .PASSED; |
| 159 | + passed_tests += 1; |
| 160 | + case .FAILED; |
| 161 | + failed_tests += 1; |
| 162 | + print("Test '%' failed.\n %\n", test.name, test.message); |
| 163 | + case .SKIPPED; |
| 164 | + skipped_tests += 1; |
| 165 | + } |
| 166 | + } |
| 167 | + print("Test summary: total: %, failed: %, succeeded: %, skipped: %, duration: % seconds\n", failed_tests + passed_tests + skipped_tests, failed_tests, passed_tests, skipped_tests, duration); |
| 168 | +} |
| 169 | + |
| 170 | +has_build_option :: inline (arg: string) -> bool { |
| 171 | + for get_build_options().compile_time_command_line if it == arg return true; |
| 172 | + return false; |
| 173 | +} |
| 174 | + |
| 175 | +TestStatus :: enum u8 { |
| 176 | + PASSED; |
| 177 | + FAILED; |
| 178 | + SKIPPED; |
| 179 | +} |
| 180 | + |
| 181 | +TestResult :: struct { |
| 182 | + name: string; |
| 183 | + status: TestStatus; |
| 184 | + message: string; |
| 185 | + test_code: string; |
| 186 | +}; |
| 187 | + |
| 188 | +TestRun :: struct { |
| 189 | + version := 2; |
| 190 | + status := TestStatus.PASSED; |
| 191 | + tests: [..] TestResult; |
| 192 | +}; |
| 193 | + |
| 194 | +#add_context test_run: TestRun; |
| 195 | +#add_context started_at: Apollo_Time; |
| 196 | + |
| 197 | +#import "Basic"; |
| 198 | +#import "Compiler"; |
| 199 | +#import "File"; |
| 200 | +#import "Program_Print"; |
0 commit comments