| ... | @@ -56,16 +56,27 @@ fn findOffset(src: []const u8, line: usize, column: usize) ?usize { | ... | @@ -56,16 +56,27 @@ fn findOffset(src: []const u8, line: usize, column: usize) ?usize { |
| 56 | } | 56 | } |
| 57 | | 57 | |
| 58 | pub const TestContext = struct { | 58 | pub const TestContext = struct { |
| | 59 | // TODO: remove these. They are deprecated. |
| 59 | zir_cmp_output_cases: std.ArrayList(ZIRCompareOutputCase), | 60 | zir_cmp_output_cases: std.ArrayList(ZIRCompareOutputCase), |
| | 61 | // TODO: remove |
| 60 | zir_transform_cases: std.ArrayList(ZIRTransformCase), | 62 | zir_transform_cases: std.ArrayList(ZIRTransformCase), |
| | 63 | // TODO: remove |
| 61 | zir_error_cases: std.ArrayList(ZIRErrorCase), | 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 | pub const ZIRCompareOutputCase = struct { | 73 | pub const ZIRCompareOutputCase = struct { |
| 64 | name: []const u8, | 74 | name: []const u8, |
| 65 | src_list: []const []const u8, | 75 | src_list: []const []const u8, |
| 66 | expected_stdout_list: []const []const u8, | 76 | expected_stdout_list: []const []const u8, |
| 67 | }; | 77 | }; |
| 68 | | 78 | |
| | 79 | // TODO: remove |
| 69 | pub const ZIRTransformCase = struct { | 80 | pub const ZIRTransformCase = struct { |
| 70 | name: []const u8, | 81 | name: []const u8, |
| 71 | cross_target: std.zig.CrossTarget, | 82 | cross_target: std.zig.CrossTarget, |
| ... | @@ -96,6 +107,7 @@ pub const TestContext = struct { | ... | @@ -96,6 +107,7 @@ pub const TestContext = struct { |
| 96 | } | 107 | } |
| 97 | }; | 108 | }; |
| 98 | | 109 | |
| | 110 | // TODO: remove |
| 99 | pub const ZIRErrorCase = struct { | 111 | pub const ZIRErrorCase = struct { |
| 100 | name: []const u8, | 112 | name: []const u8, |
| 101 | src: [:0]const u8, | 113 | src: [:0]const u8, |
| ... | @@ -103,6 +115,83 @@ pub const TestContext = struct { | ... | @@ -103,6 +115,83 @@ pub const TestContext = struct { |
| 103 | cross_target: std.zig.CrossTarget, | 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 | pub fn addZIRCompareOutput( | 195 | pub fn addZIRCompareOutput( |
| 107 | ctx: *TestContext, | 196 | ctx: *TestContext, |
| 108 | name: []const u8, | 197 | name: []const u8, |
| ... | @@ -196,6 +285,14 @@ pub const TestContext = struct { | ... | @@ -196,6 +285,14 @@ pub const TestContext = struct { |
| 196 | | 285 | |
| 197 | const native_info = try std.zig.system.NativeTargetInfo.detect(std.heap.page_allocator, .{}); | 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 | for (self.zir_cmp_output_cases.items) |case| { | 296 | for (self.zir_cmp_output_cases.items) |case| { |
| 200 | std.testing.base_allocator_instance.reset(); | 297 | std.testing.base_allocator_instance.reset(); |
| 201 | try self.runOneZIRCmpOutputCase(std.testing.allocator, root_node, case, native_info.target); | 298 | try self.runOneZIRCmpOutputCase(std.testing.allocator, root_node, case, native_info.target); |
| ... | @@ -215,6 +312,75 @@ pub const TestContext = struct { | ... | @@ -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 | fn runOneZIRCmpOutputCase( | 384 | fn runOneZIRCmpOutputCase( |
| 219 | self: *TestContext, | 385 | self: *TestContext, |
| 220 | allocator: *Allocator, | 386 | allocator: *Allocator, |
| ... | @@ -426,6 +592,10 @@ pub const TestContext = struct { | ... | @@ -426,6 +592,10 @@ pub const TestContext = struct { |
| 426 | e.* = false; | 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 | var i = module.failed_files.iterator(); | 600 | var i = module.failed_files.iterator(); |
| 431 | while (i.next()) |pair| { | 601 | while (i.next()) |pair| { |