zig-mobile-runner 0.1.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 +484 -0
- package/CONTRIBUTING.md +42 -0
- package/FEATURES.md +112 -0
- package/LICENSE +21 -0
- package/README.md +255 -0
- package/SECURITY.md +34 -0
- package/build.zig +38 -0
- package/build.zig.zon +7 -0
- package/clients/README.md +144 -0
- package/clients/go/README.md +24 -0
- package/clients/go/examples/fake-session/main.go +93 -0
- package/clients/go/go.mod +3 -0
- package/clients/go/zmr/client.go +432 -0
- package/clients/kotlin/README.md +35 -0
- package/clients/kotlin/build.gradle.kts +35 -0
- package/clients/kotlin/settings.gradle.kts +15 -0
- package/clients/kotlin/src/main/kotlin/dev/zmr/FakeSession.kt +86 -0
- package/clients/kotlin/src/main/kotlin/dev/zmr/ZmrClient.kt +67 -0
- package/clients/python/README.md +29 -0
- package/clients/python/examples/fake_session.py +48 -0
- package/clients/python/pyproject.toml +13 -0
- package/clients/python/zmr_client.py +202 -0
- package/clients/rust/Cargo.lock +107 -0
- package/clients/rust/Cargo.toml +10 -0
- package/clients/rust/README.md +19 -0
- package/clients/rust/examples/fake_session.rs +70 -0
- package/clients/rust/src/lib.rs +461 -0
- package/clients/swift/Package.swift +16 -0
- package/clients/swift/README.md +36 -0
- package/clients/swift/Sources/ZMRClient/ZMRClient.swift +114 -0
- package/clients/swift/Sources/ZMRFakeSession/main.swift +86 -0
- package/clients/typescript/README.md +34 -0
- package/clients/typescript/examples/fake-session.mjs +36 -0
- package/clients/typescript/index.d.ts +144 -0
- package/clients/typescript/index.mjs +192 -0
- package/clients/typescript/package.json +8 -0
- package/docs/adr/0001-agent-native-runner-boundary.md +31 -0
- package/docs/adr/0002-app-local-zmr-contract.md +39 -0
- package/docs/adr/0003-ios-simulator-xctest-shim.md +41 -0
- package/docs/adr/0004-benchmark-claims-and-baseline-collection.md +37 -0
- package/docs/adr/README.md +12 -0
- package/docs/ai-agents.md +156 -0
- package/docs/app-integration.md +316 -0
- package/docs/benchmarking.md +275 -0
- package/docs/client-installation.md +141 -0
- package/docs/clients.md +98 -0
- package/docs/config.md +175 -0
- package/docs/demo.md +259 -0
- package/docs/dsl.md +57 -0
- package/docs/install.md +233 -0
- package/docs/market-positioning.md +70 -0
- package/docs/npm.md +359 -0
- package/docs/protocol-fixtures/README.md +8 -0
- package/docs/protocol-fixtures/core-session.requests.jsonl +8 -0
- package/docs/protocol-fixtures/core-session.responses.jsonl +8 -0
- package/docs/protocol-versioning.md +65 -0
- package/docs/protocol.md +560 -0
- package/docs/publication.md +77 -0
- package/docs/release-audit.md +99 -0
- package/docs/release-candidate.md +111 -0
- package/docs/release-evidence.md +188 -0
- package/docs/release-notes-template.md +58 -0
- package/docs/roadmap.md +334 -0
- package/docs/scenario-authoring.md +88 -0
- package/docs/shipping.md +170 -0
- package/docs/trace-privacy.md +88 -0
- package/docs/troubleshooting.md +256 -0
- package/examples/android-app-auth-probe.json +89 -0
- package/examples/android-app-error-state.json +13 -0
- package/examples/android-app-login-smoke.json +192 -0
- package/examples/android-app-onboarding.json +12 -0
- package/examples/android-app-referral-deep-link.json +12 -0
- package/examples/android-shim-smoke.json +19 -0
- package/examples/demo-failure.json +12 -0
- package/examples/demo-fake.json +14 -0
- package/examples/ios-dev-client-open-link.json +26 -0
- package/examples/ios-dev-client-route-snapshot.json +24 -0
- package/examples/ios-shim-smoke.json +23 -0
- package/examples/ios-smoke.json +9 -0
- package/go.work +3 -0
- package/npm/agents.mjs +183 -0
- package/npm/app-config.mjs +95 -0
- package/npm/build-zmr.mjs +21 -0
- package/npm/commands.mjs +104 -0
- package/npm/generated-files.mjs +50 -0
- package/npm/index.mjs +75 -0
- package/npm/init-app.mjs +80 -0
- package/npm/package-scripts.mjs +72 -0
- package/npm/postinstall.mjs +21 -0
- package/npm/scaffold.mjs +179 -0
- package/npm/scenarios.mjs +93 -0
- package/npm/setup.mjs +69 -0
- package/npm/wizard.mjs +117 -0
- package/npm/zmr.mjs +23 -0
- package/package.json +114 -0
- 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 +26 -0
- package/schemas/action-result.schema.json +27 -0
- package/schemas/capabilities-output.schema.json +98 -0
- package/schemas/devices-output.schema.json +25 -0
- package/schemas/doctor-output.schema.json +51 -0
- package/schemas/explain-output.schema.json +51 -0
- package/schemas/import-output.schema.json +23 -0
- package/schemas/init-output.schema.json +71 -0
- package/schemas/json-rpc.schema.json +55 -0
- package/schemas/release-manifest.schema.json +43 -0
- package/schemas/release-readiness-output.schema.json +127 -0
- package/schemas/run-output.schema.json +43 -0
- package/schemas/scenario.schema.json +128 -0
- package/schemas/schemas-output.schema.json +26 -0
- package/schemas/semantic-snapshot.schema.json +116 -0
- package/schemas/snapshot.schema.json +60 -0
- package/schemas/trace-event.schema.json +14 -0
- package/schemas/trace-manifest.schema.json +59 -0
- package/schemas/validate-output.schema.json +42 -0
- package/schemas/version-output.schema.json +23 -0
- package/schemas/zmr-config.schema.json +75 -0
- package/scripts/android-emulator.sh +126 -0
- package/scripts/assert-ios-physical-ready.sh +213 -0
- package/scripts/benchmark-command.sh +307 -0
- package/scripts/benchmark.sh +359 -0
- package/scripts/benchmark_gate.py +117 -0
- package/scripts/benchmark_result_row.py +88 -0
- package/scripts/compare-benchmarks.py +288 -0
- package/scripts/create-android-demo-app.sh +342 -0
- package/scripts/create-ios-demo-app.sh +261 -0
- package/scripts/demo-android-real.sh +232 -0
- package/scripts/demo-ios-real.sh +270 -0
- package/scripts/demo.sh +464 -0
- package/scripts/device-matrix.sh +338 -0
- package/scripts/ensure-ios-shim-target.rb +237 -0
- package/scripts/install-android-shim.sh +281 -0
- package/scripts/install-ios-shim.sh +589 -0
- package/scripts/pilot-gate.sh +560 -0
- package/scripts/release-readiness.py +838 -0
- package/scripts/release-readiness.sh +91 -0
- package/scripts/run-android-pilot.sh +561 -0
- package/scripts/run-ios-pilot.sh +509 -0
- package/shims/android/README.md +21 -0
- package/shims/android/ZMRShimInstrumentedTest.java +152 -0
- package/shims/android/protocol.md +18 -0
- package/shims/ios/README.md +50 -0
- package/shims/ios/ZMRShim.swift +110 -0
- package/shims/ios/ZMRShimUITestCase.swift +475 -0
- package/shims/ios/protocol.md +74 -0
- package/skills/zmr-mobile-testing/SKILL.md +127 -0
- package/src/android.zig +344 -0
- package/src/android_device_info.zig +99 -0
- package/src/android_emulator.zig +154 -0
- package/src/android_screen_recording.zig +112 -0
- package/src/android_shell.zig +112 -0
- package/src/bundle.zig +124 -0
- package/src/bundle_redaction.zig +272 -0
- package/src/bundle_tar.zig +123 -0
- package/src/cli_devices.zig +97 -0
- package/src/cli_doctor.zig +114 -0
- package/src/cli_import.zig +70 -0
- package/src/cli_info.zig +39 -0
- package/src/cli_init.zig +72 -0
- package/src/cli_output.zig +467 -0
- package/src/cli_run.zig +259 -0
- package/src/cli_serve.zig +287 -0
- package/src/cli_trace.zig +111 -0
- package/src/cli_validate.zig +41 -0
- package/src/command.zig +211 -0
- package/src/config.zig +305 -0
- package/src/config_diagnostics.zig +212 -0
- package/src/config_paths.zig +49 -0
- package/src/device_registry.zig +37 -0
- package/src/doctor.zig +412 -0
- package/src/doctor_hints.zig +52 -0
- package/src/errors.zig +55 -0
- package/src/fake_device.zig +163 -0
- package/src/health.zig +28 -0
- package/src/importer.zig +343 -0
- package/src/importer_json.zig +100 -0
- package/src/importer_model.zig +103 -0
- package/src/ios.zig +399 -0
- package/src/ios_devices.zig +219 -0
- package/src/ios_lifecycle.zig +72 -0
- package/src/ios_shim.zig +242 -0
- package/src/ios_snapshot.zig +20 -0
- package/src/json_fields.zig +80 -0
- package/src/json_rpc.zig +150 -0
- package/src/json_rpc_methods.zig +318 -0
- package/src/json_rpc_observation.zig +31 -0
- package/src/json_rpc_params.zig +52 -0
- package/src/json_rpc_protocol.zig +110 -0
- package/src/json_rpc_trace.zig +73 -0
- package/src/main.zig +135 -0
- package/src/mcp.zig +234 -0
- package/src/mcp_protocol.zig +64 -0
- package/src/mcp_trace.zig +83 -0
- package/src/report.zig +346 -0
- package/src/report_html.zig +63 -0
- package/src/report_values.zig +27 -0
- package/src/run_options.zig +152 -0
- package/src/runner.zig +280 -0
- package/src/runner_actions.zig +109 -0
- package/src/runner_config.zig +6 -0
- package/src/runner_diagnostics.zig +268 -0
- package/src/runner_events.zig +170 -0
- package/src/runner_native.zig +88 -0
- package/src/runner_waits.zig +300 -0
- package/src/scaffold.zig +472 -0
- package/src/scenario.zig +346 -0
- package/src/scenario_fields.zig +50 -0
- package/src/schema_registry.zig +53 -0
- package/src/selector.zig +84 -0
- package/src/semantic.zig +171 -0
- package/src/trace.zig +315 -0
- package/src/trace_json.zig +340 -0
- package/src/trace_summary.zig +218 -0
- package/src/trace_summary_diagnostic.zig +202 -0
- package/src/types.zig +120 -0
- package/src/uiautomator.zig +164 -0
- package/src/validation.zig +187 -0
- package/src/version.zig +22 -0
- package/viewer/app.js +373 -0
- package/viewer/index.html +126 -0
- package/viewer/parser.js +233 -0
- package/viewer/styles.css +585 -0
package/src/report.zig
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
const cli_output = @import("cli_output.zig");
|
|
3
|
+
const report_html = @import("report_html.zig");
|
|
4
|
+
const report_values = @import("report_values.zig");
|
|
5
|
+
const trace = @import("trace.zig");
|
|
6
|
+
const trace_summary = @import("trace_summary.zig");
|
|
7
|
+
|
|
8
|
+
pub fn writeHtmlReport(
|
|
9
|
+
allocator: std.mem.Allocator,
|
|
10
|
+
input_path: []const u8,
|
|
11
|
+
out_path: []const u8,
|
|
12
|
+
) !void {
|
|
13
|
+
const results_path = try std.fs.path.join(allocator, &.{ input_path, "results.jsonl" });
|
|
14
|
+
defer allocator.free(results_path);
|
|
15
|
+
|
|
16
|
+
if (std.fs.cwd().openFile(results_path, .{})) |file| {
|
|
17
|
+
file.close();
|
|
18
|
+
return try writeBenchmarkReport(allocator, input_path, results_path, out_path);
|
|
19
|
+
} else |err| switch (err) {
|
|
20
|
+
error.FileNotFound => return try writeTraceReport(allocator, input_path, out_path),
|
|
21
|
+
else => return err,
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
pub fn writeTraceExplanation(
|
|
26
|
+
allocator: std.mem.Allocator,
|
|
27
|
+
trace_dir: []const u8,
|
|
28
|
+
writer: anytype,
|
|
29
|
+
) !void {
|
|
30
|
+
var summary = try trace_summary.read(allocator, trace_dir);
|
|
31
|
+
defer summary.deinit(allocator);
|
|
32
|
+
try writeTraceExplanationText(writer, summary);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
pub fn writeTraceExplanationJson(
|
|
36
|
+
allocator: std.mem.Allocator,
|
|
37
|
+
trace_dir: []const u8,
|
|
38
|
+
writer: anytype,
|
|
39
|
+
) !void {
|
|
40
|
+
var summary = try trace_summary.read(allocator, trace_dir);
|
|
41
|
+
defer summary.deinit(allocator);
|
|
42
|
+
try writeTraceExplanationJsonValue(writer, trace_dir, summary);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fn writeTraceExplanationText(writer: anytype, explanation: trace_summary.Summary) !void {
|
|
46
|
+
try writer.writeAll("ZMR trace explanation\n");
|
|
47
|
+
try writer.writeAll("scenario: ");
|
|
48
|
+
try writer.writeAll(explanation.scenario_name);
|
|
49
|
+
try writer.writeByte('\n');
|
|
50
|
+
if (explanation.app_id) |value| {
|
|
51
|
+
try writer.writeAll("appId: ");
|
|
52
|
+
try writer.writeAll(value);
|
|
53
|
+
try writer.writeByte('\n');
|
|
54
|
+
}
|
|
55
|
+
try writer.writeAll("status: ");
|
|
56
|
+
try writer.writeAll(explanation.status);
|
|
57
|
+
try writer.writeByte('\n');
|
|
58
|
+
if (explanation.duration_ms) |value| try writer.print("durationMs: {d}\n", .{value});
|
|
59
|
+
if (explanation.event_count) |value| try writer.print("events: {d}\n", .{value});
|
|
60
|
+
if (explanation.snapshot_count) |value| try writer.print("snapshots: {d}\n", .{value});
|
|
61
|
+
if (explanation.failed_step_index) |value| try writer.print("failedStepIndex: {d}\n", .{value});
|
|
62
|
+
if (explanation.error_name) |value| {
|
|
63
|
+
try writer.writeAll("error: ");
|
|
64
|
+
try writer.writeAll(value);
|
|
65
|
+
try writer.writeByte('\n');
|
|
66
|
+
}
|
|
67
|
+
if (explanation.diagnostic.kind) |kind| {
|
|
68
|
+
try writer.writeAll("diagnostic: ");
|
|
69
|
+
try writer.writeAll(kind);
|
|
70
|
+
if (explanation.diagnostic.status) |value| {
|
|
71
|
+
try writer.writeByte(' ');
|
|
72
|
+
try writer.writeAll(value);
|
|
73
|
+
}
|
|
74
|
+
try writer.writeByte('\n');
|
|
75
|
+
}
|
|
76
|
+
if (explanation.diagnostic.artifact_status) |value| {
|
|
77
|
+
try writer.writeAll("artifactStatus: ");
|
|
78
|
+
try writer.writeAll(value);
|
|
79
|
+
try writer.writeByte('\n');
|
|
80
|
+
}
|
|
81
|
+
if (explanation.diagnostic.semantic_status) |value| {
|
|
82
|
+
try writer.writeAll("semanticStatus: ");
|
|
83
|
+
try writer.writeAll(value);
|
|
84
|
+
try writer.writeByte('\n');
|
|
85
|
+
}
|
|
86
|
+
if (explanation.diagnostic.snapshot_id) |value| {
|
|
87
|
+
try writer.writeAll("snapshot: ");
|
|
88
|
+
try writer.writeAll(value);
|
|
89
|
+
try writer.writeByte('\n');
|
|
90
|
+
}
|
|
91
|
+
if (explanation.diagnostic.active_package) |value| {
|
|
92
|
+
try writer.writeAll("activePackage: ");
|
|
93
|
+
try writer.writeAll(value);
|
|
94
|
+
try writer.writeByte('\n');
|
|
95
|
+
}
|
|
96
|
+
if (explanation.diagnostic.active_activity) |value| {
|
|
97
|
+
try writer.writeAll("activeActivity: ");
|
|
98
|
+
try writer.writeAll(value);
|
|
99
|
+
try writer.writeByte('\n');
|
|
100
|
+
}
|
|
101
|
+
if (explanation.diagnostic.visible_texts) |value| {
|
|
102
|
+
try writer.writeAll("visibleTexts: ");
|
|
103
|
+
try writer.writeAll(value);
|
|
104
|
+
try writer.writeByte('\n');
|
|
105
|
+
}
|
|
106
|
+
if (explanation.diagnostic.nearest_matches) |value| {
|
|
107
|
+
try writer.writeAll("nearestTextMatches: ");
|
|
108
|
+
try writer.writeAll(value);
|
|
109
|
+
try writer.writeByte('\n');
|
|
110
|
+
}
|
|
111
|
+
if (explanation.last_kind) |value| {
|
|
112
|
+
try writer.writeAll("lastEvent: ");
|
|
113
|
+
try writer.writeAll(value);
|
|
114
|
+
try writer.writeByte('\n');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
fn writeTraceExplanationJsonValue(writer: anytype, trace_dir: []const u8, explanation: trace_summary.Summary) !void {
|
|
119
|
+
try writer.writeAll("{\"ok\":true,\"traceDir\":");
|
|
120
|
+
try trace.writeJsonString(writer, trace_dir);
|
|
121
|
+
try writer.writeAll(",\"scenario\":");
|
|
122
|
+
try trace.writeJsonString(writer, explanation.scenario_name);
|
|
123
|
+
try writer.writeAll(",\"status\":");
|
|
124
|
+
try trace.writeJsonString(writer, explanation.status);
|
|
125
|
+
if (explanation.app_id) |value| {
|
|
126
|
+
try writer.writeAll(",\"appId\":");
|
|
127
|
+
try trace.writeJsonString(writer, value);
|
|
128
|
+
}
|
|
129
|
+
if (explanation.duration_ms) |value| try writer.print(",\"durationMs\":{d}", .{value});
|
|
130
|
+
if (explanation.event_count) |value| try writer.print(",\"eventCount\":{d}", .{value});
|
|
131
|
+
if (explanation.snapshot_count) |value| try writer.print(",\"snapshotCount\":{d}", .{value});
|
|
132
|
+
if (explanation.failed_step_index) |value| try writer.print(",\"failedStepIndex\":{d}", .{value});
|
|
133
|
+
if (explanation.error_name) |value| {
|
|
134
|
+
try writer.writeAll(",\"error\":");
|
|
135
|
+
try trace.writeJsonString(writer, value);
|
|
136
|
+
}
|
|
137
|
+
if (explanation.diagnostic.kind) |_| {
|
|
138
|
+
try writer.writeAll(",\"diagnostic\":");
|
|
139
|
+
try trace_summary.writeDiagnosticJson(writer, explanation.diagnostic);
|
|
140
|
+
}
|
|
141
|
+
if (explanation.last_kind) |value| {
|
|
142
|
+
try writer.writeAll(",\"lastEvent\":");
|
|
143
|
+
try trace.writeJsonString(writer, value);
|
|
144
|
+
}
|
|
145
|
+
try writeTraceExplanationNextCommandsJson(writer, trace_dir);
|
|
146
|
+
try writer.writeAll("}\n");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
fn writeTraceExplanationNextCommandsJson(writer: anytype, trace_dir: []const u8) !void {
|
|
150
|
+
try writer.writeAll(",\"nextCommands\":[\"zmr report ");
|
|
151
|
+
try cli_output.writeShellArgJsonContent(writer, trace_dir);
|
|
152
|
+
try writer.writeAll(" --out ");
|
|
153
|
+
try cli_output.writeJoinedPathShellArgJsonContent(writer, trace_dir, "report.html");
|
|
154
|
+
try writer.writeAll("\",\"zmr export ");
|
|
155
|
+
try cli_output.writeShellArgJsonContent(writer, trace_dir);
|
|
156
|
+
try writer.writeAll(" --out ");
|
|
157
|
+
try cli_output.writePathWithSuffixShellArgJsonContent(writer, trace_dir, ".zmrtrace");
|
|
158
|
+
try writer.writeAll(" --redact\"]");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
fn writeBenchmarkReport(
|
|
162
|
+
allocator: std.mem.Allocator,
|
|
163
|
+
input_path: []const u8,
|
|
164
|
+
results_path: []const u8,
|
|
165
|
+
out_path: []const u8,
|
|
166
|
+
) !void {
|
|
167
|
+
const content = try std.fs.cwd().readFileAlloc(allocator, results_path, 64 * 1024 * 1024);
|
|
168
|
+
defer allocator.free(content);
|
|
169
|
+
|
|
170
|
+
var rows_html = std.ArrayList(u8).empty;
|
|
171
|
+
defer rows_html.deinit(allocator);
|
|
172
|
+
var durations = std.ArrayList(i64).empty;
|
|
173
|
+
defer durations.deinit(allocator);
|
|
174
|
+
|
|
175
|
+
var total: usize = 0;
|
|
176
|
+
var passed: usize = 0;
|
|
177
|
+
var failed: usize = 0;
|
|
178
|
+
|
|
179
|
+
var lines = std.mem.splitScalar(u8, content, '\n');
|
|
180
|
+
while (lines.next()) |raw_line| {
|
|
181
|
+
const line = std.mem.trim(u8, raw_line, " \t\r\n");
|
|
182
|
+
if (line.len == 0) continue;
|
|
183
|
+
|
|
184
|
+
const parsed = try std.json.parseFromSlice(std.json.Value, allocator, line, .{});
|
|
185
|
+
defer parsed.deinit();
|
|
186
|
+
if (parsed.value != .object) continue;
|
|
187
|
+
const object = parsed.value.object;
|
|
188
|
+
|
|
189
|
+
const tool = report_values.stringField(object, "tool") orelse "";
|
|
190
|
+
const status = report_values.stringField(object, "status") orelse "";
|
|
191
|
+
const trace_status = report_values.stringField(object, "traceStatus") orelse "";
|
|
192
|
+
const trace_error = report_values.stringField(object, "traceError") orelse "";
|
|
193
|
+
const trace_dir = report_values.stringField(object, "traceDir") orelse "";
|
|
194
|
+
const run = report_values.intField(object, "run") orelse 0;
|
|
195
|
+
const duration_ms = report_values.intField(object, "durationMs") orelse 0;
|
|
196
|
+
const failed_step = report_values.intField(object, "failedStepIndex");
|
|
197
|
+
|
|
198
|
+
total += 1;
|
|
199
|
+
if (duration_ms >= 0) try durations.append(allocator, duration_ms);
|
|
200
|
+
const row_passed = std.mem.eql(u8, status, "ok") and (trace_status.len == 0 or std.mem.eql(u8, trace_status, "passed"));
|
|
201
|
+
if (row_passed) {
|
|
202
|
+
passed += 1;
|
|
203
|
+
} else {
|
|
204
|
+
failed += 1;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const writer = rows_html.writer(allocator);
|
|
208
|
+
try writer.writeAll("<tr><td>");
|
|
209
|
+
try writer.print("{d}", .{run});
|
|
210
|
+
try writer.writeAll("</td><td>");
|
|
211
|
+
try report_html.escape(writer, tool);
|
|
212
|
+
try writer.writeAll("</td><td class=\"");
|
|
213
|
+
try writer.writeAll(if (row_passed) "ok" else "failed");
|
|
214
|
+
try writer.writeAll("\">");
|
|
215
|
+
try report_html.escape(writer, status);
|
|
216
|
+
try writer.writeAll("</td><td>");
|
|
217
|
+
try writer.print("{d}", .{duration_ms});
|
|
218
|
+
try writer.writeAll("</td><td>");
|
|
219
|
+
try report_html.escape(writer, trace_status);
|
|
220
|
+
try writer.writeAll("</td><td>");
|
|
221
|
+
if (failed_step) |index| {
|
|
222
|
+
try writer.print("failedStepIndex={d}", .{index});
|
|
223
|
+
}
|
|
224
|
+
if (trace_error.len > 0) {
|
|
225
|
+
if (failed_step != null) try writer.writeAll(" ");
|
|
226
|
+
try report_html.escape(writer, trace_error);
|
|
227
|
+
}
|
|
228
|
+
try writer.writeAll("</td><td>");
|
|
229
|
+
if (trace_dir.len > 0) {
|
|
230
|
+
const events_path = try std.fs.path.join(allocator, &.{ trace_dir, "events.jsonl" });
|
|
231
|
+
defer allocator.free(events_path);
|
|
232
|
+
try report_html.writeArtifactLink(allocator, writer, events_path, "events.jsonl");
|
|
233
|
+
}
|
|
234
|
+
try writer.writeAll("</td></tr>\n");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
std.mem.sort(i64, durations.items, {}, std.sort.asc(i64));
|
|
238
|
+
const mean = report_values.meanDuration(durations.items);
|
|
239
|
+
const p95 = report_values.percentile95(durations.items);
|
|
240
|
+
|
|
241
|
+
var html = std.ArrayList(u8).empty;
|
|
242
|
+
defer html.deinit(allocator);
|
|
243
|
+
const writer = html.writer(allocator);
|
|
244
|
+
try report_html.writeStart(writer, "ZMR Report");
|
|
245
|
+
try writer.writeAll("<h1>ZMR Report</h1>\n");
|
|
246
|
+
try writer.writeAll("<p class=\"muted\">Source: ");
|
|
247
|
+
try report_html.escape(writer, input_path);
|
|
248
|
+
try writer.writeAll("</p>\n");
|
|
249
|
+
try writer.writeAll("<section><h2>Benchmark Summary</h2><dl>");
|
|
250
|
+
try writer.print("<dt>Pass Rate</dt><dd>{d}/{d}</dd>", .{ passed, total });
|
|
251
|
+
try writer.print("<dt>Failures</dt><dd>{d}</dd>", .{failed});
|
|
252
|
+
try writer.print("<dt>Mean</dt><dd>{d}ms</dd>", .{mean});
|
|
253
|
+
try writer.print("<dt>P95</dt><dd>{d}ms</dd>", .{p95});
|
|
254
|
+
try writer.writeAll("</dl></section>\n");
|
|
255
|
+
try writer.writeAll("<section><h2>Runs</h2><table><thead><tr><th>Run</th><th>Tool</th><th>Status</th><th>Duration</th><th>Trace Status</th><th>Failure</th><th>Artifacts</th></tr></thead><tbody>\n");
|
|
256
|
+
try writer.writeAll(rows_html.items);
|
|
257
|
+
try writer.writeAll("</tbody></table></section>\n");
|
|
258
|
+
try writer.writeAll("<p class=\"warning\">Screenshots and raw UI XML may contain app data. Sanitize trace bundles before public sharing.</p>\n");
|
|
259
|
+
try report_html.writeEnd(writer);
|
|
260
|
+
try report_html.writeFile(out_path, html.items);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
fn writeTraceReport(
|
|
264
|
+
allocator: std.mem.Allocator,
|
|
265
|
+
input_path: []const u8,
|
|
266
|
+
out_path: []const u8,
|
|
267
|
+
) !void {
|
|
268
|
+
const events_path = try std.fs.path.join(allocator, &.{ input_path, "events.jsonl" });
|
|
269
|
+
defer allocator.free(events_path);
|
|
270
|
+
const content = try std.fs.cwd().readFileAlloc(allocator, events_path, 64 * 1024 * 1024);
|
|
271
|
+
defer allocator.free(content);
|
|
272
|
+
|
|
273
|
+
var events_html = std.ArrayList(u8).empty;
|
|
274
|
+
defer events_html.deinit(allocator);
|
|
275
|
+
var total: usize = 0;
|
|
276
|
+
var terminal_status: ?[]u8 = null;
|
|
277
|
+
defer if (terminal_status) |value| allocator.free(value);
|
|
278
|
+
var terminal_error: ?[]u8 = null;
|
|
279
|
+
defer if (terminal_error) |value| allocator.free(value);
|
|
280
|
+
|
|
281
|
+
var lines = std.mem.splitScalar(u8, content, '\n');
|
|
282
|
+
while (lines.next()) |raw_line| {
|
|
283
|
+
const line = std.mem.trim(u8, raw_line, " \t\r\n");
|
|
284
|
+
if (line.len == 0) continue;
|
|
285
|
+
|
|
286
|
+
const parsed = try std.json.parseFromSlice(std.json.Value, allocator, line, .{});
|
|
287
|
+
defer parsed.deinit();
|
|
288
|
+
if (parsed.value != .object) continue;
|
|
289
|
+
const object = parsed.value.object;
|
|
290
|
+
const seq = report_values.intField(object, "seq") orelse @as(i64, @intCast(total + 1));
|
|
291
|
+
const kind = report_values.stringField(object, "kind") orelse "";
|
|
292
|
+
if (std.mem.eql(u8, kind, "scenario.end")) {
|
|
293
|
+
if (object.get("payload")) |payload| {
|
|
294
|
+
if (payload == .object) {
|
|
295
|
+
if (report_values.stringField(payload.object, "status")) |value| {
|
|
296
|
+
if (terminal_status) |old| allocator.free(old);
|
|
297
|
+
terminal_status = try allocator.dupe(u8, value);
|
|
298
|
+
}
|
|
299
|
+
if (report_values.stringField(payload.object, "error")) |value| {
|
|
300
|
+
if (terminal_error) |old| allocator.free(old);
|
|
301
|
+
terminal_error = try allocator.dupe(u8, value);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
total += 1;
|
|
308
|
+
const writer = events_html.writer(allocator);
|
|
309
|
+
try writer.writeAll("<tr><td>");
|
|
310
|
+
try writer.print("{d}", .{seq});
|
|
311
|
+
try writer.writeAll("</td><td>");
|
|
312
|
+
try report_html.escape(writer, kind);
|
|
313
|
+
try writer.writeAll("</td><td><code>");
|
|
314
|
+
try report_html.escape(writer, line);
|
|
315
|
+
try writer.writeAll("</code></td></tr>\n");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
var html = std.ArrayList(u8).empty;
|
|
319
|
+
defer html.deinit(allocator);
|
|
320
|
+
const writer = html.writer(allocator);
|
|
321
|
+
try report_html.writeStart(writer, "ZMR Trace Report");
|
|
322
|
+
try writer.writeAll("<h1>ZMR Trace Report</h1>\n");
|
|
323
|
+
try writer.writeAll("<p class=\"muted\">Source: ");
|
|
324
|
+
try report_html.escape(writer, input_path);
|
|
325
|
+
try writer.writeAll("</p>\n");
|
|
326
|
+
try writer.writeAll("<section><h2>Trace Summary</h2><dl>");
|
|
327
|
+
try writer.print("<dt>Events</dt><dd>{d}</dd>", .{total});
|
|
328
|
+
try writer.writeAll("<dt>Terminal Status</dt><dd>");
|
|
329
|
+
try report_html.escape(writer, terminal_status orelse "");
|
|
330
|
+
try writer.writeAll("</dd><dt>Error</dt><dd>");
|
|
331
|
+
try report_html.escape(writer, terminal_error orelse "");
|
|
332
|
+
try writer.writeAll("</dd></dl></section>\n");
|
|
333
|
+
try writer.writeAll("<section><h2>Timeline</h2><table><thead><tr><th>Seq</th><th>Kind</th><th>Event</th></tr></thead><tbody>\n");
|
|
334
|
+
try writer.writeAll(events_html.items);
|
|
335
|
+
try writer.writeAll("</tbody></table></section>\n");
|
|
336
|
+
try writer.writeAll("<p>");
|
|
337
|
+
try report_html.writeArtifactLink(allocator, writer, events_path, "events.jsonl");
|
|
338
|
+
try writer.writeAll("</p>\n");
|
|
339
|
+
try writer.writeAll("<p class=\"warning\">Screenshots and raw UI XML may contain app data. Sanitize trace bundles before public sharing.</p>\n");
|
|
340
|
+
try report_html.writeEnd(writer);
|
|
341
|
+
try report_html.writeFile(out_path, html.items);
|
|
342
|
+
|
|
343
|
+
const relative_report_path = std.fs.path.relative(allocator, input_path, out_path) catch try allocator.dupe(u8, out_path);
|
|
344
|
+
defer allocator.free(relative_report_path);
|
|
345
|
+
try trace.attachReportPath(allocator, input_path, relative_report_path);
|
|
346
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
|
|
3
|
+
pub fn writeStart(writer: anytype, title: []const u8) !void {
|
|
4
|
+
try writer.writeAll("<!doctype html><html><head><meta charset=\"utf-8\"><title>");
|
|
5
|
+
try escape(writer, title);
|
|
6
|
+
try writer.writeAll(
|
|
7
|
+
\\</title><style>
|
|
8
|
+
\\body{font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;margin:32px;color:#17202a;background:#f7f8fa}
|
|
9
|
+
\\h1,h2{color:#111827}
|
|
10
|
+
\\section{margin:24px 0}
|
|
11
|
+
\\dl{display:grid;grid-template-columns:max-content 1fr;gap:8px 16px}
|
|
12
|
+
\\dt{font-weight:700}
|
|
13
|
+
\\table{border-collapse:collapse;width:100%;background:#fff}
|
|
14
|
+
\\th,td{border:1px solid #d8dee6;padding:8px;text-align:left;vertical-align:top}
|
|
15
|
+
\\th{background:#eef2f7}
|
|
16
|
+
\\.ok{color:#116329;font-weight:700}
|
|
17
|
+
\\.failed{color:#b42318;font-weight:700}
|
|
18
|
+
\\.muted{color:#667085}
|
|
19
|
+
\\.warning{border-left:4px solid #b54708;background:#fff7ed;padding:12px}
|
|
20
|
+
\\code{white-space:pre-wrap;word-break:break-word}
|
|
21
|
+
\\</style></head><body>
|
|
22
|
+
\\
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
pub fn writeEnd(writer: anytype) !void {
|
|
27
|
+
try writer.writeAll("</body></html>\n");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
pub fn writeFile(path: []const u8, bytes: []const u8) !void {
|
|
31
|
+
var file = try std.fs.cwd().createFile(path, .{ .truncate = true });
|
|
32
|
+
defer file.close();
|
|
33
|
+
try file.writeAll(bytes);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
pub fn writeArtifactLink(
|
|
37
|
+
allocator: std.mem.Allocator,
|
|
38
|
+
writer: anytype,
|
|
39
|
+
path: []const u8,
|
|
40
|
+
label: []const u8,
|
|
41
|
+
) !void {
|
|
42
|
+
const href = std.fs.cwd().realpathAlloc(allocator, path) catch try allocator.dupe(u8, path);
|
|
43
|
+
defer allocator.free(href);
|
|
44
|
+
|
|
45
|
+
try writer.writeAll("<a href=\"file://");
|
|
46
|
+
try escape(writer, href);
|
|
47
|
+
try writer.writeAll("\">");
|
|
48
|
+
try escape(writer, label);
|
|
49
|
+
try writer.writeAll("</a>");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
pub fn escape(writer: anytype, value: []const u8) !void {
|
|
53
|
+
for (value) |ch| {
|
|
54
|
+
switch (ch) {
|
|
55
|
+
'&' => try writer.writeAll("&"),
|
|
56
|
+
'<' => try writer.writeAll("<"),
|
|
57
|
+
'>' => try writer.writeAll(">"),
|
|
58
|
+
'"' => try writer.writeAll("""),
|
|
59
|
+
'\'' => try writer.writeAll("'"),
|
|
60
|
+
else => try writer.writeByte(ch),
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
|
|
3
|
+
pub fn stringField(object: std.json.ObjectMap, key: []const u8) ?[]const u8 {
|
|
4
|
+
const value = object.get(key) orelse return null;
|
|
5
|
+
if (value != .string) return null;
|
|
6
|
+
return value.string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
pub fn intField(object: std.json.ObjectMap, key: []const u8) ?i64 {
|
|
10
|
+
const value = object.get(key) orelse return null;
|
|
11
|
+
return switch (value) {
|
|
12
|
+
.integer => |actual| actual,
|
|
13
|
+
else => null,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
pub fn meanDuration(durations: []const i64) i64 {
|
|
18
|
+
if (durations.len == 0) return 0;
|
|
19
|
+
var total: i64 = 0;
|
|
20
|
+
for (durations) |duration| total += duration;
|
|
21
|
+
return @divTrunc(total, @as(i64, @intCast(durations.len)));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
pub fn percentile95(durations: []const i64) i64 {
|
|
25
|
+
if (durations.len == 0) return 0;
|
|
26
|
+
return durations[@as(usize, @intFromFloat(@floor(@as(f64, @floatFromInt(durations.len - 1)) * 0.95)))];
|
|
27
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
const android_emulator = @import("android_emulator.zig");
|
|
3
|
+
const config = @import("config.zig");
|
|
4
|
+
const trace = @import("trace.zig");
|
|
5
|
+
|
|
6
|
+
pub const Platform = enum {
|
|
7
|
+
android,
|
|
8
|
+
ios,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
pub const IosDeviceType = enum {
|
|
12
|
+
simulator,
|
|
13
|
+
physical,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
pub const RawRunOptions = struct {
|
|
17
|
+
scenario_path: ?[]const u8 = null,
|
|
18
|
+
serial: ?[]const u8 = null,
|
|
19
|
+
trace_dir: ?[]const u8 = null,
|
|
20
|
+
app_id: ?[]const u8 = null,
|
|
21
|
+
android_shim_path: ?[]const u8 = null,
|
|
22
|
+
ios_shim_path: ?[]const u8 = null,
|
|
23
|
+
screen_recording: ?bool = null,
|
|
24
|
+
android_avd_name: ?[]const u8 = null,
|
|
25
|
+
android_restore_snapshot: ?[]const u8 = null,
|
|
26
|
+
android_create_avd_if_missing: ?bool = null,
|
|
27
|
+
android_avd_system_image: ?[]const u8 = null,
|
|
28
|
+
android_avd_device_profile: ?[]const u8 = null,
|
|
29
|
+
android_reset_before_run: ?bool = null,
|
|
30
|
+
android_wait_ready: ?bool = null,
|
|
31
|
+
platform: Platform = .android,
|
|
32
|
+
ios_device_type: IosDeviceType = .simulator,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
pub const ResolvedRunOptions = struct {
|
|
36
|
+
scenario_path: ?[]const u8,
|
|
37
|
+
serial: ?[]const u8,
|
|
38
|
+
trace_dir: ?[]const u8,
|
|
39
|
+
app_id: []const u8,
|
|
40
|
+
android_shim_path: ?[]const u8,
|
|
41
|
+
ios_shim_path: ?[]const u8,
|
|
42
|
+
android_avd_name: ?[]const u8,
|
|
43
|
+
android_restore_snapshot: ?[]const u8,
|
|
44
|
+
android_create_avd_if_missing: bool,
|
|
45
|
+
android_avd_system_image: ?[]const u8,
|
|
46
|
+
android_avd_device_profile: ?[]const u8,
|
|
47
|
+
android_reset_before_run: bool,
|
|
48
|
+
android_wait_ready: bool,
|
|
49
|
+
platform: Platform,
|
|
50
|
+
ios_device_type: IosDeviceType,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
pub const RawServeOptions = struct {
|
|
54
|
+
serial: ?[]const u8 = null,
|
|
55
|
+
app_id: ?[]const u8 = null,
|
|
56
|
+
trace_dir: ?[]const u8 = null,
|
|
57
|
+
android_shim_path: ?[]const u8 = null,
|
|
58
|
+
ios_shim_path: ?[]const u8 = null,
|
|
59
|
+
platform: Platform = .android,
|
|
60
|
+
ios_device_type: IosDeviceType = .simulator,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
pub const ResolvedServeOptions = struct {
|
|
64
|
+
serial: ?[]const u8,
|
|
65
|
+
app_id: []const u8,
|
|
66
|
+
trace_dir: ?[]const u8,
|
|
67
|
+
android_shim_path: ?[]const u8,
|
|
68
|
+
ios_shim_path: ?[]const u8,
|
|
69
|
+
platform: Platform,
|
|
70
|
+
ios_device_type: IosDeviceType,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
pub fn resolveRun(raw: RawRunOptions, cfg: ?config.Config) ResolvedRunOptions {
|
|
74
|
+
const platform_cfg = platformConfigFor(raw.platform, cfg);
|
|
75
|
+
return .{
|
|
76
|
+
.scenario_path = raw.scenario_path orelse if (platform_cfg) |pc| pc.smoke_scenario else null,
|
|
77
|
+
.serial = raw.serial orelse if (platform_cfg) |pc| pc.default_device else null,
|
|
78
|
+
.trace_dir = raw.trace_dir orelse if (platform_cfg) |pc| pc.trace_dir else null,
|
|
79
|
+
.app_id = raw.app_id orelse if (cfg) |value| value.app_id orelse "com.example.mobiletest" else "com.example.mobiletest",
|
|
80
|
+
.android_shim_path = raw.android_shim_path orelse if (cfg) |value| value.tools.android_shim_path else null,
|
|
81
|
+
.ios_shim_path = raw.ios_shim_path orelse if (cfg) |value| value.tools.ios_shim_path else null,
|
|
82
|
+
.android_avd_name = raw.android_avd_name orelse if (platform_cfg) |pc| pc.avd_name else null,
|
|
83
|
+
.android_restore_snapshot = raw.android_restore_snapshot orelse if (platform_cfg) |pc| pc.restore_snapshot else null,
|
|
84
|
+
.android_create_avd_if_missing = raw.android_create_avd_if_missing orelse if (platform_cfg) |pc| pc.create_avd_if_missing else false,
|
|
85
|
+
.android_avd_system_image = raw.android_avd_system_image orelse if (platform_cfg) |pc| pc.avd_system_image else null,
|
|
86
|
+
.android_avd_device_profile = raw.android_avd_device_profile orelse if (platform_cfg) |pc| pc.avd_device_profile else null,
|
|
87
|
+
.android_reset_before_run = raw.android_reset_before_run orelse if (platform_cfg) |pc| pc.reset_before_run else false,
|
|
88
|
+
.android_wait_ready = raw.android_wait_ready orelse if (platform_cfg) |pc| pc.wait_ready else false,
|
|
89
|
+
.platform = raw.platform,
|
|
90
|
+
.ios_device_type = raw.ios_device_type,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
pub fn resolveServe(raw: RawServeOptions, cfg: ?config.Config) ResolvedServeOptions {
|
|
95
|
+
const platform_cfg = platformConfigFor(raw.platform, cfg);
|
|
96
|
+
return .{
|
|
97
|
+
.serial = raw.serial orelse if (platform_cfg) |pc| pc.default_device else null,
|
|
98
|
+
.app_id = raw.app_id orelse if (cfg) |value| value.app_id orelse "com.example.mobiletest" else "com.example.mobiletest",
|
|
99
|
+
.trace_dir = raw.trace_dir orelse if (platform_cfg) |pc| pc.trace_dir else null,
|
|
100
|
+
.android_shim_path = raw.android_shim_path orelse if (cfg) |value| value.tools.android_shim_path else null,
|
|
101
|
+
.ios_shim_path = raw.ios_shim_path orelse if (cfg) |value| value.tools.ios_shim_path else null,
|
|
102
|
+
.platform = raw.platform,
|
|
103
|
+
.ios_device_type = raw.ios_device_type,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
pub fn androidPreflight(
|
|
108
|
+
resolved: ResolvedRunOptions,
|
|
109
|
+
adb_path: []const u8,
|
|
110
|
+
emulator_path: []const u8,
|
|
111
|
+
avdmanager_path: []const u8,
|
|
112
|
+
) ?android_emulator.PreflightOptions {
|
|
113
|
+
const options = android_emulator.PreflightOptions{
|
|
114
|
+
.adb_path = adb_path,
|
|
115
|
+
.emulator_path = emulator_path,
|
|
116
|
+
.avdmanager_path = avdmanager_path,
|
|
117
|
+
.device_serial = resolved.serial,
|
|
118
|
+
.avd_name = resolved.android_avd_name,
|
|
119
|
+
.restore_snapshot = resolved.android_restore_snapshot,
|
|
120
|
+
.create_avd_if_missing = resolved.android_create_avd_if_missing,
|
|
121
|
+
.avd_system_image = resolved.android_avd_system_image,
|
|
122
|
+
.avd_device_profile = resolved.android_avd_device_profile,
|
|
123
|
+
.reset_before_run = resolved.android_reset_before_run,
|
|
124
|
+
.wait_ready = resolved.android_wait_ready,
|
|
125
|
+
};
|
|
126
|
+
return if (android_emulator.hasWork(options)) options else null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
pub fn traceCapture(cfg: config.Config) trace.CaptureOptions {
|
|
130
|
+
return .{
|
|
131
|
+
.capture_screenshots = cfg.artifacts.screenshots,
|
|
132
|
+
.capture_hierarchy = cfg.artifacts.hierarchy,
|
|
133
|
+
.capture_logs = cfg.artifacts.logs,
|
|
134
|
+
.capture_screen_recording = cfg.artifacts.screen_recording,
|
|
135
|
+
.redaction = .{
|
|
136
|
+
.denylist_text = cfg.redaction.denylist_text,
|
|
137
|
+
.allowlist_text = cfg.redaction.allowlist_text,
|
|
138
|
+
.denylist_resource_ids = cfg.redaction.denylist_resource_ids,
|
|
139
|
+
.allowlist_resource_ids = cfg.redaction.allowlist_resource_ids,
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
fn platformConfigFor(platform: Platform, cfg: ?config.Config) ?config.PlatformConfig {
|
|
145
|
+
if (cfg) |value| {
|
|
146
|
+
return switch (platform) {
|
|
147
|
+
.android => value.android,
|
|
148
|
+
.ios => value.ios,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
}
|