build: simplify the named build mode

Remove the following steps, since they are rarely (if never) used and
only increase the complexity of the `build.zig` file:

 - install step (zig build -Dn=n install)
 - test step (zig build -Dn=n test)
 - start step (zig build -Dn=n start)

The only remaining step is the default zigling step (zig build -Dn=n),
where the user can choose the exercise to solve.

Update the tests.  Additionally, update CheckNamedStep and CheckStep so
that they can share the same functions.

Closes 
This commit is contained in:
Manlio Perillo 2023-05-13 13:58:12 +02:00
parent ede6671c27
commit dc2539ec4e
2 changed files with 94 additions and 214 deletions

View file

@ -66,6 +66,8 @@ pub const Exercise = struct {
}
/// Returns the CompileStep for this exercise.
///
/// TODO: currently this method is no longer used.
pub fn addExecutable(self: Exercise, b: *Build, work_path: []const u8) *CompileStep {
const path = join(b.allocator, &.{ work_path, self.main_file }) catch
@panic("OOM");
@ -139,57 +141,21 @@ pub fn build(b: *Build) !void {
// If the user pass a number for an exercise
if (exno) |n| {
// Named build mode: verifies a single exercise.
if (n == 0 or n > exercises.len - 1) {
print("unknown exercise number: {}\n", .{n});
std.os.exit(2);
}
const ex = exercises[n - 1];
const build_step = ex.addExecutable(b, work_path);
const skip_step = SkipStep.create(b, ex);
if (!ex.skip)
b.installArtifact(build_step)
else
b.getInstallStep().dependOn(&skip_step.step);
const run_step = b.addRunArtifact(build_step);
const test_step = b.step(
"test",
b.fmt("Run {s} without checking output", .{ex.main_file}),
);
if (ex.skip) {
test_step.dependOn(&skip_step.step);
} else {
test_step.dependOn(&run_step.step);
}
const verify_step = ZiglingStep.create(b, ex, work_path);
const zigling_step = b.step(
"zigling",
b.fmt("Check the solution of {s}", .{ex.main_file}),
);
zigling_step.dependOn(&verify_step.step);
b.default_step = zigling_step;
const start_step = b.step(
"start",
b.fmt("Check all solutions starting at {s}", .{ex.main_file}),
);
var prev_step = verify_step;
for (exercises) |exn| {
const nth = exn.number();
if (nth > n) {
const verify_stepn = ZiglingStep.create(b, exn, work_path);
verify_stepn.step.dependOn(&prev_step.step);
prev_step = verify_stepn;
}
}
start_step.dependOn(&prev_step.step);
const verify_step = ZiglingStep.create(b, ex, work_path);
zigling_step.dependOn(&verify_step.step);
return;
}

View file

@ -20,7 +20,7 @@ pub fn addCliTests(b: *std.Build, exercises: []const Exercise) *Step {
const step = b.step("test-cli", "Test the command line interface");
{
// Test that `zig build -Dhealed -Dn=n test` selects the nth exercise.
// Test that `zig build -Dhealed -Dn=n` selects the nth exercise.
const case_step = createCase(b, "case-1");
const tmp_path = makeTempPath(b) catch |err| {
@ -31,7 +31,6 @@ pub fn addCliTests(b: *std.Build, exercises: []const Exercise) *Step {
for (exercises[0 .. exercises.len - 1]) |ex| {
const n = ex.number();
if (ex.skip) continue;
const cmd = b.addSystemCommand(&.{
b.zig_exe,
@ -39,18 +38,13 @@ pub fn addCliTests(b: *std.Build, exercises: []const Exercise) *Step {
"-Dhealed",
b.fmt("-Dhealed-path={s}", .{tmp_path}),
b.fmt("-Dn={}", .{n}),
"test",
});
cmd.setName(b.fmt("zig build -Dhealed -Dn={} test", .{n}));
cmd.setName(b.fmt("zig build -Dhealed -Dn={}", .{n}));
cmd.expectExitCode(0);
cmd.step.dependOn(&heal_step.step);
const output = if (ex.check_stdout)
cmd.captureStdOut()
else
cmd.captureStdErr();
const verify = CheckNamedStep.create(b, ex, output);
const stderr = cmd.captureStdErr();
const verify = CheckNamedStep.create(b, ex, stderr);
verify.step.dependOn(&cmd.step);
case_step.dependOn(&verify.step);
@ -63,52 +57,13 @@ pub fn addCliTests(b: *std.Build, exercises: []const Exercise) *Step {
}
{
// Test that `zig build -Dhealed -Dn=n test` skips disabled esercises.
// Test that `zig build -Dhealed` processes all the exercises in order.
const case_step = createCase(b, "case-2");
const tmp_path = makeTempPath(b) catch |err| {
return fail(step, "unable to make tmp path: {s}\n", .{@errorName(err)});
};
const heal_step = HealStep.create(b, exercises, tmp_path);
for (exercises[0 .. exercises.len - 1]) |ex| {
const n = ex.number();
if (!ex.skip) continue;
const cmd = b.addSystemCommand(&.{
b.zig_exe,
"build",
"-Dhealed",
b.fmt("-Dhealed-path={s}", .{tmp_path}),
b.fmt("-Dn={}", .{n}),
"test",
});
const expect = b.fmt("{s} skipped", .{ex.main_file});
cmd.setName(b.fmt("zig build -Dhealed -Dn={} test", .{n}));
cmd.expectExitCode(0);
cmd.addCheck(.{ .expect_stdout_exact = "" });
cmd.addCheck(.{ .expect_stderr_match = expect });
cmd.step.dependOn(&heal_step.step);
case_step.dependOn(&cmd.step);
}
const cleanup = b.addRemoveDirTree(tmp_path);
cleanup.step.dependOn(case_step);
step.dependOn(&cleanup.step);
}
{
// Test that `zig build -Dhealed` process all the exercises in order.
const case_step = createCase(b, "case-3");
const tmp_path = makeTempPath(b) catch |err| {
return fail(step, "unable to make tmp path: {s}\n", .{@errorName(err)});
};
const heal_step = HealStep.create(b, exercises, tmp_path);
heal_step.step.dependOn(case_step);
@ -124,42 +79,7 @@ pub fn addCliTests(b: *std.Build, exercises: []const Exercise) *Step {
cmd.step.dependOn(&heal_step.step);
const stderr = cmd.captureStdErr();
const verify = CheckStep.create(b, exercises, stderr, true);
verify.step.dependOn(&cmd.step);
const cleanup = b.addRemoveDirTree(tmp_path);
cleanup.step.dependOn(&verify.step);
step.dependOn(&cleanup.step);
}
{
// Test that `zig build -Dhealed -Dn=1 start` process all the exercises
// in order.
const case_step = createCase(b, "case-4");
const tmp_path = makeTempPath(b) catch |err| {
return fail(step, "unable to make tmp path: {s}\n", .{@errorName(err)});
};
const heal_step = HealStep.create(b, exercises, tmp_path);
heal_step.step.dependOn(case_step);
// TODO: when an exercise is modified, the cache is not invalidated.
const cmd = b.addSystemCommand(&.{
b.zig_exe,
"build",
"-Dhealed",
b.fmt("-Dhealed-path={s}", .{tmp_path}),
"-Dn=1",
"start",
});
cmd.setName("zig build -Dhealed -Dn=1 start");
cmd.expectExitCode(0);
cmd.step.dependOn(&heal_step.step);
const stderr = cmd.captureStdErr();
const verify = CheckStep.create(b, exercises, stderr, false);
const verify = CheckStep.create(b, exercises, stderr);
verify.step.dependOn(&cmd.step);
const cleanup = b.addRemoveDirTree(tmp_path);
@ -170,7 +90,7 @@ pub fn addCliTests(b: *std.Build, exercises: []const Exercise) *Step {
{
// Test that `zig build -Dn=1` prints the hint.
const case_step = createCase(b, "case-5");
const case_step = createCase(b, "case-3");
const cmd = b.addSystemCommand(&.{ b.zig_exe, "build", "-Dn=1" });
const expect = exercises[0].hint orelse "";
@ -197,13 +117,13 @@ fn createCase(b: *Build, name: []const u8) *Step {
return case_step;
}
/// Checks the output of `zig build -Dn=n test`.
/// Checks the output of `zig build -Dn=n`.
const CheckNamedStep = struct {
step: Step,
exercise: Exercise,
output: FileSource,
stderr: FileSource,
pub fn create(owner: *Build, exercise: Exercise, output: FileSource) *CheckNamedStep {
pub fn create(owner: *Build, exercise: Exercise, stderr: FileSource) *CheckNamedStep {
const self = owner.allocator.create(CheckNamedStep) catch @panic("OOM");
self.* = .{
.step = Step.init(.{
@ -213,7 +133,7 @@ const CheckNamedStep = struct {
.makeFn = make,
}),
.exercise = exercise,
.output = output,
.stderr = stderr,
};
return self;
@ -222,34 +142,29 @@ const CheckNamedStep = struct {
fn make(step: *Step, _: *std.Progress.Node) !void {
const b = step.owner;
const self = @fieldParentPtr(CheckNamedStep, "step", step);
const ex = self.exercise;
// Allow up to 1 MB of output capture.
const max_bytes = 1 * 1024 * 1024;
const path = self.output.getPath(b);
const raw_output = try fs.cwd().readFileAlloc(b.allocator, path, max_bytes);
const stderr_file = try fs.cwd().openFile(
self.stderr.getPath(b),
.{ .mode = .read_only },
);
defer stderr_file.close();
const actual = try root.trimLines(b.allocator, raw_output);
const expect = self.exercise.output;
if (!mem.eql(u8, expect, actual)) {
return step.fail("{s}: expected to see \"{s}\", found \"{s}\"", .{
self.exercise.main_file, expect, actual,
});
}
const stderr = stderr_file.reader();
try check_output(step, ex, stderr);
}
};
/// Checks the output of `zig build` or `zig build -Dn=1 start`.
/// Checks the output of `zig build`.
const CheckStep = struct {
step: Step,
exercises: []const Exercise,
stderr: FileSource,
has_logo: bool,
pub fn create(
owner: *Build,
exercises: []const Exercise,
stderr: FileSource,
has_logo: bool,
) *CheckStep {
const self = owner.allocator.create(CheckStep) catch @panic("OOM");
self.* = .{
@ -261,7 +176,6 @@ const CheckStep = struct {
}),
.exercises = exercises,
.stderr = stderr,
.has_logo = has_logo,
};
return self;
@ -280,7 +194,7 @@ const CheckStep = struct {
const stderr = stderr_file.reader();
for (exercises) |ex| {
if (ex.number() == 1 and self.has_logo) {
if (ex.number() == 1) {
// Skip the logo.
const nlines = mem.count(u8, root.logo, "\n");
var buf: [80]u8 = undefined;
@ -293,8 +207,9 @@ const CheckStep = struct {
try check_output(step, ex, stderr);
}
}
};
fn check_output(step: *Step, exercise: Exercise, reader: Reader) !void {
fn check_output(step: *Step, exercise: Exercise, reader: Reader) !void {
const b = step.owner;
var buf: [1024]u8 = undefined;
@ -337,14 +252,14 @@ const CheckStep = struct {
while (lineno < nlines) : (lineno += 1) {
_ = try readLine(reader, &buf) orelse @panic("EOF");
}
}
}
fn check(
fn check(
step: *Step,
exercise: Exercise,
expect: []const u8,
actual: []const u8,
) !void {
) !void {
if (!mem.eql(u8, expect, actual)) {
return step.fail("{s}: expected to see \"{s}\", found \"{s}\"", .{
exercise.main_file,
@ -352,16 +267,15 @@ const CheckStep = struct {
actual,
});
}
}
}
fn readLine(reader: fs.File.Reader, buf: []u8) !?[]const u8 {
fn readLine(reader: fs.File.Reader, buf: []u8) !?[]const u8 {
if (try reader.readUntilDelimiterOrEof(buf, '\n')) |line| {
return mem.trimRight(u8, line, " \r\n");
}
return null;
}
};
}
/// Fails with a custom error message.
const FailStep = struct {