aboutsummaryrefslogtreecommitdiff
path: root/test/tests.zig
blob: 015b55a4684c4affb4df6dd83bdb85f25b939ece (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
const std = @import("std");
const root = @import("../build.zig");

const debug = std.debug;
const fmt = std.fmt;
const fs = std.fs;

const Allocator = std.mem.Allocator;
const Build = std.build;
const RunStep = std.Build.RunStep;
const Step = Build.Step;

const Exercise = root.Exercise;

pub fn addCliTests(b: *std.Build, exercises: []const Exercise) *Step {
    const step = b.step("test-cli", "Test the command line interface");

    // We should use a temporary path, but it will make the implementation of
    // `build.zig` more complex.
    const outdir = "patches/healed";

    fs.cwd().makePath(outdir) catch |err| {
        return fail(step, "unable to make '{s}': {s}\n", .{ outdir, @errorName(err) });
    };
    heal(b.allocator, exercises, outdir) catch |err| {
        return fail(step, "unable to heal exercises: {s}\n", .{@errorName(err)});
    };

    {
        // Test that `zig build -Dn=n -Dhealed test` selects the nth exercise.
        const case_step = createCase(b, "case-1");

        var i: usize = 0;
        for (exercises[0 .. exercises.len - 1]) |ex| {
            i += 1;
            if (ex.skip) continue;

            const cmd = b.addSystemCommand(
                &.{ b.zig_exe, "build", b.fmt("-Dn={}", .{i}), "-Dhealed", "test" },
            );
            cmd.setName(b.fmt("zig build -D={} -Dhealed test", .{i}));
            cmd.expectExitCode(0);

            // Some exercise output has an extra space character.
            if (ex.check_stdout)
                expectStdOutMatch(cmd, ex.output)
            else
                expectStdErrMatch(cmd, ex.output);

            case_step.dependOn(&cmd.step);
        }

        step.dependOn(case_step);
    }

    {
        // Test that `zig build -Dn=n -Dhealed test` skips disabled esercises.
        const case_step = createCase(b, "case-2");

        var i: usize = 0;
        for (exercises[0 .. exercises.len - 1]) |ex| {
            i += 1;
            if (!ex.skip) continue;

            const cmd = b.addSystemCommand(
                &.{ b.zig_exe, "build", b.fmt("-Dn={}", .{i}), "-Dhealed", "test" },
            );
            cmd.setName(b.fmt("zig build -D={} -Dhealed test", .{i}));
            cmd.expectExitCode(0);
            cmd.expectStdOutEqual("");
            expectStdErrMatch(cmd, b.fmt("{s} skipped", .{ex.main_file}));

            case_step.dependOn(&cmd.step);
        }

        step.dependOn(case_step);
    }

    // Don't add the cleanup step, since it may delete outdir while a test case
    // is running.
    //const cleanup = b.addRemoveDirTree(outdir);
    //step.dependOn(&cleanup.step);

    return step;
}

fn createCase(b: *Build, name: []const u8) *Step {
    const case_step = b.allocator.create(Step) catch @panic("OOM");
    case_step.* = Step.init(.{
        .id = .custom,
        .name = name,
        .owner = b,
    });

    return case_step;
}

// A step that will fail.
const FailStep = struct {
    step: Step,
    error_msg: []const u8,

    pub fn create(owner: *Build, error_msg: []const u8) *FailStep {
        const self = owner.allocator.create(FailStep) catch @panic("OOM");
        self.* = .{
            .step = Step.init(.{
                .id = .custom,
                .name = "fail",
                .owner = owner,
                .makeFn = make,
            }),
            .error_msg = error_msg,
        };

        return self;
    }

    fn make(step: *Step, _: *std.Progress.Node) !void {
        const b = step.owner;
        const self = @fieldParentPtr(FailStep, "step", step);

        try step.result_error_msgs.append(b.allocator, self.error_msg);
        return error.MakeFailed;
    }
};

// A variant of `std.Build.Step.fail` that does not return an error so that it
// can be used in the configuration phase.  It returns a FailStep, so that the
// error will be cleanly handled by the build runner.
fn fail(step: *Step, comptime format: []const u8, args: anytype) *Step {
    const b = step.owner;

    const fail_step = FailStep.create(b, b.fmt(format, args));
    step.dependOn(&fail_step.step);

    return step;
}

// Heals all the exercises.
fn heal(allocator: Allocator, exercises: []const Exercise, outdir: []const u8) !void {
    const join = fs.path.join;

    const exercises_path = "exercises";
    const patches_path = "patches/patches";

    for (exercises) |ex| {
        const name = ex.baseName();

        // Use the POSIX patch variant.
        const file = try join(allocator, &.{ exercises_path, ex.main_file });
        const patch = b: {
            const patch_name = try fmt.allocPrint(allocator, "{s}.patch", .{name});
            break :b try join(allocator, &.{ patches_path, patch_name });
        };
        const output = try join(allocator, &.{ outdir, ex.main_file });

        const argv = &.{ "patch", "-i", patch, "-o", output, file };

        var child = std.process.Child.init(argv, allocator);
        child.stdout_behavior = .Ignore; // the POSIX standard says that stdout is not used
        _ = try child.spawnAndWait();
    }
}

//
// Missing functions from std.Build.RunStep
//

/// Adds a check for stderr match. Does not add any other checks.
pub fn expectStdErrMatch(self: *RunStep, bytes: []const u8) void {
    const new_check: RunStep.StdIo.Check = .{
        .expect_stderr_match = self.step.owner.dupe(bytes),
    };
    self.addCheck(new_check);
}

/// Adds a check for stdout match as well as a check for exit code 0, if
/// there is not already an expected termination check.
pub fn expectStdOutMatch(self: *RunStep, bytes: []const u8) void {
    const new_check: RunStep.StdIo.Check = .{
        .expect_stdout_match = self.step.owner.dupe(bytes),
    };
    self.addCheck(new_check);
    if (!self.hasTermCheck()) {
        self.expectExitCode(0);
    }
}