From 9ab9ebf33f7637236cea568a3565888b78fd08e5 Mon Sep 17 00:00:00 2001 From: Manlio Perillo Date: Mon, 15 May 2023 15:23:10 +0200 Subject: [PATCH] Refactor testing support Following the implementation in `std.Build.Step.Compile, add the Kind type to differentiate between a normal executable and a test executable running zig tests. Replace `Exercise.run_test` field with `kind`. Compile the exercise in both the exe and test cases, reducing code duplication. Add the `check_output` and `check_test` methods in ZiglingStep, in order to differentiate the code checking a normal executable and a test executable. Update the tests to correctly check both the exe and test cases. Remove the temporary code added in commit 832772c. --- build.zig | 115 +++++++++++++++++++++---------------------------- test/tests.zig | 28 +++++------- 2 files changed, 58 insertions(+), 85 deletions(-) diff --git a/build.zig b/build.zig index a13b016..46a260d 100644 --- a/build.zig +++ b/build.zig @@ -13,6 +13,13 @@ const assert = std.debug.assert; const join = std.fs.path.join; const print = std.debug.print; +const Kind = enum { + /// Run the artifact as a normal executable. + exe, + /// Run the artifact as a test. + @"test", +}; + pub const Exercise = struct { /// main_file must have the format key_name.zig. /// The key will be used as a shorthand to build just one example. @@ -34,9 +41,8 @@ pub const Exercise = struct { /// We need to keep track of this, so we compile with libc. link_libc: bool = false, - /// This exercise doesn't have a main function. - /// We only call the test. - run_test: bool = false, + /// This exercise kind. + kind: Kind = .exe, /// This exercise is not supported by the current Zig compiler. skip: bool = false, @@ -225,18 +231,6 @@ const ZiglingStep = struct { return; } - // Test exercise. - if (self.exercise.run_test) { - self.is_testing = true; - const result_msg = self.testing(prog_node) catch { - std.os.exit(2); - }; - const output = try trimLines(self.step.owner.allocator, result_msg); - print("\n{s}PASSED:\n{s}{s}\n\n", .{ green_text, output, reset_text }); - return; - } - - // Normal exercise. const exe_path = self.compile(prog_node) catch { if (self.exercise.hint) |hint| print("\n{s}Ziglings hint: {s}{s}", .{ bold_text, hint, reset_text }); @@ -276,10 +270,14 @@ const ZiglingStep = struct { return err; }; - const raw_output = if (self.exercise.check_stdout) - result.stdout - else - result.stderr; + switch (self.exercise.kind) { + .exe => return self.check_output(result), + .@"test" => return self.check_test(result), + } + } + + fn check_output(self: *ZiglingStep, result: Child.ExecResult) !void { + const b = self.step.owner; // Make sure it exited cleanly. switch (result.term) { @@ -299,6 +297,11 @@ const ZiglingStep = struct { }, } + const raw_output = if (self.exercise.check_stdout) + result.stdout + else + result.stderr; + // Validate the output. // NOTE: exercise.output can never contain a CR character. // See https://ziglang.org/documentation/master/#Source-Encoding. @@ -323,55 +326,28 @@ const ZiglingStep = struct { print("{s}PASSED:\n{s}{s}\n\n", .{ green_text, output, reset_text }); } - fn testing(self: *ZiglingStep, prog_node: *std.Progress.Node) ![]const u8 { - print("Testing {s}...\n", .{self.exercise.main_file}); - - const b = self.step.owner; - const exercise_path = self.exercise.main_file; - const path = join(b.allocator, &.{ self.work_path, exercise_path }) catch - @panic("OOM"); - - var zig_args = std.ArrayList([]const u8).init(b.allocator); - defer zig_args.deinit(); - - zig_args.append(b.zig_exe) catch @panic("OOM"); - zig_args.append("test") catch @panic("OOM"); - - zig_args.append(b.pathFromRoot(path)) catch @panic("OOM"); - - const argv = zig_args.items; - var code: u8 = undefined; - _ = self.eval(argv, &code, prog_node) catch |err| { - self.printErrors(); - - switch (err) { - error.FileNotFound => { - print("{s}{s}: Unable to spawn the following command: file not found{s}\n", .{ - red_text, self.exercise.main_file, reset_text, + fn check_test(self: *ZiglingStep, result: Child.ExecResult) !void { + switch (result.term) { + .Exited => |code| { + if (code != 0) { + // The test failed. + print("{s}{s}{s}\n", .{ + red_text, result.stderr, reset_text, }); - dumpArgs(argv); - }, - error.ExitCodeFailure => { - // Expected when test fails. - }, - error.ProcessTerminated => { - print("{s}{s}: The following command terminated unexpectedly:{s}\n", .{ - red_text, self.exercise.main_file, reset_text, - }); - dumpArgs(argv); - }, - else => { - print("{s}{s}: Unexpected error: {s}{s}\n", .{ - red_text, self.exercise.main_file, @errorName(err), reset_text, - }); - dumpArgs(argv); - }, - } - return err; - }; + return error.TestFailed; + } + }, + else => { + print("{s}{s} terminated unexpectedly{s}\n", .{ + red_text, self.exercise.main_file, reset_text, + }); - return self.result_messages; + return error.UnexpectedTermination; + }, + } + + print("{s}PASSED{s}\n\n", .{ green_text, reset_text }); } fn compile(self: *ZiglingStep, prog_node: *std.Progress.Node) ![]const u8 { @@ -386,7 +362,12 @@ const ZiglingStep = struct { defer zig_args.deinit(); zig_args.append(b.zig_exe) catch @panic("OOM"); - zig_args.append("build-exe") catch @panic("OOM"); + + const cmd = switch (self.exercise.kind) { + .exe => "build-exe", + .@"test" => "test", + }; + zig_args.append(cmd) catch @panic("OOM"); // Enable C support for exercises that use C functions. if (self.exercise.link_libc) { @@ -1220,7 +1201,7 @@ const exercises = [_]Exercise{ .{ .main_file = "102_testing.zig", .output = "", - .run_test = true, + .kind = .@"test", }, .{ .main_file = "999_the_end.zig", diff --git a/test/tests.zig b/test/tests.zig index b25b29c..45c075c 100644 --- a/test/tests.zig +++ b/test/tests.zig @@ -93,7 +93,7 @@ pub fn addCliTests(b: *std.Build, exercises: []const Exercise) *Step { const case_step = createCase(b, "case-3"); for (exercises[0 .. exercises.len - 1]) |ex| { - if (ex.skip or ex.run_test) continue; + if (ex.skip) continue; if (ex.hint) |hint| { const n = ex.number(); @@ -249,21 +249,6 @@ fn check_output(step: *Step, exercise: Exercise, reader: Reader) !void { return; } - if (exercise.run_test) { - { - const actual = try readLine(reader, &buf) orelse "EOF"; - const expect = b.fmt("Testing {s}...", .{exercise.main_file}); - try check(step, exercise, expect, actual); - } - - { - const actual = try readLine(reader, &buf) orelse "EOF"; - try check(step, exercise, "", actual); - } - - return; - } - { const actual = try readLine(reader, &buf) orelse "EOF"; const expect = b.fmt("Compiling {s}...", .{exercise.main_file}); @@ -278,12 +263,19 @@ fn check_output(step: *Step, exercise: Exercise, reader: Reader) !void { { const actual = try readLine(reader, &buf) orelse "EOF"; - const expect = "PASSED:"; + const expect = switch (exercise.kind) { + .exe => "PASSED:", + .@"test" => "PASSED", + }; try check(step, exercise, expect, actual); } // Skip the exercise output. - const nlines = 1 + mem.count(u8, exercise.output, "\n") + 1; + const nlines = switch (exercise.kind) { + .exe => 1 + mem.count(u8, exercise.output, "\n") + 1, + .@"test" => 1, + }; + var lineno: usize = 0; while (lineno < nlines) : (lineno += 1) { _ = try readLine(reader, &buf) orelse @panic("EOF");