zeno-mobile-runner 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/FEATURES.md +1 -1
- package/README.md +9 -1
- package/build.zig.zon +2 -2
- package/clients/kotlin/README.md +1 -1
- package/clients/kotlin/build.gradle.kts +1 -1
- package/clients/python/pyproject.toml +1 -1
- package/clients/rust/Cargo.lock +1 -1
- package/clients/rust/Cargo.toml +1 -1
- package/clients/typescript/package.json +1 -1
- package/docs/protocol-fixtures/core-session.responses.jsonl +1 -1
- package/docs/protocol.md +10 -10
- package/examples/ios-dev-client-open-link.json +24 -13
- package/examples/ios-dev-client-route-snapshot.json +33 -8
- package/npm/scenarios.mjs +15 -8
- package/npm/wizard.mjs +1 -1
- package/package.json +3 -1
- package/prebuilds/darwin-arm64/zmr +0 -0
- package/prebuilds/darwin-x64/zmr +0 -0
- package/prebuilds/linux-arm64/zmr +0 -0
- package/prebuilds/linux-x64/zmr +0 -0
- package/scripts/create-react-native-expo-demo-app.sh +11 -13
- package/shims/ios/ZMRShim.swift +40 -12
- package/shims/ios/ZMRShimUITestCase.swift +142 -16
- package/src/android.zig +10 -9
- package/src/android_emulator.zig +22 -11
- package/src/android_screen_recording.zig +11 -7
- package/src/bundle.zig +10 -9
- package/src/bundle_redaction.zig +29 -28
- package/src/bundle_tar.zig +15 -12
- package/src/cli_devices.zig +7 -3
- package/src/cli_discover.zig +7 -3
- package/src/cli_doctor.zig +7 -3
- package/src/cli_draft.zig +51 -47
- package/src/cli_explore.zig +7 -3
- package/src/cli_import.zig +8 -4
- package/src/cli_info.zig +13 -6
- package/src/cli_init.zig +9 -5
- package/src/cli_inspect.zig +8 -4
- package/src/cli_run.zig +22 -16
- package/src/cli_serve.zig +3 -3
- package/src/cli_trace.zig +25 -12
- package/src/cli_validate.zig +8 -4
- package/src/command.zig +81 -99
- package/src/config.zig +2 -1
- package/src/config_diagnostics.zig +2 -1
- package/src/config_paths.zig +2 -1
- package/src/doctor.zig +8 -7
- package/src/doctor_hints.zig +1 -1
- package/src/errors.zig +5 -5
- package/src/importer.zig +8 -7
- package/src/ios.zig +26 -29
- package/src/ios_devices.zig +6 -5
- package/src/ios_lifecycle.zig +4 -4
- package/src/json_rpc.zig +39 -40
- package/src/json_rpc_methods.zig +8 -8
- package/src/json_rpc_observation.zig +9 -8
- package/src/json_rpc_params.zig +1 -1
- package/src/json_rpc_trace.zig +22 -21
- package/src/main.zig +22 -10
- package/src/mcp.zig +28 -19
- package/src/mcp_trace.zig +30 -29
- package/src/report.zig +39 -36
- package/src/report_html.zig +5 -4
- package/src/runner.zig +2 -1
- package/src/runner_actions.zig +20 -17
- package/src/runner_diagnostics.zig +4 -4
- package/src/runner_events.zig +55 -51
- package/src/runner_native.zig +21 -19
- package/src/runner_waits.zig +46 -41
- package/src/scaffold.zig +25 -24
- package/src/scenario.zig +4 -3
- package/src/stdio.zig +129 -0
- package/src/trace.zig +34 -26
- package/src/trace_summary.zig +3 -2
- package/src/trace_summary_diagnostic.zig +15 -13
- package/src/validation.zig +5 -4
- package/src/version.zig +1 -1
package/src/stdio.zig
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
|
|
3
|
+
const default_buffer_size = 8192;
|
|
4
|
+
var process_environ: ?std.process.Environ = null;
|
|
5
|
+
var process_threaded: ?std.Io.Threaded = null;
|
|
6
|
+
|
|
7
|
+
fn processIo() std.Io {
|
|
8
|
+
if (process_threaded) |*threaded| return threaded.io();
|
|
9
|
+
return std.Io.Threaded.global_single_threaded.io();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
pub fn initProcess(init: std.process.Init.Minimal, allocator: std.mem.Allocator) void {
|
|
13
|
+
process_environ = init.environ;
|
|
14
|
+
process_threaded = .init(allocator, .{
|
|
15
|
+
.argv0 = .init(init.args),
|
|
16
|
+
.environ = init.environ,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
pub fn deinitProcess() void {
|
|
21
|
+
if (process_threaded) |*threaded| {
|
|
22
|
+
threaded.deinit();
|
|
23
|
+
process_threaded = null;
|
|
24
|
+
}
|
|
25
|
+
process_environ = null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
pub fn io() std.Io {
|
|
29
|
+
return processIo();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
pub fn sleepNs(nanoseconds: u64) void {
|
|
33
|
+
std.Io.sleep(
|
|
34
|
+
processIo(),
|
|
35
|
+
std.Io.Duration.fromNanoseconds(@intCast(nanoseconds)),
|
|
36
|
+
.awake,
|
|
37
|
+
) catch {};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
pub fn nowNs() i96 {
|
|
41
|
+
return std.Io.Clock.real.now(processIo()).nanoseconds;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
pub fn nowMs() i64 {
|
|
45
|
+
return @intCast(@divTrunc(nowNs(), std.time.ns_per_ms));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
pub fn getenv(name: []const u8) ?[]const u8 {
|
|
49
|
+
const environ = process_environ orelse return null;
|
|
50
|
+
const block = environ.block;
|
|
51
|
+
const Block = @TypeOf(block);
|
|
52
|
+
if (Block != std.process.Environ.PosixBlock) return null;
|
|
53
|
+
|
|
54
|
+
for (block.view().slice) |entry_ptr| {
|
|
55
|
+
const entry = std.mem.span(entry_ptr);
|
|
56
|
+
if (entry.len <= name.len or entry[name.len] != '=') continue;
|
|
57
|
+
if (std.mem.eql(u8, entry[0..name.len], name)) return entry[name.len + 1 ..];
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
pub fn access(path: []const u8) !void {
|
|
63
|
+
return accessWithOptions(path, .{});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
pub fn accessWithOptions(path: []const u8, options: std.Io.Dir.AccessOptions) !void {
|
|
67
|
+
return std.Io.Dir.cwd().access(processIo(), path, options);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
pub fn readFileAlloc(allocator: std.mem.Allocator, path: []const u8, limit: usize) ![]u8 {
|
|
71
|
+
return std.Io.Dir.cwd().readFileAlloc(processIo(), path, allocator, .limited(limit));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
pub const Output = struct {
|
|
75
|
+
buffer: [default_buffer_size]u8 = undefined,
|
|
76
|
+
file_writer: std.Io.File.Writer = undefined,
|
|
77
|
+
initialized: bool = false,
|
|
78
|
+
|
|
79
|
+
pub fn init(self: *Output, file: std.Io.File) void {
|
|
80
|
+
self.file_writer = file.writerStreaming(processIo(), &self.buffer);
|
|
81
|
+
self.initialized = true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
pub fn writer(self: *Output) *std.Io.Writer {
|
|
85
|
+
return &self.file_writer.interface;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
pub fn flush(self: *Output) !void {
|
|
89
|
+
if (self.initialized) try self.file_writer.interface.flush();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
pub fn deinit(self: *Output) void {
|
|
93
|
+
self.flush() catch {};
|
|
94
|
+
self.initialized = false;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
pub const Input = struct {
|
|
99
|
+
buffer: [default_buffer_size]u8 = undefined,
|
|
100
|
+
file_reader: std.Io.File.Reader = undefined,
|
|
101
|
+
|
|
102
|
+
pub fn init(self: *Input, file: std.Io.File) void {
|
|
103
|
+
self.file_reader = file.readerStreaming(processIo(), &self.buffer);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
pub fn reader(self: *Input) *std.Io.Reader {
|
|
107
|
+
return &self.file_reader.interface;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
pub fn readLineAlloc(reader: *std.Io.Reader, allocator: std.mem.Allocator, max_bytes: usize) !?[]u8 {
|
|
112
|
+
var out: std.Io.Writer.Allocating = .init(allocator);
|
|
113
|
+
errdefer out.deinit();
|
|
114
|
+
|
|
115
|
+
_ = try reader.streamDelimiterLimit(&out.writer, '\n', .limited(max_bytes));
|
|
116
|
+
|
|
117
|
+
const next = reader.peek(1) catch |err| switch (err) {
|
|
118
|
+
error.EndOfStream => {
|
|
119
|
+
if (out.writer.end == 0) {
|
|
120
|
+
out.deinit();
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
return try out.toOwnedSlice();
|
|
124
|
+
},
|
|
125
|
+
else => |actual| return actual,
|
|
126
|
+
};
|
|
127
|
+
if (next.len > 0 and next[0] == '\n') reader.toss(1);
|
|
128
|
+
return try out.toOwnedSlice();
|
|
129
|
+
}
|
package/src/trace.zig
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const std = @import("std");
|
|
2
|
+
const stdio = @import("stdio.zig");
|
|
2
3
|
const selector = @import("selector.zig");
|
|
3
4
|
const trace_json = @import("trace_json.zig");
|
|
4
5
|
const types = @import("types.zig");
|
|
@@ -28,11 +29,11 @@ pub const TraceWriter = struct {
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
pub fn initWithOptions(allocator: std.mem.Allocator, root_dir: []const u8, capture: CaptureOptions) !TraceWriter {
|
|
31
|
-
try std.
|
|
32
|
+
try std.Io.Dir.cwd().createDirPath(stdio.io(), root_dir);
|
|
32
33
|
try resetTraceDirectory(allocator, root_dir);
|
|
33
34
|
const artifacts_path = try std.fs.path.join(allocator, &.{ root_dir, "artifacts" });
|
|
34
35
|
defer allocator.free(artifacts_path);
|
|
35
|
-
try std.
|
|
36
|
+
try std.Io.Dir.cwd().createDirPath(stdio.io(), artifacts_path);
|
|
36
37
|
return .{
|
|
37
38
|
.allocator = allocator,
|
|
38
39
|
.root_dir = try allocator.dupe(u8, root_dir),
|
|
@@ -51,7 +52,7 @@ pub const TraceWriter = struct {
|
|
|
51
52
|
.scenario_name = try self.allocator.dupe(u8, scenario_name),
|
|
52
53
|
.app_id = try dupeOptional(self.allocator, app_id),
|
|
53
54
|
.status = try self.allocator.dupe(u8, "running"),
|
|
54
|
-
.started_at_ms =
|
|
55
|
+
.started_at_ms = stdio.nowMs(),
|
|
55
56
|
};
|
|
56
57
|
try self.writeManifest();
|
|
57
58
|
}
|
|
@@ -68,7 +69,7 @@ pub const TraceWriter = struct {
|
|
|
68
69
|
var manifest = &self.manifest.?;
|
|
69
70
|
self.allocator.free(manifest.status);
|
|
70
71
|
manifest.status = try self.allocator.dupe(u8, options.status);
|
|
71
|
-
manifest.ended_at_ms =
|
|
72
|
+
manifest.ended_at_ms = stdio.nowMs();
|
|
72
73
|
manifest.failed_step_index = options.failed_step_index;
|
|
73
74
|
if (manifest.error_name) |value| self.allocator.free(value);
|
|
74
75
|
manifest.error_name = try dupeOptional(self.allocator, options.error_name);
|
|
@@ -99,9 +100,9 @@ pub const TraceWriter = struct {
|
|
|
99
100
|
pub fn writeArtifact(self: *TraceWriter, name: []const u8, bytes: []const u8) ![]const u8 {
|
|
100
101
|
const path = try self.artifactPath(name);
|
|
101
102
|
errdefer self.allocator.free(path);
|
|
102
|
-
var file = try std.
|
|
103
|
-
defer file.close();
|
|
104
|
-
try file.
|
|
103
|
+
var file = try std.Io.Dir.cwd().createFile(stdio.io(), path, .{ .truncate = true });
|
|
104
|
+
defer file.close(stdio.io());
|
|
105
|
+
try std.Io.File.writeStreamingAll(file, stdio.io(), bytes);
|
|
105
106
|
return path;
|
|
106
107
|
}
|
|
107
108
|
|
|
@@ -110,15 +111,22 @@ pub const TraceWriter = struct {
|
|
|
110
111
|
if (isPartialFailureEvent(kind, payload)) self.partial_failure_count += 1;
|
|
111
112
|
const path = try std.fs.path.join(self.allocator, &.{ self.root_dir, "events.jsonl" });
|
|
112
113
|
defer self.allocator.free(path);
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
const existing = stdio.readFileAlloc(self.allocator, path, 64 * 1024 * 1024) catch |err| switch (err) {
|
|
115
|
+
error.FileNotFound => "",
|
|
116
|
+
else => return err,
|
|
117
|
+
};
|
|
118
|
+
const had_existing_file = existing.ptr != "".ptr;
|
|
119
|
+
defer if (had_existing_file) self.allocator.free(existing);
|
|
120
|
+
|
|
121
|
+
var file = try std.Io.Dir.cwd().createFile(stdio.io(), path, .{ .truncate = true });
|
|
122
|
+
defer file.close(stdio.io());
|
|
116
123
|
var write_buffer: [4096]u8 = undefined;
|
|
117
|
-
var file_writer = file.writerStreaming(&write_buffer);
|
|
124
|
+
var file_writer = file.writerStreaming(stdio.io(), &write_buffer);
|
|
118
125
|
const writer = &file_writer.interface;
|
|
126
|
+
if (existing.len > 0) try writer.writeAll(existing);
|
|
119
127
|
try writer.print(
|
|
120
128
|
"{{\"seq\":{d},\"timestampMs\":{d},\"kind\":\"{s}\",\"payload\":",
|
|
121
|
-
.{ self.event_count,
|
|
129
|
+
.{ self.event_count, stdio.nowMs(), kind },
|
|
122
130
|
);
|
|
123
131
|
try trace_json.writeRedactedJsonPayload(self.allocator, writer, payload, self.capture.redaction);
|
|
124
132
|
try writer.writeAll("}\n");
|
|
@@ -131,10 +139,10 @@ pub const TraceWriter = struct {
|
|
|
131
139
|
defer self.allocator.free(file_name);
|
|
132
140
|
const path = try self.artifactPath(file_name);
|
|
133
141
|
errdefer self.allocator.free(path);
|
|
134
|
-
var file = try std.
|
|
135
|
-
defer file.close();
|
|
142
|
+
var file = try std.Io.Dir.cwd().createFile(stdio.io(), path, .{ .truncate = true });
|
|
143
|
+
defer file.close(stdio.io());
|
|
136
144
|
var write_buffer: [8192]u8 = undefined;
|
|
137
|
-
var file_writer = file.
|
|
145
|
+
var file_writer = file.writerStreaming(stdio.io(), &write_buffer);
|
|
138
146
|
try trace_json.writeSnapshotJsonRedacted(&file_writer.interface, snapshot, self.capture.redaction);
|
|
139
147
|
try file_writer.interface.flush();
|
|
140
148
|
return path;
|
|
@@ -149,10 +157,10 @@ pub const TraceWriter = struct {
|
|
|
149
157
|
const manifest = self.manifest.?;
|
|
150
158
|
const path = try std.fs.path.join(self.allocator, &.{ self.root_dir, "trace.json" });
|
|
151
159
|
defer self.allocator.free(path);
|
|
152
|
-
var file = try std.
|
|
153
|
-
defer file.close();
|
|
160
|
+
var file = try std.Io.Dir.cwd().createFile(stdio.io(), path, .{ .truncate = true });
|
|
161
|
+
defer file.close(stdio.io());
|
|
154
162
|
var write_buffer: [4096]u8 = undefined;
|
|
155
|
-
var file_writer = file.
|
|
163
|
+
var file_writer = file.writerStreaming(stdio.io(), &write_buffer);
|
|
156
164
|
const writer = &file_writer.interface;
|
|
157
165
|
|
|
158
166
|
try writer.writeAll("{");
|
|
@@ -223,7 +231,7 @@ fn resetTraceDirectory(allocator: std.mem.Allocator, root_dir: []const u8) !void
|
|
|
223
231
|
for (stale_files) |name| {
|
|
224
232
|
const path = try std.fs.path.join(allocator, &.{ root_dir, name });
|
|
225
233
|
defer allocator.free(path);
|
|
226
|
-
std.
|
|
234
|
+
std.Io.Dir.cwd().deleteFile(stdio.io(), path) catch |err| switch (err) {
|
|
227
235
|
error.FileNotFound => {},
|
|
228
236
|
else => return err,
|
|
229
237
|
};
|
|
@@ -232,11 +240,11 @@ fn resetTraceDirectory(allocator: std.mem.Allocator, root_dir: []const u8) !void
|
|
|
232
240
|
const artifacts_path = try std.fs.path.join(allocator, &.{ root_dir, "artifacts" });
|
|
233
241
|
defer allocator.free(artifacts_path);
|
|
234
242
|
var artifacts_exists = true;
|
|
235
|
-
|
|
243
|
+
stdio.access(artifacts_path) catch |err| switch (err) {
|
|
236
244
|
error.FileNotFound => artifacts_exists = false,
|
|
237
245
|
else => return err,
|
|
238
246
|
};
|
|
239
|
-
if (artifacts_exists) try std.
|
|
247
|
+
if (artifacts_exists) try std.Io.Dir.cwd().deleteTree(stdio.io(), artifacts_path);
|
|
240
248
|
}
|
|
241
249
|
|
|
242
250
|
const Manifest = struct {
|
|
@@ -271,7 +279,7 @@ pub fn attachReportPath(allocator: std.mem.Allocator, root_dir: []const u8, repo
|
|
|
271
279
|
const manifest_path = try std.fs.path.join(allocator, &.{ root_dir, "trace.json" });
|
|
272
280
|
defer allocator.free(manifest_path);
|
|
273
281
|
|
|
274
|
-
const content =
|
|
282
|
+
const content = stdio.readFileAlloc(allocator, manifest_path, 1024 * 1024) catch |err| switch (err) {
|
|
275
283
|
error.FileNotFound => return,
|
|
276
284
|
else => return err,
|
|
277
285
|
};
|
|
@@ -283,12 +291,12 @@ pub fn attachReportPath(allocator: std.mem.Allocator, root_dir: []const u8, repo
|
|
|
283
291
|
|
|
284
292
|
const arena_allocator = parsed.arena.allocator();
|
|
285
293
|
const owned_report_path = try arena_allocator.dupe(u8, report_path);
|
|
286
|
-
try parsed.value.object.put("reportPath", .{ .string = owned_report_path });
|
|
294
|
+
try parsed.value.object.put(arena_allocator, "reportPath", .{ .string = owned_report_path });
|
|
287
295
|
|
|
288
|
-
var file = try std.
|
|
289
|
-
defer file.close();
|
|
296
|
+
var file = try std.Io.Dir.cwd().createFile(stdio.io(), manifest_path, .{ .truncate = true });
|
|
297
|
+
defer file.close(stdio.io());
|
|
290
298
|
var write_buffer: [4096]u8 = undefined;
|
|
291
|
-
var file_writer = file.
|
|
299
|
+
var file_writer = file.writerStreaming(stdio.io(), &write_buffer);
|
|
292
300
|
try std.json.Stringify.value(parsed.value, .{}, &file_writer.interface);
|
|
293
301
|
try file_writer.interface.writeByte('\n');
|
|
294
302
|
try file_writer.interface.flush();
|
package/src/trace_summary.zig
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const std = @import("std");
|
|
2
|
+
const stdio = @import("stdio.zig");
|
|
2
3
|
const trace = @import("trace.zig");
|
|
3
4
|
const trace_summary_diagnostic = @import("trace_summary_diagnostic.zig");
|
|
4
5
|
|
|
@@ -59,7 +60,7 @@ const TerminalEvent = struct {
|
|
|
59
60
|
pub fn read(allocator: std.mem.Allocator, trace_dir: []const u8) !Summary {
|
|
60
61
|
const manifest_path = try std.fs.path.join(allocator, &.{ trace_dir, "trace.json" });
|
|
61
62
|
defer allocator.free(manifest_path);
|
|
62
|
-
const manifest_content = try
|
|
63
|
+
const manifest_content = try stdio.readFileAlloc(allocator, manifest_path, 1024 * 1024);
|
|
63
64
|
defer allocator.free(manifest_content);
|
|
64
65
|
|
|
65
66
|
const manifest = try std.json.parseFromSlice(std.json.Value, allocator, manifest_content, .{});
|
|
@@ -91,7 +92,7 @@ pub fn read(allocator: std.mem.Allocator, trace_dir: []const u8) !Summary {
|
|
|
91
92
|
|
|
92
93
|
const events_path = try std.fs.path.join(allocator, &.{ trace_dir, events_path_value });
|
|
93
94
|
defer allocator.free(events_path);
|
|
94
|
-
if (
|
|
95
|
+
if (stdio.readFileAlloc(allocator, events_path, 64 * 1024 * 1024)) |events_content| {
|
|
95
96
|
defer allocator.free(events_content);
|
|
96
97
|
try scanEvents(allocator, events_content, &terminal, &diagnostic, &partial_failure, &last_kind);
|
|
97
98
|
} else |err| switch (err) {
|
|
@@ -142,44 +142,46 @@ fn writeJoinedStringArrayJson(writer: anytype, value: []const u8) !void {
|
|
|
142
142
|
|
|
143
143
|
fn joinStringArray(allocator: std.mem.Allocator, value: std.json.Value, limit: usize) !?[]u8 {
|
|
144
144
|
if (value != .array or value.array.items.len == 0) return null;
|
|
145
|
-
var out =
|
|
146
|
-
errdefer out.deinit(
|
|
145
|
+
var out: std.Io.Writer.Allocating = .init(allocator);
|
|
146
|
+
errdefer out.deinit();
|
|
147
|
+
const writer = &out.writer;
|
|
147
148
|
var written: usize = 0;
|
|
148
149
|
for (value.array.items) |item| {
|
|
149
150
|
if (item != .string) continue;
|
|
150
|
-
if (written > 0) try
|
|
151
|
-
try
|
|
151
|
+
if (written > 0) try writer.writeAll(" | ");
|
|
152
|
+
try writer.writeAll(item.string);
|
|
152
153
|
written += 1;
|
|
153
154
|
if (written >= limit) break;
|
|
154
155
|
}
|
|
155
156
|
if (written == 0) {
|
|
156
|
-
out.deinit(
|
|
157
|
+
out.deinit();
|
|
157
158
|
return null;
|
|
158
159
|
}
|
|
159
|
-
return try out.toOwnedSlice(
|
|
160
|
+
return try out.toOwnedSlice();
|
|
160
161
|
}
|
|
161
162
|
|
|
162
163
|
fn joinNearestMatches(allocator: std.mem.Allocator, value: std.json.Value, limit: usize) !?[]u8 {
|
|
163
164
|
if (value != .array or value.array.items.len == 0) return null;
|
|
164
|
-
var out =
|
|
165
|
-
errdefer out.deinit(
|
|
165
|
+
var out: std.Io.Writer.Allocating = .init(allocator);
|
|
166
|
+
errdefer out.deinit();
|
|
167
|
+
const writer = &out.writer;
|
|
166
168
|
var written: usize = 0;
|
|
167
169
|
for (value.array.items) |item| {
|
|
168
170
|
if (item != .object) continue;
|
|
169
171
|
const text = stringField(item.object, "text") orelse continue;
|
|
170
|
-
if (written > 0) try
|
|
171
|
-
try
|
|
172
|
+
if (written > 0) try writer.writeAll(" | ");
|
|
173
|
+
try writer.writeAll(text);
|
|
172
174
|
if (intField(item.object, "score")) |score| {
|
|
173
|
-
try
|
|
175
|
+
try writer.print(" (score {d})", .{score});
|
|
174
176
|
}
|
|
175
177
|
written += 1;
|
|
176
178
|
if (written >= limit) break;
|
|
177
179
|
}
|
|
178
180
|
if (written == 0) {
|
|
179
|
-
out.deinit(
|
|
181
|
+
out.deinit();
|
|
180
182
|
return null;
|
|
181
183
|
}
|
|
182
|
-
return try out.toOwnedSlice(
|
|
184
|
+
return try out.toOwnedSlice();
|
|
183
185
|
}
|
|
184
186
|
|
|
185
187
|
fn dupeOptionalString(allocator: std.mem.Allocator, value: ?[]const u8) !?[]u8 {
|
package/src/validation.zig
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const std = @import("std");
|
|
2
|
+
const stdio = @import("stdio.zig");
|
|
2
3
|
const errors = @import("errors.zig");
|
|
3
4
|
const scenario = @import("scenario.zig");
|
|
4
5
|
|
|
@@ -23,7 +24,7 @@ pub const Result = struct {
|
|
|
23
24
|
};
|
|
24
25
|
|
|
25
26
|
pub fn validateFile(allocator: std.mem.Allocator, path: []const u8) !Result {
|
|
26
|
-
const content =
|
|
27
|
+
const content = stdio.readFileAlloc(allocator, path, 16 * 1024 * 1024) catch |err| return failure(allocator, null, err);
|
|
27
28
|
defer allocator.free(content);
|
|
28
29
|
const script = scenario.parseSlice(allocator, content) catch |err| return failure(allocator, content, err);
|
|
29
30
|
defer script.deinit(allocator);
|
|
@@ -91,10 +92,10 @@ fn diagnoseFailure(allocator: std.mem.Allocator, content: []const u8, err: anyer
|
|
|
91
92
|
=> try pathDiagnostic(allocator, content, "$.steps", "steps"),
|
|
92
93
|
error.StepMissingAction,
|
|
93
94
|
error.StepActionMustBeString,
|
|
94
|
-
error.
|
|
95
|
-
error.
|
|
95
|
+
error.unknownAction,
|
|
96
|
+
error.unknownScenarioAction,
|
|
96
97
|
=> try pathDiagnostic(allocator, content, "$.steps[].action", "action"),
|
|
97
|
-
error.
|
|
98
|
+
error.unknownScrollDirection,
|
|
98
99
|
=> try pathDiagnostic(allocator, content, "$.steps[].direction", "direction"),
|
|
99
100
|
error.StepMissingUrl,
|
|
100
101
|
=> try pathDiagnostic(allocator, content, "$.steps[].url", "url"),
|
package/src/version.zig
CHANGED