| ... | ... | @@ -56,16 +56,27 @@ fn findOffset(src: []const u8, line: usize, column: usize) ?usize { |
| 56 | 56 | } |
| 57 | 57 | |
| 58 | 58 | pub const TestContext = struct { |
| 59 | // TODO: remove these. They are deprecated. |
| 59 | 60 | zir_cmp_output_cases: std.ArrayList(ZIRCompareOutputCase), |
| 61 | // TODO: remove |
| 60 | 62 | zir_transform_cases: std.ArrayList(ZIRTransformCase), |
| 63 | // TODO: remove |
| 61 | 64 | zir_error_cases: std.ArrayList(ZIRErrorCase), |
| 62 | 65 | |
| 66 | /// TODO: find a way to treat cases as individual tests as far as |
| 67 | /// `zig test` is concerned. If we have 100 tests, they should *not* be |
| 68 | /// considered as *one*. "ZIR" isn't really a *test*, it's a *category* of |
| 69 | /// tests. |
| 70 | zir_cases: std.ArrayList(ZIRCase), |
| 71 | |
| 72 | // TODO: remove |
| 63 | 73 | pub const ZIRCompareOutputCase = struct { |
| 64 | 74 | name: []const u8, |
| 65 | 75 | src_list: []const []const u8, |
| 66 | 76 | expected_stdout_list: []const []const u8, |
| 67 | 77 | }; |
| 68 | 78 | |
| 79 | // TODO: remove |
| 69 | 80 | pub const ZIRTransformCase = struct { |
| 70 | 81 | name: []const u8, |
| 71 | 82 | cross_target: std.zig.CrossTarget, |
| ... | ... | @@ -96,6 +107,7 @@ pub const TestContext = struct { |
| 96 | 107 | } |
| 97 | 108 | }; |
| 98 | 109 | |
| 110 | // TODO: remove |
| 99 | 111 | pub const ZIRErrorCase = struct { |
| 100 | 112 | name: []const u8, |
| 101 | 113 | src: [:0]const u8, |
| ... | ... | @@ -103,6 +115,83 @@ pub const TestContext = struct { |
| 103 | 115 | cross_target: std.zig.CrossTarget, |
| 104 | 116 | }; |
| 105 | 117 | |
| 118 | pub const ZIRStageType = enum { |
| 119 | /// A transformation stage transforms the input ZIR and tests against |
| 120 | /// the expected output |
| 121 | Transformation, |
| 122 | /// An error stage attempts to compile bad code, and ensures that it |
| 123 | /// fails to compile, and for the expected reasons |
| 124 | Error, |
| 125 | /// An execution stage compiles and runs the input ZIR, feeding in |
| 126 | /// provided input and ensuring that the outputs match what is expected |
| 127 | Execution, |
| 128 | /// A compilation stage checks that the ZIR compiles without any issues |
| 129 | Compiles, |
| 130 | }; |
| 131 | |
| 132 | pub const ZIRStage = struct { |
| 133 | /// The input to the current stage. We simulate an incremental update |
| 134 | /// with the file's contents changed to this value each stage. |
| 135 | /// |
| 136 | /// This value can change entirely between stages, which would be akin |
| 137 | /// to deleting the source file and creating a new one from scratch; or |
| 138 | /// you can keep it mostly consistent, with small changes, testing the |
| 139 | /// effects of the incremental compilation. |
| 140 | src: [:0]const u8, |
| 141 | case: union(ZIRStageType) { |
| 142 | /// The expected output ZIR |
| 143 | Transformation: []const u8, |
| 144 | /// A slice containing the expected errors *in sequential order*. |
| 145 | Error: []const ErrorMsg, |
| 146 | |
| 147 | /// Input to feed to the program, and expected outputs. |
| 148 | /// |
| 149 | /// If stdout, stderr, and exit_code are all null, addZIRCase will |
| 150 | /// discard the test. To test for successful compilation, use a |
| 151 | /// dedicated Compile stage instead. |
| 152 | Execution: struct { |
| 153 | stdin: ?[]const u8, |
| 154 | stdout: ?[]const u8, |
| 155 | stderr: ?[]const u8, |
| 156 | exit_code: ?u8, |
| 157 | }, |
| 158 | /// A Compiles test checks only that compilation of the given ZIR |
| 159 | /// succeeds. To test outputs, use an Execution test. It is good to |
| 160 | /// use a Compiles test before an Execution, as the overhead should |
| 161 | /// be low (due to incremental compilation) and TODO: provide a way |
| 162 | /// to check changed / new / etc decls in testing mode |
| 163 | /// (usingnamespace a debug info struct with a comptime flag?) |
| 164 | Compiles: void, |
| 165 | }, |
| 166 | }; |
| 167 | |
| 168 | /// A ZIRCase consists of a set of *stages*. A stage can transform ZIR, |
| 169 | /// compile it, ensure that compilation fails, and more. The same Module is |
| 170 | /// used for each stage, so each stage's source is treated as a single file |
| 171 | /// being updated by the test harness and incrementally compiled. |
| 172 | pub const ZIRCase = struct { |
| 173 | name: []const u8, |
| 174 | /// The platform the ZIR targets. For non-native platforms, an emulator |
| 175 | /// such as QEMU is required for tests to complete. |
| 176 | /// |
| 177 | target: std.zig.CrossTarget, |
| 178 | stages: []ZIRStage, |
| 179 | }; |
| 180 | |
| 181 | pub fn addZIRCase( |
| 182 | ctx: *TestContext, |
| 183 | name: []const u8, |
| 184 | target: std.zig.CrossTarget, |
| 185 | stages: []ZIRStage, |
| 186 | ) !void { |
| 187 | const case = .{ |
| 188 | .name = name, |
| 189 | .target = target, |
| 190 | .stages = stages, |
| 191 | }; |
| 192 | try ctx.cases.append(case); |
| 193 | } |
| 194 | |
| 106 | 195 | pub fn addZIRCompareOutput( |
| 107 | 196 | ctx: *TestContext, |
| 108 | 197 | name: []const u8, |
| ... | ... | @@ -196,6 +285,14 @@ pub const TestContext = struct { |
| 196 | 285 | |
| 197 | 286 | const native_info = try std.zig.system.NativeTargetInfo.detect(std.heap.page_allocator, .{}); |
| 198 | 287 | |
| 288 | for (self.zir_cases.items) |case| { |
| 289 | std.testing.base_allocator_instance.reset(); |
| 290 | const info = try std.zig.system.NativeTargetInfo.detect(std.testing.allocator, case.target); |
| 291 | try self.runOneZIRCase(std.testing.allocator, root_node, case, info.target); |
| 292 | try std.testing.allocator_instance.validate(); |
| 293 | } |
| 294 | |
| 295 | // TODO: wipe the rest of this function |
| 199 | 296 | for (self.zir_cmp_output_cases.items) |case| { |
| 200 | 297 | std.testing.base_allocator_instance.reset(); |
| 201 | 298 | try self.runOneZIRCmpOutputCase(std.testing.allocator, root_node, case, native_info.target); |
| ... | ... | @@ -215,6 +312,75 @@ pub const TestContext = struct { |
| 215 | 312 | } |
| 216 | 313 | } |
| 217 | 314 | |
| 315 | fn runOneZIRCase(self: *TestContext, allocator: *Allocator, root_node: *std.Progress.Node, case: ZIRCase, target: std.Target) !void { |
| 316 | var tmp = std.testing.tmpDir(.{}); |
| 317 | defer tmp.cleanup(); |
| 318 | |
| 319 | const tmp_src_path = "test_case.zir"; |
| 320 | const root_pkg = try Package.create(allocator, tmp.dir, ".", tmp_src_path); |
| 321 | defer root_pkg.destroy(); |
| 322 | |
| 323 | var prg_node = root_node.start(case.name, case.stages.len); |
| 324 | prg_node.activate(); |
| 325 | defer prg_node.end(); |
| 326 | |
| 327 | var module = try Module.init(allocator, .{ |
| 328 | .target = target, |
| 329 | // This is an Executable, as opposed to e.g. a *library*. This does |
| 330 | // not mean no ZIR is generated. |
| 331 | // |
| 332 | // TODO: support tests for object file building, and library builds |
| 333 | // and linking. This will require a rework to support multi-file |
| 334 | // tests. |
| 335 | .output_mode = .Exe, |
| 336 | // TODO: support testing optimizations |
| 337 | .optimize_mode = .Debug, |
| 338 | .bin_file_dir = tmp.dir, |
| 339 | .bin_file_path = "test_case", |
| 340 | .root_pkg = root_pkg, |
| 341 | }); |
| 342 | defer module.deinit(); |
| 343 | |
| 344 | for (case.stages) |s| { |
| 345 | // TODO: remove before committing. This is for ZLS ;) |
| 346 | const stage: ZIRStage = s; |
| 347 | |
| 348 | var stage_node = prg_node.start("stage", 4); |
| 349 | stage_node.activate(); |
| 350 | defer stage_node.end(); |
| 351 | |
| 352 | var sync_node = stage_node.start("write", null); |
| 353 | sync_node.activate(); |
| 354 | try tmp.dir.writeFile(tmp_src_path, stage.src); |
| 355 | sync_node.end(); |
| 356 | |
| 357 | var module_node = stage_node.start("parse/analysis/codegen", null); |
| 358 | module_node.activate(); |
| 359 | try module.update(); |
| 360 | module_node.end(); |
| 361 | |
| 362 | switch (stage.case) { |
| 363 | .Transformation => |expected_output| { |
| 364 | var emit_node = stage_node.start("emit", null); |
| 365 | emit_node.activate(); |
| 366 | var new_zir_module = try zir.emit(allocator, module); |
| 367 | defer new_zir_module.deinit(allocator); |
| 368 | emit_node.end(); |
| 369 | |
| 370 | var write_node = stage_node.start("write", null); |
| 371 | write_node.activate(); |
| 372 | var out_zir = std.ArrayList(u8).init(allocator); |
| 373 | defer out_zir.deinit(); |
| 374 | try new_zir_module.writeToStream(allocator, out_zir.outStream()); |
| 375 | write_node.end(); |
| 376 | |
| 377 | std.testing.expectEqualSlices(u8, expected_output, out_zir.items); |
| 378 | }, |
| 379 | else => return error.unimplemented, |
| 380 | } |
| 381 | } |
| 382 | } |
| 383 | |
| 218 | 384 | fn runOneZIRCmpOutputCase( |
| 219 | 385 | self: *TestContext, |
| 220 | 386 | allocator: *Allocator, |
| ... | ... | @@ -426,6 +592,10 @@ pub const TestContext = struct { |
| 426 | 592 | e.* = false; |
| 427 | 593 | } |
| 428 | 594 | |
| 595 | // TODO: check the input error list in sequential order, manually |
| 596 | // incrementing indices when needed. This would allow deduplicating the |
| 597 | // following three blocks into one, and the restriction it imposes on |
| 598 | // test writers is one that naturally flows anyways. |
| 429 | 599 | { |
| 430 | 600 | var i = module.failed_files.iterator(); |
| 431 | 601 | while (i.next()) |pair| { |