Skip to content

Commit 02f50f2

Browse files
Merge pull request #3 from allyourcodebase/tests
Replace Zig tests with upstream testing framework
2 parents fabb236 + b8a2c66 commit 02f50f2

File tree

4 files changed

+745
-139
lines changed

4 files changed

+745
-139
lines changed

ClarTestStep.zig

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
//! Runs a Clar test and lightly 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: 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: std.BoundedArray(u8, 1024) = .{};
81+
const w = buf.writer();
82+
83+
var parser: TapParser = .default;
84+
var node: ?std.Progress.Node = null;
85+
defer if (node) |n| n.end();
86+
87+
while (true) {
88+
r.streamUntilDelimiter(w, '\n', null) catch |err| switch (err) {
89+
error.EndOfStream => if (try poller.poll()) continue else break,
90+
else => return err,
91+
};
92+
93+
const line = buf.constSlice();
94+
defer buf.resize(0) catch unreachable;
95+
96+
switch (try parser.parseLine(arena, line)) {
97+
.start_suite => |suite| {
98+
if (node) |n| n.end();
99+
node = options.progress_node.start(suite, 0);
100+
},
101+
.ok => {
102+
if (node) |n| n.completeOne();
103+
},
104+
.failure => |fail| {
105+
// @Cleanup print failures in a nicer way. Avoid redundant "error:" prefixes on newlines with minimal allocations.
106+
try step.result_error_msgs.append(arena, fail.description.items);
107+
try step.result_error_msgs.appendSlice(arena, fail.reasons.items);
108+
try step.result_error_msgs.append(arena, "\n");
109+
if (node) |n| n.completeOne();
110+
parser.reset();
111+
},
112+
.feed_line => {},
113+
}
114+
}
115+
116+
const term = try child.wait();
117+
try step.handleChildProcessTerm(term, null, argv_list.items);
118+
}
119+
120+
try step.writeManifestAndWatch(&man);
121+
}
122+
123+
const TapParser = struct {
124+
state: State,
125+
wip_failure: Result.Failure,
126+
127+
const Result = union(enum) {
128+
start_suite: []const u8,
129+
ok,
130+
failure: Failure,
131+
feed_line,
132+
133+
const Failure = struct {
134+
description: std.ArrayListUnmanaged(u8),
135+
reasons: std.ArrayListUnmanaged([]const u8),
136+
};
137+
};
138+
139+
const keyword = struct {
140+
const suite_start = "# start of suite ";
141+
const ok = "ok ";
142+
const not_ok = "not ok ";
143+
const spacer1 = " - ";
144+
const spacer2 = ": ";
145+
const yaml_blk = " ---";
146+
const pre_reason = "reason: |";
147+
const at = "at:";
148+
const file = "file: ";
149+
const line = "line: ";
150+
};
151+
152+
const State = enum {
153+
start,
154+
desc,
155+
yaml_start,
156+
pre_reason,
157+
reason,
158+
file,
159+
line,
160+
};
161+
162+
fn parseLine(p: *TapParser, step_arena: Allocator, line: []const u8) Allocator.Error!Result {
163+
loop: switch (p.state) {
164+
.start => {
165+
if (mem.startsWith(u8, line, keyword.suite_start)) {
166+
const suite_start = skip(line, keyword.spacer2, keyword.suite_start.len) orelse @panic("expected suite number");
167+
return .{ .start_suite = line[suite_start..] };
168+
} else if (mem.startsWith(u8, line, keyword.ok)) {
169+
return .ok;
170+
} else if (mem.startsWith(u8, line, keyword.not_ok)) {
171+
p.state = .desc;
172+
continue :loop p.state;
173+
}
174+
},
175+
176+
// Failure parsing
177+
.desc => {
178+
const name_start = skip(line, keyword.spacer1, keyword.not_ok.len) orelse @panic("expected spacer");
179+
const name = mem.trim(u8, line[name_start..], &std.ascii.whitespace);
180+
try p.wip_failure.description.appendSlice(step_arena, name);
181+
try p.wip_failure.description.appendSlice(step_arena, ": ");
182+
p.state = .yaml_start;
183+
},
184+
.yaml_start => {
185+
_ = mem.indexOf(u8, line, keyword.yaml_blk) orelse @panic("expected yaml_blk");
186+
p.state = .pre_reason;
187+
},
188+
.pre_reason => {
189+
_ = mem.indexOf(u8, line, keyword.pre_reason) orelse @panic("expected pre_reason");
190+
p.state = .reason;
191+
},
192+
.reason => {
193+
if (mem.indexOf(u8, line, keyword.at) != null) {
194+
p.state = .file;
195+
} else {
196+
const ln = mem.trim(u8, line, &std.ascii.whitespace);
197+
try p.wip_failure.reasons.append(step_arena, try step_arena.dupe(u8, ln));
198+
}
199+
},
200+
.file => {
201+
const file_start = skip(line, keyword.file, 0) orelse @panic("expected file");
202+
const file = mem.trim(u8, line[file_start..], std.ascii.whitespace ++ "'");
203+
try p.wip_failure.description.appendSlice(step_arena, file);
204+
try p.wip_failure.description.append(step_arena, ':');
205+
p.state = .line;
206+
},
207+
.line => {
208+
const line_start = skip(line, keyword.line, 0) orelse @panic("expected line");
209+
const fail_line = mem.trim(u8, line[line_start..], &std.ascii.whitespace);
210+
try p.wip_failure.description.appendSlice(step_arena, fail_line);
211+
p.state = .start;
212+
return .{ .failure = p.wip_failure };
213+
},
214+
}
215+
216+
return .feed_line;
217+
}
218+
219+
fn skip(line: []const u8, to_skip: []const u8, start: usize) ?usize {
220+
const index = mem.indexOfPos(u8, line, start, to_skip) orelse return null;
221+
return to_skip.len + index;
222+
}
223+
224+
const default: TapParser = .{
225+
.state = .start,
226+
.wip_failure = .{
227+
.description = .empty,
228+
.reasons = .empty,
229+
},
230+
};
231+
232+
fn reset(p: *TapParser) void {
233+
p.* = default;
234+
}
235+
};
236+
237+
const std = @import("std");
238+
const mem = std.mem;
239+
const Step = std.Build.Step;
240+
const Allocator = mem.Allocator;

0 commit comments

Comments
 (0)