Commit d4fd7c6a01

Noam Preil <pleasantatk@gmail.com>
2020-06-02 21:29:59
Stage2/Testing: Staged test harness draft design
1 parent 2ed07a3
Changed files (1)
src-self-hosted
src-self-hosted/test.zig
@@ -56,16 +56,27 @@ fn findOffset(src: []const u8, line: usize, column: usize) ?usize {
 }
 
 pub const TestContext = struct {
+    // TODO: remove these. They are deprecated.
     zir_cmp_output_cases: std.ArrayList(ZIRCompareOutputCase),
+    // TODO: remove
     zir_transform_cases: std.ArrayList(ZIRTransformCase),
+    // TODO: remove
     zir_error_cases: std.ArrayList(ZIRErrorCase),
 
+    /// TODO: find a way to treat cases as individual tests as far as
+    /// `zig test` is concerned. If we have 100 tests, they should *not* be
+    /// considered as *one*. "ZIR" isn't really a *test*, it's a *category* of
+    /// tests.
+    zir_cases: std.ArrayList(ZIRCase),
+
+    // TODO: remove
     pub const ZIRCompareOutputCase = struct {
         name: []const u8,
         src_list: []const []const u8,
         expected_stdout_list: []const []const u8,
     };
 
+    // TODO: remove
     pub const ZIRTransformCase = struct {
         name: []const u8,
         cross_target: std.zig.CrossTarget,
@@ -96,6 +107,7 @@ pub const TestContext = struct {
         }
     };
 
+    // TODO: remove
     pub const ZIRErrorCase = struct {
         name: []const u8,
         src: [:0]const u8,
@@ -103,6 +115,83 @@ pub const TestContext = struct {
         cross_target: std.zig.CrossTarget,
     };
 
+    pub const ZIRStageType = enum {
+        /// A transformation stage transforms the input ZIR and tests against
+        /// the expected output
+        Transformation,
+        /// An error stage attempts to compile bad code, and ensures that it
+        /// fails to compile, and for the expected reasons
+        Error,
+        /// An execution stage compiles and runs the input ZIR, feeding in
+        /// provided input and ensuring that the outputs match what is expected
+        Execution,
+        /// A compilation stage checks that the ZIR compiles without any issues
+        Compiles,
+    };
+
+    pub const ZIRStage = struct {
+        /// The input to the current stage. We simulate an incremental update
+        /// with the file's contents changed to this value each stage.
+        ///
+        /// This value can change entirely between stages, which would be akin
+        /// to deleting the source file and creating a new one from scratch; or
+        /// you can keep it mostly consistent, with small changes, testing the
+        /// effects of the incremental compilation.
+        src: [:0]const u8,
+        case: union(ZIRStageType) {
+            /// The expected output ZIR
+            Transformation: []const u8,
+            /// A slice containing the expected errors *in sequential order*.
+            Error: []const ErrorMsg,
+
+            /// Input to feed to the program, and expected outputs.
+            ///
+            /// If stdout, stderr, and exit_code are all null, addZIRCase will
+            /// discard the test. To test for successful compilation, use a
+            /// dedicated Compile stage instead.
+            Execution: struct {
+                stdin: ?[]const u8,
+                stdout: ?[]const u8,
+                stderr: ?[]const u8,
+                exit_code: ?u8,
+            },
+            /// A Compiles test checks only that compilation of the given ZIR
+            /// succeeds. To test outputs, use an Execution test. It is good to
+            /// use a Compiles test before an Execution, as the overhead should
+            /// be low (due to incremental compilation) and TODO: provide a way
+            /// to check changed / new / etc decls in testing mode
+            /// (usingnamespace a debug info struct with a comptime flag?)
+            Compiles: void,
+        },
+    };
+
+    /// A ZIRCase consists of a set of *stages*. A stage can transform ZIR,
+    /// compile it, ensure that compilation fails, and more. The same Module is
+    /// used for each stage, so each stage's source is treated as a single file
+    /// being updated by the test harness and incrementally compiled.
+    pub const ZIRCase = struct {
+        name: []const u8,
+        /// The platform the ZIR targets. For non-native platforms, an emulator
+        /// such as QEMU is required for tests to complete.
+        ///
+        target: std.zig.CrossTarget,
+        stages: []ZIRStage,
+    };
+
+    pub fn addZIRCase(
+        ctx: *TestContext,
+        name: []const u8,
+        target: std.zig.CrossTarget,
+        stages: []ZIRStage,
+    ) !void {
+        const case = .{
+            .name = name,
+            .target = target,
+            .stages = stages,
+        };
+        try ctx.cases.append(case);
+    }
+
     pub fn addZIRCompareOutput(
         ctx: *TestContext,
         name: []const u8,
@@ -196,6 +285,14 @@ pub const TestContext = struct {
 
         const native_info = try std.zig.system.NativeTargetInfo.detect(std.heap.page_allocator, .{});
 
+        for (self.zir_cases.items) |case| {
+            std.testing.base_allocator_instance.reset();
+            const info = try std.zig.system.NativeTargetInfo.detect(std.testing.allocator, case.target);
+            try self.runOneZIRCase(std.testing.allocator, root_node, case, info.target);
+            try std.testing.allocator_instance.validate();
+        }
+
+        // TODO: wipe the rest of this function
         for (self.zir_cmp_output_cases.items) |case| {
             std.testing.base_allocator_instance.reset();
             try self.runOneZIRCmpOutputCase(std.testing.allocator, root_node, case, native_info.target);
@@ -215,6 +312,75 @@ pub const TestContext = struct {
         }
     }
 
+    fn runOneZIRCase(self: *TestContext, allocator: *Allocator, root_node: *std.Progress.Node, case: ZIRCase, target: std.Target) !void {
+        var tmp = std.testing.tmpDir(.{});
+        defer tmp.cleanup();
+
+        const tmp_src_path = "test_case.zir";
+        const root_pkg = try Package.create(allocator, tmp.dir, ".", tmp_src_path);
+        defer root_pkg.destroy();
+
+        var prg_node = root_node.start(case.name, case.stages.len);
+        prg_node.activate();
+        defer prg_node.end();
+
+        var module = try Module.init(allocator, .{
+            .target = target,
+            // This is an Executable, as opposed to e.g. a *library*. This does
+            // not mean no ZIR is generated.
+            //
+            // TODO: support tests for object file building, and library builds
+            // and linking. This will require a rework to support multi-file
+            // tests.
+            .output_mode = .Exe,
+            // TODO: support testing optimizations
+            .optimize_mode = .Debug,
+            .bin_file_dir = tmp.dir,
+            .bin_file_path = "test_case",
+            .root_pkg = root_pkg,
+        });
+        defer module.deinit();
+
+        for (case.stages) |s| {
+            // TODO: remove before committing. This is for ZLS ;)
+            const stage: ZIRStage = s;
+
+            var stage_node = prg_node.start("stage", 4);
+            stage_node.activate();
+            defer stage_node.end();
+
+            var sync_node = stage_node.start("write", null);
+            sync_node.activate();
+            try tmp.dir.writeFile(tmp_src_path, stage.src);
+            sync_node.end();
+
+            var module_node = stage_node.start("parse/analysis/codegen", null);
+            module_node.activate();
+            try module.update();
+            module_node.end();
+
+            switch (stage.case) {
+                .Transformation => |expected_output| {
+                    var emit_node = stage_node.start("emit", null);
+                    emit_node.activate();
+                    var new_zir_module = try zir.emit(allocator, module);
+                    defer new_zir_module.deinit(allocator);
+                    emit_node.end();
+
+                    var write_node = stage_node.start("write", null);
+                    write_node.activate();
+                    var out_zir = std.ArrayList(u8).init(allocator);
+                    defer out_zir.deinit();
+                    try new_zir_module.writeToStream(allocator, out_zir.outStream());
+                    write_node.end();
+
+                    std.testing.expectEqualSlices(u8, expected_output, out_zir.items);
+                },
+                else => return error.unimplemented,
+            }
+        }
+    }
+
     fn runOneZIRCmpOutputCase(
         self: *TestContext,
         allocator: *Allocator,
@@ -426,6 +592,10 @@ pub const TestContext = struct {
             e.* = false;
         }
 
+        // TODO: check the input error list in sequential order, manually
+        // incrementing indices when needed. This would allow deduplicating the
+        // following three blocks into one, and the restriction it imposes on
+        // test writers is one that naturally flows anyways.
         {
             var i = module.failed_files.iterator();
             while (i.next()) |pair| {