Skip to content

Commit 31ddee9

Browse files
committed
improve TAP parser output
Avoids unecessary allocations and returns more information for progress output. Instead of copying strings from the TAP stream only to format them later, we simply write the formatted strings to parser.wip_failure as we parse them, avoiding unecessary allocations. This works because we're using the same allocator (step.owner.allocator) to parse and append errors.
1 parent e6a7d74 commit 31ddee9

File tree

1 file changed

+86
-57
lines changed

1 file changed

+86
-57
lines changed

ClarTestStep.zig

Lines changed: 86 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//! Runs a Clar test and parses it's [TAP](https://testanything.org/) stream,
1+
//! Runs a Clar test and lightly parses it's [TAP](https://testanything.org/) stream,
22
//! reporting progress/errors to the build system.
33
// Based on Step.Run
44

@@ -77,27 +77,39 @@ fn make(step: *Step, options: std.Build.Step.MakeOptions) !void {
7777
const fifo = poller.fifo(.stdout);
7878
const r = fifo.reader();
7979

80-
var buf: [1024]u8 = undefined;
81-
var fbs = std.io.fixedBufferStream(&buf);
82-
const w = fbs.writer();
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();
8386

84-
var parser: FailureParser = .default;
8587
while (true) {
8688
r.streamUntilDelimiter(w, '\n', null) catch |err| switch (err) {
8789
error.EndOfStream => if (try poller.poll()) continue else break,
8890
else => return err,
8991
};
9092

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);
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 => {},
101113
}
102114
}
103115

@@ -108,24 +120,34 @@ fn make(step: *Step, options: std.Build.Step.MakeOptions) !void {
108120
try step.writeManifestAndWatch(&man);
109121
}
110122

111-
const FailureParser = struct {
123+
const TapParser = struct {
112124
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,
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+
};
120137
};
121138

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: ";
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+
};
129151

130152
const State = enum {
131153
start,
@@ -137,68 +159,75 @@ const FailureParser = struct {
137159
line,
138160
};
139161

140-
fn parseLine(p: *FailureParser, allocator: Allocator, line: []const u8) Allocator.Error!?Failure {
162+
fn parseLine(p: *TapParser, step_arena: Allocator, line: []const u8) Allocator.Error!Result {
141163
loop: switch (p.state) {
142164
.start => {
143-
if (std.mem.startsWith(u8, line, not_ok)) {
144-
@branchHint(.unlikely);
165+
if (std.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 (std.mem.startsWith(u8, line, keyword.ok)) {
169+
return .ok;
170+
} else if (std.mem.startsWith(u8, line, keyword.not_ok)) {
145171
p.state = .desc;
146172
continue :loop p.state;
147173
}
148174
},
175+
176+
// Failure parsing
149177
.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..]);
178+
const name_start = skip(line, keyword.spacer1, keyword.not_ok.len) orelse @panic("expected spacer");
179+
try p.wip_failure.description.appendSlice(step_arena, line[name_start..]);
180+
try p.wip_failure.description.appendSlice(step_arena, ": ");
152181
p.state = .yaml_start;
153182
},
154183
.yaml_start => {
155-
_ = std.mem.indexOf(u8, line, yaml_blk) orelse @panic("expected yaml_blk");
184+
_ = std.mem.indexOf(u8, line, keyword.yaml_blk) orelse @panic("expected yaml_blk");
156185
p.state = .pre_reason;
157186
},
158187
.pre_reason => {
159-
_ = std.mem.indexOf(u8, line, pre_reason) orelse @panic("expected pre_reason");
188+
_ = std.mem.indexOf(u8, line, keyword.pre_reason) orelse @panic("expected pre_reason");
160189
p.state = .reason;
161190
},
162191
.reason => {
163-
if (std.mem.indexOf(u8, line, at) != null) {
192+
if (std.mem.indexOf(u8, line, keyword.at) != null) {
164193
p.state = .file;
165194
} else {
166195
const ln = std.mem.trim(u8, line, &std.ascii.whitespace);
167-
try p.fail.reasons.append(allocator, try allocator.dupe(u8, ln));
196+
try p.wip_failure.reasons.append(step_arena, try step_arena.dupe(u8, ln));
168197
}
169198
},
170199
.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..], &.{'\''}));
200+
const file_start = skip(line, keyword.file, 0) orelse @panic("expected file");
201+
const file = std.mem.trim(u8, line[file_start..], &.{'\''});
202+
try p.wip_failure.description.appendSlice(step_arena, file);
203+
try p.wip_failure.description.append(step_arena, ':');
173204
p.state = .line;
174205
},
175206
.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..]);
207+
const line_start = skip(line, keyword.line, 0) orelse @panic("expected line");
208+
try p.wip_failure.description.appendSlice(step_arena, line[line_start..]);
178209
p.state = .start;
179-
return p.fail;
210+
return .{ .failure = p.wip_failure };
180211
},
181212
}
182213

183-
return null;
214+
return .feed_line;
215+
}
216+
217+
fn skip(line: []const u8, to_skip: []const u8, start: usize) ?usize {
218+
const index = std.mem.indexOfPos(u8, line, start, to_skip) orelse return null;
219+
return to_skip.len + index;
184220
}
185221

186-
const default: FailureParser = .{
222+
const default: TapParser = .{
187223
.state = .start,
188-
.fail = .{
189-
.desc = undefined,
190-
.reasons = .{},
191-
.file = undefined,
192-
.line = undefined,
224+
.wip_failure = .{
225+
.description = .empty,
226+
.reasons = .empty,
193227
},
194228
};
195229

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);
230+
fn reset(p: *TapParser) void {
202231
p.* = default;
203232
}
204233
};

0 commit comments

Comments
 (0)