zeno-mobile-runner 0.1.3 → 0.2.0
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 +192 -2
- package/FEATURES.md +50 -7
- package/README.md +168 -120
- package/build.zig.zon +3 -3
- package/clients/README.md +60 -3
- package/clients/go/README.md +12 -0
- package/clients/go/zmr/client.go +142 -0
- package/clients/kotlin/README.md +18 -1
- package/clients/kotlin/build.gradle.kts +1 -1
- package/clients/kotlin/src/main/kotlin/dev/zmr/ZmrClient.kt +76 -1
- package/clients/python/README.md +19 -0
- package/clients/python/pyproject.toml +1 -1
- package/clients/python/zmr_client.py +33 -0
- package/clients/rust/Cargo.lock +1 -1
- package/clients/rust/Cargo.toml +1 -1
- package/clients/rust/README.md +25 -1
- package/clients/rust/src/lib.rs +201 -0
- package/clients/swift/README.md +18 -0
- package/clients/swift/Sources/ZMRClient/ZMRClient.swift +82 -0
- package/clients/typescript/README.md +16 -0
- package/clients/typescript/index.d.ts +12 -0
- package/clients/typescript/index.mjs +16 -0
- package/clients/typescript/package.json +1 -1
- package/docs/agent-discovery.md +151 -22
- package/docs/ai-agents.md +99 -11
- package/docs/benchmarking.md +49 -3
- package/docs/benchmarks/2026-06-09-android-workflow.md +73 -0
- package/docs/benchmarks/2026-06-09-android-workflow.results.jsonl +20 -0
- package/docs/benchmarks/2026-06-09-framework-baseline-status.md +32 -0
- package/docs/benchmarks/2026-06-09-ios-appium-comparison.md +115 -0
- package/docs/benchmarks/2026-06-09-ios-appium-comparison.results.jsonl +40 -0
- package/docs/benchmarks/2026-06-09-ios-demo.md +90 -0
- package/docs/benchmarks/2026-06-09-ios-demo.results.jsonl +20 -0
- package/docs/benchmarks/2026-06-09-ios-maestro-comparison.md +128 -0
- package/docs/benchmarks/2026-06-09-ios-maestro-comparison.results.jsonl +40 -0
- package/docs/benchmarks/2026-06-09-ios-workflow-comparison.md +143 -0
- package/docs/benchmarks/2026-06-09-ios-workflow-comparison.results.jsonl +40 -0
- package/docs/benchmarks/2026-06-09-ios-xctest-floor.md +106 -0
- package/docs/benchmarks/2026-06-09-ios-xctest-floor.results.jsonl +40 -0
- package/docs/benchmarks/README.md +36 -0
- package/docs/benchmarks/benchmark-lab-v1.json +155 -0
- package/docs/benchmarks/benchmark-lab-v1.md +95 -0
- package/docs/clients.md +26 -6
- package/docs/demo.md +40 -1
- package/docs/expo-smoke.md +8 -8
- package/docs/frameworks.md +10 -0
- package/docs/install.md +3 -2
- package/docs/npm.md +100 -4
- package/docs/production-readiness.md +123 -0
- package/docs/protocol-fixtures/core-session.responses.jsonl +1 -1
- package/docs/protocol.md +215 -16
- package/docs/scenario-authoring.md +18 -0
- package/docs/trace-privacy.md +9 -0
- package/docs/troubleshooting.md +7 -1
- package/examples/android-workflow.json +79 -0
- package/examples/ios-shim-workflow.json +79 -0
- package/examples/react-native-expo-workflow.json +75 -0
- package/npm/agents.mjs +16 -0
- package/npm/commands.mjs +9 -5
- package/package.json +6 -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/schemas/README.md +4 -0
- package/schemas/discover-output.schema.json +83 -0
- package/schemas/draft-output.schema.json +58 -0
- package/schemas/explore-output.schema.json +94 -0
- package/schemas/inspect-output.schema.json +88 -0
- package/schemas/run-output.schema.json +2 -0
- package/scripts/benchmark-lab.py +253 -0
- package/scripts/create-android-demo-app.sh +324 -29
- package/scripts/create-ios-demo-app.sh +174 -7
- package/scripts/create-react-native-expo-demo-app.sh +727 -0
- package/scripts/demo.sh +3 -0
- package/scripts/install-ios-shim.sh +2 -2
- package/scripts/release-readiness.py +43 -0
- package/scripts/run-android-pilot.sh +35 -9
- package/scripts/run-ios-pilot.sh +11 -4
- package/shims/ios/ZMRShim.swift +10 -0
- package/shims/ios/ZMRShimUITestCase.swift +42 -0
- package/shims/ios/protocol.md +1 -0
- package/skills/zmr-mobile-testing/SKILL.md +28 -3
- package/src/cli_discover.zig +239 -0
- package/src/cli_draft.zig +924 -0
- package/src/cli_explore.zig +136 -0
- package/src/cli_import.zig +31 -15
- package/src/cli_inspect.zig +310 -0
- package/src/cli_output.zig +26 -2
- package/src/cli_run.zig +28 -0
- package/src/cli_trace.zig +45 -15
- package/src/cli_validate.zig +12 -6
- package/src/errors.zig +9 -0
- package/src/ios.zig +49 -12
- package/src/ios_shim.zig +36 -2
- package/src/json_rpc_methods.zig +85 -11
- package/src/json_rpc_params.zig +8 -0
- package/src/json_rpc_protocol.zig +1 -1
- package/src/json_rpc_trace.zig +112 -0
- package/src/main.zig +27 -2
- package/src/mcp.zig +209 -6
- package/src/mcp_protocol.zig +29 -1
- package/src/mcp_trace.zig +126 -4
- package/src/report.zig +186 -0
- package/src/runner.zig +26 -4
- package/src/runner_actions.zig +10 -0
- package/src/runner_diagnostics.zig +31 -1
- package/src/runner_events.zig +70 -7
- package/src/runner_native.zig +17 -1
- package/src/runner_waits.zig +82 -19
- package/src/scaffold.zig +28 -12
- package/src/scenario.zig +32 -4
- package/src/schema_registry.zig +4 -0
- package/src/version.zig +1 -1
- package/viewer/app.js +23 -3
package/src/mcp_trace.zig
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
const std = @import("std");
|
|
2
2
|
const bundle = @import("bundle.zig");
|
|
3
|
+
const cli_discover = @import("cli_discover.zig");
|
|
4
|
+
const cli_explore = @import("cli_explore.zig");
|
|
3
5
|
const mcp_protocol = @import("mcp_protocol.zig");
|
|
6
|
+
const report = @import("report.zig");
|
|
7
|
+
const runner_events = @import("runner_events.zig");
|
|
4
8
|
const trace = @import("trace.zig");
|
|
5
9
|
|
|
6
10
|
pub fn writeEventsToolResult(
|
|
@@ -12,7 +16,10 @@ pub fn writeEventsToolResult(
|
|
|
12
16
|
limit: u64,
|
|
13
17
|
) !void {
|
|
14
18
|
const tw = live_trace orelse {
|
|
15
|
-
|
|
19
|
+
var no_trace_payload = std.ArrayList(u8).empty;
|
|
20
|
+
defer no_trace_payload.deinit(allocator);
|
|
21
|
+
try no_trace_payload.writer(allocator).print("{{\"traceDir\":null,\"afterSeq\":{d},\"nextSeq\":{d},\"latestSeq\":0,\"events\":[]}}", .{ after_seq, after_seq });
|
|
22
|
+
try mcp_protocol.writeToolTextResult(writer, id, no_trace_payload.items);
|
|
16
23
|
return;
|
|
17
24
|
};
|
|
18
25
|
|
|
@@ -29,7 +36,11 @@ pub fn writeEventsToolResult(
|
|
|
29
36
|
const payload_writer = payload.writer(allocator);
|
|
30
37
|
try payload_writer.writeAll("{\"traceDir\":");
|
|
31
38
|
try trace.writeJsonString(payload_writer, tw.root_dir);
|
|
32
|
-
try payload_writer.print(",\"afterSeq\":{d},\"
|
|
39
|
+
try payload_writer.print(",\"afterSeq\":{d},\"nextSeq\":", .{after_seq});
|
|
40
|
+
var events_json = std.ArrayList(u8).empty;
|
|
41
|
+
defer events_json.deinit(allocator);
|
|
42
|
+
const events_writer = events_json.writer(allocator);
|
|
43
|
+
var next_seq = after_seq;
|
|
33
44
|
var emitted: u64 = 0;
|
|
34
45
|
var lines = std.mem.splitScalar(u8, content, '\n');
|
|
35
46
|
while (lines.next()) |raw_line| {
|
|
@@ -43,10 +54,13 @@ pub fn writeEventsToolResult(
|
|
|
43
54
|
if (seq_value != .integer or seq_value.integer <= 0) continue;
|
|
44
55
|
const seq = @as(u64, @intCast(seq_value.integer));
|
|
45
56
|
if (seq <= after_seq) continue;
|
|
46
|
-
if (emitted > 0) try
|
|
47
|
-
try
|
|
57
|
+
if (emitted > 0) try events_writer.writeAll(",");
|
|
58
|
+
try events_writer.writeAll(line);
|
|
59
|
+
next_seq = seq;
|
|
48
60
|
emitted += 1;
|
|
49
61
|
}
|
|
62
|
+
try payload_writer.print("{d},\"latestSeq\":{d},\"events\":[", .{ next_seq, tw.event_count });
|
|
63
|
+
try payload_writer.writeAll(events_json.items);
|
|
50
64
|
try payload_writer.writeAll("]}");
|
|
51
65
|
try mcp_protocol.writeToolTextResult(writer, id, payload.items);
|
|
52
66
|
}
|
|
@@ -81,3 +95,111 @@ pub fn writeExportToolResult(
|
|
|
81
95
|
try payload_writer.print(",\"redacted\":{},\"omitScreenshots\":{}}}", .{ redact, omit_screenshots });
|
|
82
96
|
try mcp_protocol.writeToolTextResult(writer, id, payload.items);
|
|
83
97
|
}
|
|
98
|
+
|
|
99
|
+
pub fn writeExplainToolResult(
|
|
100
|
+
allocator: std.mem.Allocator,
|
|
101
|
+
writer: anytype,
|
|
102
|
+
id: ?std.json.Value,
|
|
103
|
+
live_trace: ?*trace.TraceWriter,
|
|
104
|
+
) !void {
|
|
105
|
+
const tw = live_trace orelse {
|
|
106
|
+
try mcp_protocol.writeToolTextResult(writer, id, "{\"traceDir\":null,\"message\":\"start zmr mcp with --trace-dir to enable live trace explanation\"}");
|
|
107
|
+
return;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
try tw.flushManifest();
|
|
111
|
+
var payload = std.ArrayList(u8).empty;
|
|
112
|
+
defer payload.deinit(allocator);
|
|
113
|
+
try report.writeTraceExplanationJson(allocator, tw.root_dir, payload.writer(allocator));
|
|
114
|
+
try mcp_protocol.writeToolTextResult(writer, id, std.mem.trimRight(u8, payload.items, " \t\r\n"));
|
|
115
|
+
try tw.recordEvent("trace.explain", "{\"status\":\"ok\"}");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
pub fn writeDiscoverToolResult(
|
|
119
|
+
allocator: std.mem.Allocator,
|
|
120
|
+
writer: anytype,
|
|
121
|
+
id: ?std.json.Value,
|
|
122
|
+
live_trace: ?*trace.TraceWriter,
|
|
123
|
+
out_path: []const u8,
|
|
124
|
+
include_actions: bool,
|
|
125
|
+
validate: bool,
|
|
126
|
+
force: bool,
|
|
127
|
+
name: ?[]const u8,
|
|
128
|
+
app_id: ?[]const u8,
|
|
129
|
+
) !void {
|
|
130
|
+
const tw = live_trace orelse {
|
|
131
|
+
try mcp_protocol.writeToolTextResult(writer, id, "{\"ok\":false,\"mode\":\"discover\",\"traceDir\":null,\"message\":\"start zmr mcp with --trace-dir to enable live trace discovery\"}");
|
|
132
|
+
return;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
try tw.flushManifest();
|
|
136
|
+
var discovered = try cli_discover.discoverFromTrace(allocator, .{
|
|
137
|
+
.from_trace = tw.root_dir,
|
|
138
|
+
.out_path = out_path,
|
|
139
|
+
.name = name,
|
|
140
|
+
.app_id = app_id,
|
|
141
|
+
.include_actions = include_actions,
|
|
142
|
+
.validate = validate,
|
|
143
|
+
.force = force,
|
|
144
|
+
.json = true,
|
|
145
|
+
});
|
|
146
|
+
defer discovered.deinit(allocator);
|
|
147
|
+
try runner_events.recordTraceDiscover(
|
|
148
|
+
tw,
|
|
149
|
+
if (discovered.summary.ok) "ok" else "failed",
|
|
150
|
+
discovered.summary.draft.out_path,
|
|
151
|
+
include_actions,
|
|
152
|
+
discovered.summary.validated,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
var payload = std.ArrayList(u8).empty;
|
|
156
|
+
defer payload.deinit(allocator);
|
|
157
|
+
try cli_discover.writeJson(payload.writer(allocator), discovered.summary, discovered.validation);
|
|
158
|
+
try mcp_protocol.writeToolTextResult(writer, id, std.mem.trimRight(u8, payload.items, " \t\r\n"));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
pub fn writeExploreToolResult(
|
|
162
|
+
allocator: std.mem.Allocator,
|
|
163
|
+
writer: anytype,
|
|
164
|
+
id: ?std.json.Value,
|
|
165
|
+
live_trace: ?*trace.TraceWriter,
|
|
166
|
+
out_path: []const u8,
|
|
167
|
+
goal: []const u8,
|
|
168
|
+
include_actions: bool,
|
|
169
|
+
validate: bool,
|
|
170
|
+
force: bool,
|
|
171
|
+
name: ?[]const u8,
|
|
172
|
+
app_id: ?[]const u8,
|
|
173
|
+
) !void {
|
|
174
|
+
const tw = live_trace orelse {
|
|
175
|
+
try mcp_protocol.writeToolTextResult(writer, id, "{\"ok\":false,\"mode\":\"explore\",\"traceDir\":null,\"message\":\"start zmr mcp with --trace-dir to enable live trace exploration\"}");
|
|
176
|
+
return;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
try tw.flushManifest();
|
|
180
|
+
var explored = try cli_explore.exploreFromTrace(allocator, .{
|
|
181
|
+
.from_trace = tw.root_dir,
|
|
182
|
+
.out_path = out_path,
|
|
183
|
+
.goal = goal,
|
|
184
|
+
.name = name,
|
|
185
|
+
.app_id = app_id,
|
|
186
|
+
.include_actions = include_actions,
|
|
187
|
+
.validate = validate,
|
|
188
|
+
.force = force,
|
|
189
|
+
.json = true,
|
|
190
|
+
});
|
|
191
|
+
defer explored.deinit(allocator);
|
|
192
|
+
try runner_events.recordTraceExplore(
|
|
193
|
+
tw,
|
|
194
|
+
if (explored.summary.ok) "ok" else "failed",
|
|
195
|
+
explored.discovered.summary.draft.out_path,
|
|
196
|
+
goal,
|
|
197
|
+
include_actions,
|
|
198
|
+
explored.discovered.summary.validated,
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
var payload = std.ArrayList(u8).empty;
|
|
202
|
+
defer payload.deinit(allocator);
|
|
203
|
+
try cli_explore.writeJson(payload.writer(allocator), explored.summary, explored.discovered.summary, explored.discovered.validation);
|
|
204
|
+
try mcp_protocol.writeToolTextResult(writer, id, std.mem.trimRight(u8, payload.items, " \t\r\n"));
|
|
205
|
+
}
|
package/src/report.zig
CHANGED
|
@@ -22,6 +22,23 @@ pub fn writeHtmlReport(
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
pub fn writeJUnitReport(
|
|
26
|
+
allocator: std.mem.Allocator,
|
|
27
|
+
input_path: []const u8,
|
|
28
|
+
out_path: []const u8,
|
|
29
|
+
) !void {
|
|
30
|
+
const results_path = try std.fs.path.join(allocator, &.{ input_path, "results.jsonl" });
|
|
31
|
+
defer allocator.free(results_path);
|
|
32
|
+
|
|
33
|
+
if (std.fs.cwd().openFile(results_path, .{})) |file| {
|
|
34
|
+
file.close();
|
|
35
|
+
return try writeBenchmarkJUnitReport(allocator, input_path, results_path, out_path);
|
|
36
|
+
} else |err| switch (err) {
|
|
37
|
+
error.FileNotFound => return try writeTraceJUnitReport(allocator, input_path, out_path),
|
|
38
|
+
else => return err,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
25
42
|
pub fn writeTraceExplanation(
|
|
26
43
|
allocator: std.mem.Allocator,
|
|
27
44
|
trace_dir: []const u8,
|
|
@@ -151,6 +168,8 @@ fn writeTraceExplanationNextCommandsJson(writer: anytype, trace_dir: []const u8)
|
|
|
151
168
|
try cli_output.writeShellArgJsonContent(writer, trace_dir);
|
|
152
169
|
try writer.writeAll(" --out ");
|
|
153
170
|
try cli_output.writeJoinedPathShellArgJsonContent(writer, trace_dir, "report.html");
|
|
171
|
+
try writer.writeAll(" --junit ");
|
|
172
|
+
try cli_output.writeJoinedPathShellArgJsonContent(writer, trace_dir, "junit.xml");
|
|
154
173
|
try writer.writeAll("\",\"zmr export ");
|
|
155
174
|
try cli_output.writeShellArgJsonContent(writer, trace_dir);
|
|
156
175
|
try writer.writeAll(" --out ");
|
|
@@ -260,6 +279,89 @@ fn writeBenchmarkReport(
|
|
|
260
279
|
try report_html.writeFile(out_path, html.items);
|
|
261
280
|
}
|
|
262
281
|
|
|
282
|
+
fn writeBenchmarkJUnitReport(
|
|
283
|
+
allocator: std.mem.Allocator,
|
|
284
|
+
input_path: []const u8,
|
|
285
|
+
results_path: []const u8,
|
|
286
|
+
out_path: []const u8,
|
|
287
|
+
) !void {
|
|
288
|
+
const content = try std.fs.cwd().readFileAlloc(allocator, results_path, 64 * 1024 * 1024);
|
|
289
|
+
defer allocator.free(content);
|
|
290
|
+
|
|
291
|
+
var cases_xml = std.ArrayList(u8).empty;
|
|
292
|
+
defer cases_xml.deinit(allocator);
|
|
293
|
+
|
|
294
|
+
var total: usize = 0;
|
|
295
|
+
var failed: usize = 0;
|
|
296
|
+
var total_duration_ms: i64 = 0;
|
|
297
|
+
|
|
298
|
+
var lines = std.mem.splitScalar(u8, content, '\n');
|
|
299
|
+
while (lines.next()) |raw_line| {
|
|
300
|
+
const line = std.mem.trim(u8, raw_line, " \t\r\n");
|
|
301
|
+
if (line.len == 0) continue;
|
|
302
|
+
|
|
303
|
+
const parsed = try std.json.parseFromSlice(std.json.Value, allocator, line, .{});
|
|
304
|
+
defer parsed.deinit();
|
|
305
|
+
if (parsed.value != .object) continue;
|
|
306
|
+
const object = parsed.value.object;
|
|
307
|
+
|
|
308
|
+
const tool = report_values.stringField(object, "tool") orelse "zmr";
|
|
309
|
+
const status = report_values.stringField(object, "status") orelse "";
|
|
310
|
+
const trace_status = report_values.stringField(object, "traceStatus") orelse "";
|
|
311
|
+
const trace_error = report_values.stringField(object, "traceError") orelse "";
|
|
312
|
+
const trace_dir = report_values.stringField(object, "traceDir") orelse "";
|
|
313
|
+
const run = report_values.intField(object, "run") orelse @as(i64, @intCast(total + 1));
|
|
314
|
+
const duration_ms = report_values.intField(object, "durationMs") orelse 0;
|
|
315
|
+
const failed_step = report_values.intField(object, "failedStepIndex");
|
|
316
|
+
const row_passed = std.mem.eql(u8, status, "ok") and (trace_status.len == 0 or std.mem.eql(u8, trace_status, "passed"));
|
|
317
|
+
|
|
318
|
+
total += 1;
|
|
319
|
+
if (!row_passed) failed += 1;
|
|
320
|
+
if (duration_ms > 0) total_duration_ms += duration_ms;
|
|
321
|
+
|
|
322
|
+
const writer = cases_xml.writer(allocator);
|
|
323
|
+
try writer.writeAll(" <testcase classname=\"");
|
|
324
|
+
try report_html.escape(writer, tool);
|
|
325
|
+
try writer.writeAll("\" name=\"run ");
|
|
326
|
+
try writer.print("{d}", .{run});
|
|
327
|
+
try writer.writeAll("\" time=\"");
|
|
328
|
+
try writeSeconds(writer, duration_ms);
|
|
329
|
+
try writer.writeAll("\">");
|
|
330
|
+
if (!row_passed) {
|
|
331
|
+
const message = if (trace_error.len > 0) trace_error else if (trace_status.len > 0) trace_status else status;
|
|
332
|
+
try writer.writeAll("<failure message=\"");
|
|
333
|
+
try report_html.escape(writer, message);
|
|
334
|
+
try writer.writeAll("\" type=\"ZMRFailure\">");
|
|
335
|
+
if (failed_step) |index| try writer.print("failedStepIndex={d}", .{index});
|
|
336
|
+
if (trace_dir.len > 0) {
|
|
337
|
+
if (failed_step != null) try writer.writeAll(" ");
|
|
338
|
+
try writer.writeAll("traceDir=");
|
|
339
|
+
try report_html.escape(writer, trace_dir);
|
|
340
|
+
}
|
|
341
|
+
try writer.writeAll("</failure>");
|
|
342
|
+
}
|
|
343
|
+
try writer.writeAll("</testcase>\n");
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
var xml = std.ArrayList(u8).empty;
|
|
347
|
+
defer xml.deinit(allocator);
|
|
348
|
+
const writer = xml.writer(allocator);
|
|
349
|
+
try writeJUnitHeader(writer);
|
|
350
|
+
try writer.writeAll("<testsuite name=\"ZMR Benchmark\" tests=\"");
|
|
351
|
+
try writer.print("{d}", .{total});
|
|
352
|
+
try writer.writeAll("\" failures=\"");
|
|
353
|
+
try writer.print("{d}", .{failed});
|
|
354
|
+
try writer.writeAll("\" errors=\"0\" skipped=\"0\" time=\"");
|
|
355
|
+
try writeSeconds(writer, total_duration_ms);
|
|
356
|
+
try writer.writeAll("\">\n");
|
|
357
|
+
try writer.writeAll(" <properties>\n");
|
|
358
|
+
try writeJUnitProperty(writer, "source", input_path);
|
|
359
|
+
try writer.writeAll(" </properties>\n");
|
|
360
|
+
try writer.writeAll(cases_xml.items);
|
|
361
|
+
try writer.writeAll("</testsuite>\n");
|
|
362
|
+
try report_html.writeFile(out_path, xml.items);
|
|
363
|
+
}
|
|
364
|
+
|
|
263
365
|
fn writeTraceReport(
|
|
264
366
|
allocator: std.mem.Allocator,
|
|
265
367
|
input_path: []const u8,
|
|
@@ -344,3 +446,87 @@ fn writeTraceReport(
|
|
|
344
446
|
defer allocator.free(relative_report_path);
|
|
345
447
|
try trace.attachReportPath(allocator, input_path, relative_report_path);
|
|
346
448
|
}
|
|
449
|
+
|
|
450
|
+
fn writeTraceJUnitReport(
|
|
451
|
+
allocator: std.mem.Allocator,
|
|
452
|
+
input_path: []const u8,
|
|
453
|
+
out_path: []const u8,
|
|
454
|
+
) !void {
|
|
455
|
+
var summary = try trace_summary.read(allocator, input_path);
|
|
456
|
+
defer summary.deinit(allocator);
|
|
457
|
+
|
|
458
|
+
const failed = !isPassedStatus(summary.status);
|
|
459
|
+
var xml = std.ArrayList(u8).empty;
|
|
460
|
+
defer xml.deinit(allocator);
|
|
461
|
+
const writer = xml.writer(allocator);
|
|
462
|
+
|
|
463
|
+
try writeJUnitHeader(writer);
|
|
464
|
+
try writer.writeAll("<testsuite name=\"ZMR\" tests=\"1\" failures=\"");
|
|
465
|
+
try writer.writeAll(if (failed) "1" else "0");
|
|
466
|
+
try writer.writeAll("\" errors=\"0\" skipped=\"0\" time=\"");
|
|
467
|
+
try writeSeconds(writer, summary.duration_ms orelse 0);
|
|
468
|
+
try writer.writeAll("\">\n");
|
|
469
|
+
try writer.writeAll(" <properties>\n");
|
|
470
|
+
try writeJUnitProperty(writer, "traceDir", input_path);
|
|
471
|
+
try writeJUnitProperty(writer, "status", summary.status);
|
|
472
|
+
if (summary.event_count) |value| try writeJUnitIntProperty(writer, "eventCount", value);
|
|
473
|
+
if (summary.snapshot_count) |value| try writeJUnitIntProperty(writer, "snapshotCount", value);
|
|
474
|
+
if (summary.failed_step_index) |value| try writeJUnitIntProperty(writer, "failedStepIndex", value);
|
|
475
|
+
try writer.writeAll(" </properties>\n");
|
|
476
|
+
|
|
477
|
+
try writer.writeAll(" <testcase classname=\"");
|
|
478
|
+
try report_html.escape(writer, summary.app_id orelse "zmr");
|
|
479
|
+
try writer.writeAll("\" name=\"");
|
|
480
|
+
try report_html.escape(writer, summary.scenario_name);
|
|
481
|
+
try writer.writeAll("\" time=\"");
|
|
482
|
+
try writeSeconds(writer, summary.duration_ms orelse 0);
|
|
483
|
+
try writer.writeAll("\">");
|
|
484
|
+
if (failed) {
|
|
485
|
+
const message = summary.error_name orelse summary.status;
|
|
486
|
+
const failure_type = summary.error_name orelse "ZMRFailure";
|
|
487
|
+
try writer.writeAll("<failure message=\"");
|
|
488
|
+
try report_html.escape(writer, message);
|
|
489
|
+
try writer.writeAll("\" type=\"");
|
|
490
|
+
try report_html.escape(writer, failure_type);
|
|
491
|
+
try writer.writeAll("\">");
|
|
492
|
+
if (summary.failed_step_index) |index| {
|
|
493
|
+
try writer.print("failedStepIndex={d}", .{index});
|
|
494
|
+
} else {
|
|
495
|
+
try writer.writeAll("status=");
|
|
496
|
+
try report_html.escape(writer, summary.status);
|
|
497
|
+
}
|
|
498
|
+
try writer.writeAll("</failure>");
|
|
499
|
+
}
|
|
500
|
+
try writer.writeAll("</testcase>\n");
|
|
501
|
+
try writer.writeAll("</testsuite>\n");
|
|
502
|
+
try report_html.writeFile(out_path, xml.items);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
fn isPassedStatus(status: []const u8) bool {
|
|
506
|
+
return std.mem.eql(u8, status, "passed");
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
fn writeJUnitHeader(writer: anytype) !void {
|
|
510
|
+
try writer.writeAll("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
fn writeJUnitProperty(writer: anytype, name: []const u8, value: []const u8) !void {
|
|
514
|
+
try writer.writeAll(" <property name=\"");
|
|
515
|
+
try report_html.escape(writer, name);
|
|
516
|
+
try writer.writeAll("\" value=\"");
|
|
517
|
+
try report_html.escape(writer, value);
|
|
518
|
+
try writer.writeAll("\"/>\n");
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
fn writeJUnitIntProperty(writer: anytype, name: []const u8, value: i64) !void {
|
|
522
|
+
try writer.writeAll(" <property name=\"");
|
|
523
|
+
try report_html.escape(writer, name);
|
|
524
|
+
try writer.writeAll("\" value=\"");
|
|
525
|
+
try writer.print("{d}", .{value});
|
|
526
|
+
try writer.writeAll("\"/>\n");
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
fn writeSeconds(writer: anytype, duration_ms: i64) !void {
|
|
530
|
+
const safe_ms = @max(duration_ms, 0);
|
|
531
|
+
try writer.print("{d}.{d:0>3}", .{ @divTrunc(safe_ms, 1000), @mod(safe_ms, 1000) });
|
|
532
|
+
}
|
package/src/runner.zig
CHANGED
|
@@ -102,6 +102,7 @@ pub fn executeStep(
|
|
|
102
102
|
},
|
|
103
103
|
.press_back => {
|
|
104
104
|
try device.pressBack();
|
|
105
|
+
if (writer) |tw| try tw.recordEvent("ui.pressBack", "{\"status\":\"ok\"}");
|
|
105
106
|
try settleDevice(device, options);
|
|
106
107
|
},
|
|
107
108
|
.hide_keyboard => {
|
|
@@ -111,6 +112,7 @@ pub fn executeStep(
|
|
|
111
112
|
},
|
|
112
113
|
.swipe => |swipe| {
|
|
113
114
|
try device.swipe(swipe.x1, swipe.y1, swipe.x2, swipe.y2, swipe.duration_ms);
|
|
115
|
+
if (writer) |tw| try runner_events.recordSwipe(tw, swipe.x1, swipe.y1, swipe.x2, swipe.y2, swipe.duration_ms);
|
|
114
116
|
try settleDevice(device, options);
|
|
115
117
|
},
|
|
116
118
|
.wait_visible => |wait| {
|
|
@@ -122,11 +124,11 @@ pub fn executeStep(
|
|
|
122
124
|
.wait_any => |wait| {
|
|
123
125
|
if (try waitUntilAnyVisible(device, wait.selectors, wait.timeout_ms, writer, options) == null) return error.WaitTimeout;
|
|
124
126
|
},
|
|
125
|
-
.assert_visible => |
|
|
126
|
-
if (!try
|
|
127
|
+
.assert_visible => |assertion| {
|
|
128
|
+
if (!try assertVisible(device, assertion.selector, assertion.timeout_ms orelse options.default_timeout_ms, writer, options)) return error.AssertionFailed;
|
|
127
129
|
},
|
|
128
|
-
.assert_not_visible => |
|
|
129
|
-
if (!try
|
|
130
|
+
.assert_not_visible => |assertion| {
|
|
131
|
+
if (!try assertNotVisible(device, assertion.selector, assertion.timeout_ms orelse options.default_timeout_ms, writer, options)) return error.AssertionFailed;
|
|
130
132
|
},
|
|
131
133
|
.assert_none_visible => |assertion| {
|
|
132
134
|
if (!try assertNoneVisible(device, assertion.selectors, assertion.timeout_ms, writer, options)) return error.AssertionFailed;
|
|
@@ -231,6 +233,26 @@ pub fn waitUntilAnyVisible(
|
|
|
231
233
|
return try runner_waits.waitUntilAnyVisible(device, selectors, timeout_ms, writer, options);
|
|
232
234
|
}
|
|
233
235
|
|
|
236
|
+
pub fn assertVisible(
|
|
237
|
+
device: anytype,
|
|
238
|
+
wanted: selector.Selector,
|
|
239
|
+
timeout_ms: u64,
|
|
240
|
+
writer: ?*trace.TraceWriter,
|
|
241
|
+
options: RunOptions,
|
|
242
|
+
) !bool {
|
|
243
|
+
return try runner_waits.assertVisible(device, wanted, timeout_ms, writer, options);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
pub fn assertNotVisible(
|
|
247
|
+
device: anytype,
|
|
248
|
+
wanted: selector.Selector,
|
|
249
|
+
timeout_ms: u64,
|
|
250
|
+
writer: ?*trace.TraceWriter,
|
|
251
|
+
options: RunOptions,
|
|
252
|
+
) !bool {
|
|
253
|
+
return try runner_waits.assertNotVisible(device, wanted, timeout_ms, writer, options);
|
|
254
|
+
}
|
|
255
|
+
|
|
234
256
|
pub fn assertNoneVisible(
|
|
235
257
|
device: anytype,
|
|
236
258
|
selectors: []const selector.Selector,
|
package/src/runner_actions.zig
CHANGED
|
@@ -61,6 +61,16 @@ pub fn typeTextSelector(
|
|
|
61
61
|
if (try runner_native.tryTypeTextSelector(device, wanted, text, writer, options.settle_ms)) return;
|
|
62
62
|
try tapSelector(device, wanted, writer, options);
|
|
63
63
|
try device.typeText(text);
|
|
64
|
+
if (writer) |tw| {
|
|
65
|
+
var payload = std.ArrayList(u8).empty;
|
|
66
|
+
defer payload.deinit(tw.allocator);
|
|
67
|
+
try payload.writer(tw.allocator).writeAll("{\"status\":\"ok\",\"selector\":");
|
|
68
|
+
try trace.writeSelectorJson(payload.writer(tw.allocator), wanted);
|
|
69
|
+
try payload.writer(tw.allocator).writeAll(",\"text\":");
|
|
70
|
+
try trace.writeJsonString(payload.writer(tw.allocator), text);
|
|
71
|
+
try payload.writer(tw.allocator).writeAll("}");
|
|
72
|
+
try tw.recordEvent("ui.type", payload.items);
|
|
73
|
+
}
|
|
64
74
|
try settleDevice(device, options);
|
|
65
75
|
}
|
|
66
76
|
|
|
@@ -10,10 +10,26 @@ pub fn record(
|
|
|
10
10
|
strategy: ?[]const u8,
|
|
11
11
|
selectors: []const selector.Selector,
|
|
12
12
|
snap: types.ObservationSnapshot,
|
|
13
|
+
) !void {
|
|
14
|
+
try recordWithOptions(tw, kind, status, strategy, selectors, snap, .{});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
pub const DiagnosticOptions = struct {
|
|
18
|
+
timeout_ms: ?u64 = null,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
pub fn recordWithOptions(
|
|
22
|
+
tw: *trace.TraceWriter,
|
|
23
|
+
kind: []const u8,
|
|
24
|
+
status: []const u8,
|
|
25
|
+
strategy: ?[]const u8,
|
|
26
|
+
selectors: []const selector.Selector,
|
|
27
|
+
snap: types.ObservationSnapshot,
|
|
28
|
+
options: DiagnosticOptions,
|
|
13
29
|
) !void {
|
|
14
30
|
var payload = std.ArrayList(u8).empty;
|
|
15
31
|
defer payload.deinit(tw.allocator);
|
|
16
|
-
try
|
|
32
|
+
try writeSelectorDiagnosticJsonWithOptions(payload.writer(tw.allocator), status, strategy, selectors, snap, options);
|
|
17
33
|
try tw.recordEvent(kind, payload.items);
|
|
18
34
|
}
|
|
19
35
|
|
|
@@ -23,12 +39,26 @@ pub fn writeSelectorDiagnosticJson(
|
|
|
23
39
|
strategy: ?[]const u8,
|
|
24
40
|
selectors: []const selector.Selector,
|
|
25
41
|
snap: types.ObservationSnapshot,
|
|
42
|
+
) !void {
|
|
43
|
+
try writeSelectorDiagnosticJsonWithOptions(writer, status, strategy, selectors, snap, .{});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
pub fn writeSelectorDiagnosticJsonWithOptions(
|
|
47
|
+
writer: anytype,
|
|
48
|
+
status: []const u8,
|
|
49
|
+
strategy: ?[]const u8,
|
|
50
|
+
selectors: []const selector.Selector,
|
|
51
|
+
snap: types.ObservationSnapshot,
|
|
52
|
+
options: DiagnosticOptions,
|
|
26
53
|
) !void {
|
|
27
54
|
try writer.print("{{\"status\":\"{s}\"", .{status});
|
|
28
55
|
if (strategy) |value| {
|
|
29
56
|
try writer.writeAll(",\"strategy\":");
|
|
30
57
|
try trace.writeJsonString(writer, value);
|
|
31
58
|
}
|
|
59
|
+
if (options.timeout_ms) |timeout_ms| {
|
|
60
|
+
try writer.print(",\"timeoutMs\":{d}", .{timeout_ms});
|
|
61
|
+
}
|
|
32
62
|
try writer.print(",\"snapshotId\":\"{s}\",\"selectors\":[", .{snap.id});
|
|
33
63
|
for (selectors, 0..) |wanted, index| {
|
|
34
64
|
if (index > 0) try writer.writeAll(",");
|
package/src/runner_events.zig
CHANGED
|
@@ -13,21 +13,21 @@ pub fn eventString(allocator: std.mem.Allocator, value: []const u8) ![]const u8
|
|
|
13
13
|
return try buffer.toOwnedSlice(allocator);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
pub fn recordNativeWait(tw: *trace.TraceWriter, kind: []const u8, wanted: selector.Selector, matched_index: ?usize) !void {
|
|
16
|
+
pub fn recordNativeWait(tw: *trace.TraceWriter, kind: []const u8, wanted: selector.Selector, matched_index: ?usize, timeout_ms: u64) !void {
|
|
17
17
|
var payload = std.ArrayList(u8).empty;
|
|
18
18
|
defer payload.deinit(tw.allocator);
|
|
19
19
|
try payload.writer(tw.allocator).writeAll("{\"status\":\"ok\",\"strategy\":\"nativeSelector\"");
|
|
20
20
|
if (matched_index) |index| try payload.writer(tw.allocator).print(",\"matchedIndex\":{d}", .{index});
|
|
21
21
|
try payload.writer(tw.allocator).writeAll(",\"selector\":");
|
|
22
22
|
try trace.writeSelectorJson(payload.writer(tw.allocator), wanted);
|
|
23
|
-
try payload.writer(tw.allocator).
|
|
23
|
+
try payload.writer(tw.allocator).print(",\"timeoutMs\":{d}}}", .{timeout_ms});
|
|
24
24
|
try tw.recordEvent(kind, payload.items);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
pub fn recordNativeWaitTimeout(tw: *trace.TraceWriter, kind: []const u8, selectors: []const selector.Selector) !void {
|
|
27
|
+
pub fn recordNativeWaitTimeout(tw: *trace.TraceWriter, kind: []const u8, selectors: []const selector.Selector, timeout_ms: u64) !void {
|
|
28
28
|
var payload = std.ArrayList(u8).empty;
|
|
29
29
|
defer payload.deinit(tw.allocator);
|
|
30
|
-
try payload.writer(tw.allocator).
|
|
30
|
+
try payload.writer(tw.allocator).print("{{\"status\":\"timeout\",\"strategy\":\"nativeSelector\",\"timeoutMs\":{d},\"selectors\":[", .{timeout_ms});
|
|
31
31
|
for (selectors, 0..) |wanted, index| {
|
|
32
32
|
if (index > 0) try payload.writer(tw.allocator).writeAll(",");
|
|
33
33
|
try trace.writeSelectorJson(payload.writer(tw.allocator), wanted);
|
|
@@ -36,13 +36,28 @@ pub fn recordNativeWaitTimeout(tw: *trace.TraceWriter, kind: []const u8, selecto
|
|
|
36
36
|
try tw.recordEvent(kind, payload.items);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
pub fn recordNativeWaitTimeoutWithDiagnostics(device: anytype, tw: *trace.TraceWriter, kind: []const u8, selectors: []const selector.Selector) !void {
|
|
39
|
+
pub fn recordNativeWaitTimeoutWithDiagnostics(device: anytype, tw: *trace.TraceWriter, kind: []const u8, selectors: []const selector.Selector, timeout_ms: u64) !void {
|
|
40
40
|
var snap = device.snapshot(tw) catch {
|
|
41
|
-
try recordNativeWaitTimeout(tw, kind, selectors);
|
|
41
|
+
try recordNativeWaitTimeout(tw, kind, selectors, timeout_ms);
|
|
42
42
|
return;
|
|
43
43
|
};
|
|
44
44
|
defer snap.deinit(device.allocator);
|
|
45
|
-
try
|
|
45
|
+
try recordDiagnosticWithStrategyAndTimeout(tw, kind, "timeout", "nativeSelector", selectors, snap, timeout_ms);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
pub fn recordSelectorArrayStatus(tw: *trace.TraceWriter, kind: []const u8, status: []const u8, selectors: []const selector.Selector, timeout_ms: u64) !void {
|
|
49
|
+
var payload = std.ArrayList(u8).empty;
|
|
50
|
+
defer payload.deinit(tw.allocator);
|
|
51
|
+
const out = payload.writer(tw.allocator);
|
|
52
|
+
try out.writeAll("{\"status\":");
|
|
53
|
+
try trace.writeJsonString(out, status);
|
|
54
|
+
try out.writeAll(",\"selectors\":[");
|
|
55
|
+
for (selectors, 0..) |wanted, index| {
|
|
56
|
+
if (index > 0) try out.writeAll(",");
|
|
57
|
+
try trace.writeSelectorJson(out, wanted);
|
|
58
|
+
}
|
|
59
|
+
try out.print("],\"timeoutMs\":{d}}}", .{timeout_ms});
|
|
60
|
+
try tw.recordEvent(kind, payload.items);
|
|
46
61
|
}
|
|
47
62
|
|
|
48
63
|
pub fn recordSelectorEvent(tw: *trace.TraceWriter, kind: []const u8, wanted: selector.Selector) !void {
|
|
@@ -72,6 +87,42 @@ pub fn recordActionStatus(tw: *trace.TraceWriter, kind: []const u8, status: []co
|
|
|
72
87
|
try tw.recordEvent(kind, payload.items);
|
|
73
88
|
}
|
|
74
89
|
|
|
90
|
+
pub fn recordSwipe(tw: *trace.TraceWriter, x1: i32, y1: i32, x2: i32, y2: i32, duration_ms: u32) !void {
|
|
91
|
+
const payload = try std.fmt.allocPrint(
|
|
92
|
+
tw.allocator,
|
|
93
|
+
"{{\"status\":\"ok\",\"x1\":{d},\"y1\":{d},\"x2\":{d},\"y2\":{d},\"durationMs\":{d}}}",
|
|
94
|
+
.{ x1, y1, x2, y2, duration_ms },
|
|
95
|
+
);
|
|
96
|
+
defer tw.allocator.free(payload);
|
|
97
|
+
try tw.recordEvent("ui.swipe", payload);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
pub fn recordTraceDiscover(tw: *trace.TraceWriter, status: []const u8, out_path: []const u8, include_actions: bool, validated: bool) !void {
|
|
101
|
+
var payload = std.ArrayList(u8).empty;
|
|
102
|
+
defer payload.deinit(tw.allocator);
|
|
103
|
+
const out = payload.writer(tw.allocator);
|
|
104
|
+
try out.writeAll("{\"status\":");
|
|
105
|
+
try trace.writeJsonString(out, status);
|
|
106
|
+
try out.writeAll(",\"out\":");
|
|
107
|
+
try trace.writeJsonString(out, out_path);
|
|
108
|
+
try out.print(",\"includeActions\":{},\"validated\":{}}}", .{ include_actions, validated });
|
|
109
|
+
try tw.recordEvent("trace.discover", payload.items);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
pub fn recordTraceExplore(tw: *trace.TraceWriter, status: []const u8, out_path: []const u8, goal: []const u8, include_actions: bool, validated: bool) !void {
|
|
113
|
+
var payload = std.ArrayList(u8).empty;
|
|
114
|
+
defer payload.deinit(tw.allocator);
|
|
115
|
+
const out = payload.writer(tw.allocator);
|
|
116
|
+
try out.writeAll("{\"status\":");
|
|
117
|
+
try trace.writeJsonString(out, status);
|
|
118
|
+
try out.writeAll(",\"out\":");
|
|
119
|
+
try trace.writeJsonString(out, out_path);
|
|
120
|
+
try out.writeAll(",\"goal\":");
|
|
121
|
+
try trace.writeJsonString(out, goal);
|
|
122
|
+
try out.print(",\"includeActions\":{},\"validated\":{}}}", .{ include_actions, validated });
|
|
123
|
+
try tw.recordEvent("trace.explore", payload.items);
|
|
124
|
+
}
|
|
125
|
+
|
|
75
126
|
pub fn recordStepError(tw: *trace.TraceWriter, index: usize, err: anyerror) !void {
|
|
76
127
|
const payload = try std.fmt.allocPrint(
|
|
77
128
|
tw.allocator,
|
|
@@ -159,6 +210,18 @@ pub fn recordDiagnosticWithStrategy(
|
|
|
159
210
|
try runner_diagnostics.record(tw, kind, status, strategy, selectors, snap);
|
|
160
211
|
}
|
|
161
212
|
|
|
213
|
+
pub fn recordDiagnosticWithStrategyAndTimeout(
|
|
214
|
+
tw: *trace.TraceWriter,
|
|
215
|
+
kind: []const u8,
|
|
216
|
+
status: []const u8,
|
|
217
|
+
strategy: ?[]const u8,
|
|
218
|
+
selectors: []const selector.Selector,
|
|
219
|
+
snap: types.ObservationSnapshot,
|
|
220
|
+
timeout_ms: u64,
|
|
221
|
+
) !void {
|
|
222
|
+
try runner_diagnostics.recordWithOptions(tw, kind, status, strategy, selectors, snap, .{ .timeout_ms = timeout_ms });
|
|
223
|
+
}
|
|
224
|
+
|
|
162
225
|
pub fn writeSelectorDiagnosticJson(
|
|
163
226
|
writer: anytype,
|
|
164
227
|
status: []const u8,
|
package/src/runner_native.zig
CHANGED
|
@@ -32,7 +32,7 @@ pub fn tryTypeTextSelector(
|
|
|
32
32
|
return err;
|
|
33
33
|
};
|
|
34
34
|
if (!typed) return false;
|
|
35
|
-
if (writer) |tw| try
|
|
35
|
+
if (writer) |tw| try recordSelectorTextAction(tw, "ui.type", wanted, text);
|
|
36
36
|
try device.settle(settle_ms);
|
|
37
37
|
return true;
|
|
38
38
|
}
|
|
@@ -70,6 +70,22 @@ fn recordSelectorAction(
|
|
|
70
70
|
try tw.recordEvent(kind, payload.items);
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
fn recordSelectorTextAction(
|
|
74
|
+
tw: *trace.TraceWriter,
|
|
75
|
+
kind: []const u8,
|
|
76
|
+
wanted: selector.Selector,
|
|
77
|
+
text: []const u8,
|
|
78
|
+
) !void {
|
|
79
|
+
var payload = std.ArrayList(u8).empty;
|
|
80
|
+
defer payload.deinit(tw.allocator);
|
|
81
|
+
try payload.writer(tw.allocator).writeAll("{\"status\":\"ok\",\"strategy\":\"nativeSelector\",\"selector\":");
|
|
82
|
+
try trace.writeSelectorJson(payload.writer(tw.allocator), wanted);
|
|
83
|
+
try payload.writer(tw.allocator).writeAll(",\"text\":");
|
|
84
|
+
try trace.writeJsonString(payload.writer(tw.allocator), text);
|
|
85
|
+
try payload.writer(tw.allocator).writeAll("}");
|
|
86
|
+
try tw.recordEvent(kind, payload.items);
|
|
87
|
+
}
|
|
88
|
+
|
|
73
89
|
fn recordSelectorActionFailure(
|
|
74
90
|
tw: *trace.TraceWriter,
|
|
75
91
|
kind: []const u8,
|