Skip to content

Commit e6a7d74

Browse files
committed
replace test Run step with custom step
(Lightly) parses [TAP](https://testanything.org/) output from the Clar test runner, reporting errors to the build system. The parser could be more robust.
1 parent d64dfc0 commit e6a7d74

File tree

2 files changed

+216
-21
lines changed

2 files changed

+216
-21
lines changed

ClarTestStep.zig

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
//! Runs a Clar test and parses it's [TAP](https://testanything.org/) stream,
2+
//! reporting progress/errors to the build system.
3+
// Based on Step.Run
4+
5+
step: Step,
6+
runner: *Step.Compile,
7+
args: std.ArrayListUnmanaged([]const u8),
8+
9+
const ClarTestStep = @This();
10+
11+
pub fn create(owner: *std.Build, name: []const u8, runner: *Step.Compile) *ClarTestStep {
12+
const clar = owner.allocator.create(ClarTestStep) catch @panic("OOM");
13+
clar.* = .{
14+
.step = Step.init(.{
15+
.id = .custom,
16+
.name = name,
17+
.owner = owner,
18+
.makeFn = make,
19+
}),
20+
.runner = runner,
21+
.args = .{},
22+
};
23+
runner.getEmittedBin().addStepDependencies(&clar.step);
24+
return clar;
25+
}
26+
27+
pub fn addArg(clar: *ClarTestStep, arg: []const u8) void {
28+
const b = clar.step.owner;
29+
clar.args.append(b.allocator, b.dupe(arg)) catch @panic("OOM");
30+
}
31+
32+
pub fn addArgs(clar: *ClarTestStep, args: []const []const u8) void {
33+
for (args) |arg| clar.addArg(arg);
34+
}
35+
36+
fn make(step: *Step, options: std.Build.Step.MakeOptions) !void {
37+
const clar: *ClarTestStep = @fieldParentPtr("step", step);
38+
const b = step.owner;
39+
const arena = b.allocator;
40+
41+
var man = b.graph.cache.obtain();
42+
defer man.deinit();
43+
44+
var argv_list = std.ArrayList([]const u8).init(arena);
45+
{
46+
const file_path = clar.runner.installed_path orelse clar.runner.generated_bin.?.path.?;
47+
try argv_list.append(file_path);
48+
_ = try man.addFile(file_path, null);
49+
}
50+
try argv_list.append("-t"); // force TAP output
51+
for (clar.args.items) |arg| {
52+
try argv_list.append(arg);
53+
man.hash.addBytes(arg);
54+
}
55+
56+
if (try step.cacheHitAndWatch(&man)) {
57+
// cache hit, skip running command
58+
step.result_cached = true;
59+
return;
60+
}
61+
62+
{
63+
var child = std.process.Child.init(argv_list.items, arena);
64+
child.stdin_behavior = .Ignore;
65+
child.stdout_behavior = .Pipe;
66+
child.stderr_behavior = .Ignore;
67+
68+
try child.spawn();
69+
70+
var poller = std.io.poll(
71+
b.allocator,
72+
enum { stdout },
73+
.{ .stdout = child.stdout.? },
74+
);
75+
defer poller.deinit();
76+
77+
const fifo = poller.fifo(.stdout);
78+
const r = fifo.reader();
79+
80+
var buf: [1024]u8 = undefined;
81+
var fbs = std.io.fixedBufferStream(&buf);
82+
const w = fbs.writer();
83+
84+
var parser: FailureParser = .default;
85+
while (true) {
86+
r.streamUntilDelimiter(w, '\n', null) catch |err| switch (err) {
87+
error.EndOfStream => if (try poller.poll()) continue else break,
88+
else => return err,
89+
};
90+
91+
const line = fbs.getWritten();
92+
defer fbs.reset();
93+
94+
options.progress_node.completeOne();
95+
96+
if (try parser.parseLine(arena, line)) |err| {
97+
try step.addError("{s}: {s}:{s}", .{ err.desc, err.file, err.line });
98+
try step.result_error_msgs.appendSlice(arena, b.dupeStrings(err.reasons.items));
99+
try step.result_error_msgs.append(arena, "\n");
100+
parser.reset(arena);
101+
}
102+
}
103+
104+
const term = try child.wait();
105+
try step.handleChildProcessTerm(term, null, argv_list.items);
106+
}
107+
108+
try step.writeManifestAndWatch(&man);
109+
}
110+
111+
const FailureParser = struct {
112+
state: State,
113+
fail: Failure,
114+
115+
const Failure = struct {
116+
desc: []const u8,
117+
reasons: std.ArrayListUnmanaged([]const u8),
118+
file: []const u8,
119+
line: []const u8,
120+
};
121+
122+
const not_ok = "not ok ";
123+
const spacer = " - ";
124+
const yaml_blk = " ---";
125+
const pre_reason = "reason: |";
126+
const at = "at:";
127+
const file = "file: ";
128+
const _line = "line: ";
129+
130+
const State = enum {
131+
start,
132+
desc,
133+
yaml_start,
134+
pre_reason,
135+
reason,
136+
file,
137+
line,
138+
};
139+
140+
fn parseLine(p: *FailureParser, allocator: Allocator, line: []const u8) Allocator.Error!?Failure {
141+
loop: switch (p.state) {
142+
.start => {
143+
if (std.mem.startsWith(u8, line, not_ok)) {
144+
@branchHint(.unlikely);
145+
p.state = .desc;
146+
continue :loop p.state;
147+
}
148+
},
149+
.desc => {
150+
const name_start = spacer.len + (std.mem.indexOfPos(u8, line, not_ok.len, spacer) orelse @panic("expected spacer"));
151+
p.fail.desc = try allocator.dupe(u8, line[name_start..]);
152+
p.state = .yaml_start;
153+
},
154+
.yaml_start => {
155+
_ = std.mem.indexOf(u8, line, yaml_blk) orelse @panic("expected yaml_blk");
156+
p.state = .pre_reason;
157+
},
158+
.pre_reason => {
159+
_ = std.mem.indexOf(u8, line, pre_reason) orelse @panic("expected pre_reason");
160+
p.state = .reason;
161+
},
162+
.reason => {
163+
if (std.mem.indexOf(u8, line, at) != null) {
164+
p.state = .file;
165+
} else {
166+
const ln = std.mem.trim(u8, line, &std.ascii.whitespace);
167+
try p.fail.reasons.append(allocator, try allocator.dupe(u8, ln));
168+
}
169+
},
170+
.file => {
171+
const file_start = file.len + (std.mem.indexOf(u8, line, file) orelse @panic("expected file"));
172+
p.fail.file = try allocator.dupe(u8, std.mem.trim(u8, line[file_start..], &.{'\''}));
173+
p.state = .line;
174+
},
175+
.line => {
176+
const line_start = _line.len + (std.mem.indexOf(u8, line, _line) orelse @panic("expected line"));
177+
p.fail.line = try allocator.dupe(u8, line[line_start..]);
178+
p.state = .start;
179+
return p.fail;
180+
},
181+
}
182+
183+
return null;
184+
}
185+
186+
const default: FailureParser = .{
187+
.state = .start,
188+
.fail = .{
189+
.desc = undefined,
190+
.reasons = .{},
191+
.file = undefined,
192+
.line = undefined,
193+
},
194+
};
195+
196+
fn reset(p: *FailureParser, allocator: Allocator) void {
197+
for (p.fail.reasons.items) |reason| allocator.free(reason);
198+
p.fail.reasons.deinit(allocator);
199+
allocator.free(p.fail.desc);
200+
allocator.free(p.fail.file);
201+
allocator.free(p.fail.line);
202+
p.* = default;
203+
}
204+
};
205+
206+
const std = @import("std");
207+
const Step = std.Build.Step;
208+
const Allocator = std.mem.Allocator;

build.zig

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -438,13 +438,16 @@ pub fn build(b: *std.Build) !void {
438438
top_level_step: *std.Build.Step,
439439
runner: *std.Build.Step.Compile,
440440

441+
const ClarStep = @import("ClarTestStep.zig");
442+
441443
fn addTest(
442444
self: @This(),
443445
name: []const u8,
444446
args: []const []const u8,
445447
) void {
446-
const run = self.addTestInner(name);
447-
run.addArgs(args);
448+
const clar = ClarStep.create(self.b, name, self.runner);
449+
self.top_level_step.dependOn(&clar.step);
450+
clar.addArgs(args);
448451
}
449452

450453
fn addTestFiltered(
@@ -453,29 +456,13 @@ pub fn build(b: *std.Build) !void {
453456
/// Comma seperated list of tests
454457
tests: []const u8,
455458
) void {
456-
const run = self.addTestInner(name);
459+
const clar = ClarStep.create(self.b, name, self.runner);
460+
self.top_level_step.dependOn(&clar.step);
457461
var iter = std.mem.tokenizeScalar(u8, tests, ',');
458462
while (iter.next()) |filter| {
459-
run.addArg(self.b.fmt("-s{s}", .{filter}));
463+
clar.addArg(self.b.fmt("-s{s}", .{filter}));
460464
}
461465
}
462-
463-
fn addTestInner(self: @This(), name: []const u8) *std.Build.Step.Run {
464-
const run = self.b.addRunArtifact(self.runner);
465-
run.setName(name);
466-
run.addArg("-q"); // only report tests that had an error
467-
// @Todo parse TAP output from Clar test runner and report errors that way.
468-
// @Cleanup this is a very brittle way of reporting errors from the test runner
469-
run.expectStdOutEqual(
470-
\\Loaded 384 suites:
471-
\\Started (test status codes: OK='.' FAILURE='F' SKIPPED='S')
472-
\\
473-
\\
474-
\\
475-
);
476-
self.top_level_step.dependOn(&run.step);
477-
return run;
478-
}
479466
};
480467

481468
const helper: TestHelper = .{

0 commit comments

Comments
 (0)