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/semantic.zig
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
const trace = @import("trace.zig");
|
|
3
|
+
const types = @import("types.zig");
|
|
4
|
+
|
|
5
|
+
pub fn roleForNode(node: types.UiNode) []const u8 {
|
|
6
|
+
if (classContains(node.class_name, "Button")) return "button";
|
|
7
|
+
if (classContains(node.class_name, "EditText") or classContains(node.class_name, "TextField") or classContains(node.class_name, "SecureTextField")) return "textbox";
|
|
8
|
+
if (classContains(node.class_name, "Switch")) return "switch";
|
|
9
|
+
if (classContains(node.class_name, "CheckBox") or classContains(node.class_name, "Checkbox")) return "checkbox";
|
|
10
|
+
if (classContains(node.class_name, "RadioButton")) return "radio";
|
|
11
|
+
if (classContains(node.class_name, "Image")) return "image";
|
|
12
|
+
if (classContains(node.class_name, "StaticText") or classContains(node.class_name, "TextView") or classContains(node.class_name, "Text")) return "text";
|
|
13
|
+
if (node.content_desc != null or node.text != null) return "text";
|
|
14
|
+
return "node";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
pub fn accessibleName(node: types.UiNode) []const u8 {
|
|
18
|
+
if (node.content_desc) |value| {
|
|
19
|
+
if (value.len > 0) return value;
|
|
20
|
+
}
|
|
21
|
+
if (node.text) |value| {
|
|
22
|
+
if (value.len > 0) return value;
|
|
23
|
+
}
|
|
24
|
+
if (node.resource_id) |value| {
|
|
25
|
+
if (value.len > 0) return value;
|
|
26
|
+
}
|
|
27
|
+
return node.stable_id;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
pub fn recommendedAction(node: types.UiNode) ?[]const u8 {
|
|
31
|
+
if (!node.visible or !node.enabled) return null;
|
|
32
|
+
const role = roleForNode(node);
|
|
33
|
+
if (std.mem.eql(u8, role, "textbox")) return "type";
|
|
34
|
+
if (std.mem.eql(u8, role, "button") or
|
|
35
|
+
std.mem.eql(u8, role, "switch") or
|
|
36
|
+
std.mem.eql(u8, role, "checkbox") or
|
|
37
|
+
std.mem.eql(u8, role, "radio"))
|
|
38
|
+
{
|
|
39
|
+
return "tap";
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
pub fn isInteractive(node: types.UiNode) bool {
|
|
45
|
+
return recommendedAction(node) != null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
pub fn writeSemanticSnapshotJson(writer: anytype, snapshot: types.ObservationSnapshot) !void {
|
|
49
|
+
try writer.writeAll("{");
|
|
50
|
+
try writer.writeAll("\"id\":");
|
|
51
|
+
try trace.writeJsonString(writer, snapshot.id);
|
|
52
|
+
try writer.print(",\"timestampMs\":{d}", .{snapshot.timestamp_ms});
|
|
53
|
+
try writer.print(
|
|
54
|
+
",\"viewport\":{{\"width\":{d},\"height\":{d}}}",
|
|
55
|
+
.{ snapshot.viewport.width, snapshot.viewport.height },
|
|
56
|
+
);
|
|
57
|
+
try writeNullableField(writer, "activePackage", snapshot.active_package);
|
|
58
|
+
try writeNullableField(writer, "activeActivity", snapshot.active_activity);
|
|
59
|
+
try writeNullableField(writer, "focusedNodeId", snapshot.focused_node_id);
|
|
60
|
+
try writer.writeAll(",\"nodes\":[");
|
|
61
|
+
|
|
62
|
+
var interactive_count: usize = 0;
|
|
63
|
+
for (snapshot.nodes, 0..) |node, index| {
|
|
64
|
+
if (index > 0) try writer.writeAll(",");
|
|
65
|
+
if (isInteractive(node)) interactive_count += 1;
|
|
66
|
+
try writeSemanticNodeJson(writer, node);
|
|
67
|
+
}
|
|
68
|
+
try writer.writeAll("],\"summary\":{");
|
|
69
|
+
try writer.print("\"nodeCount\":{d},\"interactiveCount\":{d},\"visibleText\":[", .{ snapshot.nodes.len, interactive_count });
|
|
70
|
+
var first_text = true;
|
|
71
|
+
for (snapshot.nodes) |node| {
|
|
72
|
+
if (!node.visible) continue;
|
|
73
|
+
const text = visibleLabel(node) orelse continue;
|
|
74
|
+
if (text.len == 0) continue;
|
|
75
|
+
if (!first_text) try writer.writeAll(",");
|
|
76
|
+
first_text = false;
|
|
77
|
+
try trace.writeJsonString(writer, text);
|
|
78
|
+
}
|
|
79
|
+
try writer.writeAll("]}}");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
fn writeSemanticNodeJson(writer: anytype, node: types.UiNode) !void {
|
|
83
|
+
try writer.writeAll("{\"id\":");
|
|
84
|
+
try trace.writeJsonString(writer, node.stable_id);
|
|
85
|
+
try writer.writeAll(",\"role\":");
|
|
86
|
+
try trace.writeJsonString(writer, roleForNode(node));
|
|
87
|
+
try writer.writeAll(",\"name\":");
|
|
88
|
+
try trace.writeJsonString(writer, accessibleName(node));
|
|
89
|
+
try writer.writeAll(",\"selector\":");
|
|
90
|
+
try writeBestSelectorJson(writer, node);
|
|
91
|
+
try writer.writeAll(",\"source\":{");
|
|
92
|
+
try writer.writeAll("\"className\":");
|
|
93
|
+
try trace.writeJsonString(writer, node.class_name);
|
|
94
|
+
try writeNullableField(writer, "resourceId", node.resource_id);
|
|
95
|
+
try writeNullableField(writer, "text", node.text);
|
|
96
|
+
try writeNullableField(writer, "contentDesc", node.content_desc);
|
|
97
|
+
try writer.writeAll("},\"bounds\":");
|
|
98
|
+
try writeBoundsJson(writer, node.bounds);
|
|
99
|
+
try writer.print(
|
|
100
|
+
",\"enabled\":{},\"visible\":{},\"selected\":{},\"interactive\":{}",
|
|
101
|
+
.{ node.enabled, node.visible, node.selected, isInteractive(node) },
|
|
102
|
+
);
|
|
103
|
+
try writer.writeAll(",\"recommendedAction\":");
|
|
104
|
+
if (recommendedAction(node)) |action| {
|
|
105
|
+
try trace.writeJsonString(writer, action);
|
|
106
|
+
} else {
|
|
107
|
+
try writer.writeAll("null");
|
|
108
|
+
}
|
|
109
|
+
try writer.writeAll("}");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
fn writeBestSelectorJson(writer: anytype, node: types.UiNode) !void {
|
|
113
|
+
try writer.writeAll("{");
|
|
114
|
+
if (node.resource_id) |value| {
|
|
115
|
+
if (value.len > 0) {
|
|
116
|
+
try writer.writeAll("\"resourceId\":");
|
|
117
|
+
try trace.writeJsonString(writer, value);
|
|
118
|
+
try writer.writeAll("}");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (node.content_desc) |value| {
|
|
123
|
+
if (value.len > 0) {
|
|
124
|
+
try writer.writeAll("\"contentDesc\":");
|
|
125
|
+
try trace.writeJsonString(writer, value);
|
|
126
|
+
try writer.writeAll("}");
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (node.text) |value| {
|
|
131
|
+
if (value.len > 0) {
|
|
132
|
+
try writer.writeAll("\"text\":");
|
|
133
|
+
try trace.writeJsonString(writer, value);
|
|
134
|
+
try writer.writeAll("}");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
try writer.writeAll("\"stableId\":");
|
|
139
|
+
try trace.writeJsonString(writer, node.stable_id);
|
|
140
|
+
try writer.writeAll("}");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
fn writeBoundsJson(writer: anytype, bounds: types.Bounds) !void {
|
|
144
|
+
try writer.print(
|
|
145
|
+
"{{\"x\":{d},\"y\":{d},\"width\":{d},\"height\":{d},\"centerX\":{d},\"centerY\":{d}}}",
|
|
146
|
+
.{ bounds.x, bounds.y, bounds.width, bounds.height, bounds.centerX(), bounds.centerY() },
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
fn writeNullableField(writer: anytype, key: []const u8, value: ?[]const u8) !void {
|
|
151
|
+
try writer.print(",\"{s}\":", .{key});
|
|
152
|
+
if (value) |actual| {
|
|
153
|
+
try trace.writeJsonString(writer, actual);
|
|
154
|
+
} else {
|
|
155
|
+
try writer.writeAll("null");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
fn visibleLabel(node: types.UiNode) ?[]const u8 {
|
|
160
|
+
if (node.text) |value| {
|
|
161
|
+
if (value.len > 0) return value;
|
|
162
|
+
}
|
|
163
|
+
if (node.content_desc) |value| {
|
|
164
|
+
if (value.len > 0) return value;
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
fn classContains(class_name: []const u8, needle: []const u8) bool {
|
|
170
|
+
return std.mem.indexOf(u8, class_name, needle) != null;
|
|
171
|
+
}
|
package/src/trace.zig
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
const selector = @import("selector.zig");
|
|
3
|
+
const trace_json = @import("trace_json.zig");
|
|
4
|
+
const types = @import("types.zig");
|
|
5
|
+
const version = @import("version.zig");
|
|
6
|
+
|
|
7
|
+
pub const RedactionRules = trace_json.RedactionRules;
|
|
8
|
+
|
|
9
|
+
pub const CaptureOptions = struct {
|
|
10
|
+
capture_screenshots: bool = true,
|
|
11
|
+
capture_hierarchy: bool = true,
|
|
12
|
+
capture_logs: bool = true,
|
|
13
|
+
capture_screen_recording: bool = false,
|
|
14
|
+
redaction: RedactionRules = .{},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
pub const TraceWriter = struct {
|
|
18
|
+
allocator: std.mem.Allocator,
|
|
19
|
+
root_dir: []const u8,
|
|
20
|
+
event_count: usize = 0,
|
|
21
|
+
snapshot_count: usize = 0,
|
|
22
|
+
partial_failure_count: usize = 0,
|
|
23
|
+
manifest: ?Manifest = null,
|
|
24
|
+
capture: CaptureOptions = .{},
|
|
25
|
+
|
|
26
|
+
pub fn init(allocator: std.mem.Allocator, root_dir: []const u8) !TraceWriter {
|
|
27
|
+
return try initWithOptions(allocator, root_dir, .{});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
pub fn initWithOptions(allocator: std.mem.Allocator, root_dir: []const u8, capture: CaptureOptions) !TraceWriter {
|
|
31
|
+
try std.fs.cwd().makePath(root_dir);
|
|
32
|
+
try resetTraceDirectory(allocator, root_dir);
|
|
33
|
+
const artifacts_path = try std.fs.path.join(allocator, &.{ root_dir, "artifacts" });
|
|
34
|
+
defer allocator.free(artifacts_path);
|
|
35
|
+
try std.fs.cwd().makePath(artifacts_path);
|
|
36
|
+
return .{
|
|
37
|
+
.allocator = allocator,
|
|
38
|
+
.root_dir = try allocator.dupe(u8, root_dir),
|
|
39
|
+
.capture = capture,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
pub fn deinit(self: *TraceWriter) void {
|
|
44
|
+
if (self.manifest) |*manifest| manifest.deinit(self.allocator);
|
|
45
|
+
self.allocator.free(self.root_dir);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
pub fn startManifest(self: *TraceWriter, scenario_name: []const u8, app_id: ?[]const u8) !void {
|
|
49
|
+
if (self.manifest) |*existing| existing.deinit(self.allocator);
|
|
50
|
+
self.manifest = .{
|
|
51
|
+
.scenario_name = try self.allocator.dupe(u8, scenario_name),
|
|
52
|
+
.app_id = try dupeOptional(self.allocator, app_id),
|
|
53
|
+
.status = try self.allocator.dupe(u8, "running"),
|
|
54
|
+
.started_at_ms = std.time.milliTimestamp(),
|
|
55
|
+
};
|
|
56
|
+
try self.writeManifest();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
pub const FinishManifestOptions = struct {
|
|
60
|
+
status: []const u8,
|
|
61
|
+
failed_step_index: ?usize = null,
|
|
62
|
+
error_name: ?[]const u8 = null,
|
|
63
|
+
report_path: ?[]const u8 = null,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
pub fn finishManifest(self: *TraceWriter, options: FinishManifestOptions) !void {
|
|
67
|
+
if (self.manifest == null) return;
|
|
68
|
+
var manifest = &self.manifest.?;
|
|
69
|
+
self.allocator.free(manifest.status);
|
|
70
|
+
manifest.status = try self.allocator.dupe(u8, options.status);
|
|
71
|
+
manifest.ended_at_ms = std.time.milliTimestamp();
|
|
72
|
+
manifest.failed_step_index = options.failed_step_index;
|
|
73
|
+
if (manifest.error_name) |value| self.allocator.free(value);
|
|
74
|
+
manifest.error_name = try dupeOptional(self.allocator, options.error_name);
|
|
75
|
+
if (options.report_path) |value| {
|
|
76
|
+
if (manifest.report_path) |old| self.allocator.free(old);
|
|
77
|
+
manifest.report_path = try self.allocator.dupe(u8, value);
|
|
78
|
+
}
|
|
79
|
+
try self.writeManifest();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
pub fn attachReport(self: *TraceWriter, report_path: []const u8) !void {
|
|
83
|
+
if (self.manifest == null) return;
|
|
84
|
+
var manifest = &self.manifest.?;
|
|
85
|
+
if (manifest.report_path) |old| self.allocator.free(old);
|
|
86
|
+
manifest.report_path = try self.allocator.dupe(u8, report_path);
|
|
87
|
+
try self.writeManifest();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
pub fn nextSnapshotId(self: *TraceWriter) ![]const u8 {
|
|
91
|
+
self.snapshot_count += 1;
|
|
92
|
+
return try std.fmt.allocPrint(self.allocator, "snapshot-{d}", .{self.snapshot_count});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
pub fn artifactPath(self: *TraceWriter, name: []const u8) ![]const u8 {
|
|
96
|
+
return try std.fs.path.join(self.allocator, &.{ self.root_dir, "artifacts", name });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
pub fn writeArtifact(self: *TraceWriter, name: []const u8, bytes: []const u8) ![]const u8 {
|
|
100
|
+
const path = try self.artifactPath(name);
|
|
101
|
+
errdefer self.allocator.free(path);
|
|
102
|
+
var file = try std.fs.cwd().createFile(path, .{ .truncate = true });
|
|
103
|
+
defer file.close();
|
|
104
|
+
try file.writeAll(bytes);
|
|
105
|
+
return path;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
pub fn recordEvent(self: *TraceWriter, kind: []const u8, payload: []const u8) !void {
|
|
109
|
+
self.event_count += 1;
|
|
110
|
+
if (isPartialFailureEvent(kind, payload)) self.partial_failure_count += 1;
|
|
111
|
+
const path = try std.fs.path.join(self.allocator, &.{ self.root_dir, "events.jsonl" });
|
|
112
|
+
defer self.allocator.free(path);
|
|
113
|
+
var file = try std.fs.cwd().createFile(path, .{ .truncate = false });
|
|
114
|
+
defer file.close();
|
|
115
|
+
try file.seekFromEnd(0);
|
|
116
|
+
var write_buffer: [4096]u8 = undefined;
|
|
117
|
+
var file_writer = file.writerStreaming(&write_buffer);
|
|
118
|
+
const writer = &file_writer.interface;
|
|
119
|
+
try writer.print(
|
|
120
|
+
"{{\"seq\":{d},\"timestampMs\":{d},\"kind\":\"{s}\",\"payload\":",
|
|
121
|
+
.{ self.event_count, std.time.milliTimestamp(), kind },
|
|
122
|
+
);
|
|
123
|
+
try trace_json.writeRedactedJsonPayload(self.allocator, writer, payload, self.capture.redaction);
|
|
124
|
+
try writer.writeAll("}\n");
|
|
125
|
+
try writer.flush();
|
|
126
|
+
try self.writeManifest();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
pub fn writeSnapshot(self: *TraceWriter, snapshot: types.ObservationSnapshot) ![]const u8 {
|
|
130
|
+
const file_name = try std.fmt.allocPrint(self.allocator, "{s}.json", .{snapshot.id});
|
|
131
|
+
defer self.allocator.free(file_name);
|
|
132
|
+
const path = try self.artifactPath(file_name);
|
|
133
|
+
errdefer self.allocator.free(path);
|
|
134
|
+
var file = try std.fs.cwd().createFile(path, .{ .truncate = true });
|
|
135
|
+
defer file.close();
|
|
136
|
+
var write_buffer: [8192]u8 = undefined;
|
|
137
|
+
var file_writer = file.writer(&write_buffer);
|
|
138
|
+
try trace_json.writeSnapshotJsonRedacted(&file_writer.interface, snapshot, self.capture.redaction);
|
|
139
|
+
try file_writer.interface.flush();
|
|
140
|
+
return path;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
pub fn flushManifest(self: *TraceWriter) !void {
|
|
144
|
+
try self.writeManifest();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
fn writeManifest(self: *TraceWriter) !void {
|
|
148
|
+
if (self.manifest == null) return;
|
|
149
|
+
const manifest = self.manifest.?;
|
|
150
|
+
const path = try std.fs.path.join(self.allocator, &.{ self.root_dir, "trace.json" });
|
|
151
|
+
defer self.allocator.free(path);
|
|
152
|
+
var file = try std.fs.cwd().createFile(path, .{ .truncate = true });
|
|
153
|
+
defer file.close();
|
|
154
|
+
var write_buffer: [4096]u8 = undefined;
|
|
155
|
+
var file_writer = file.writer(&write_buffer);
|
|
156
|
+
const writer = &file_writer.interface;
|
|
157
|
+
|
|
158
|
+
try writer.writeAll("{");
|
|
159
|
+
try writer.writeAll("\"schemaVersion\":1");
|
|
160
|
+
try writer.writeAll(",\"runnerVersion\":");
|
|
161
|
+
try writeJsonString(writer, version.runner_version);
|
|
162
|
+
try writer.writeAll(",\"protocolVersion\":");
|
|
163
|
+
try writeJsonString(writer, version.protocol_version);
|
|
164
|
+
try writer.writeAll(",\"scenarioName\":");
|
|
165
|
+
try writeJsonString(writer, manifest.scenario_name);
|
|
166
|
+
try writer.writeAll(",\"appId\":");
|
|
167
|
+
if (manifest.app_id) |app_id| {
|
|
168
|
+
try writeJsonString(writer, app_id);
|
|
169
|
+
} else {
|
|
170
|
+
try writer.writeAll("null");
|
|
171
|
+
}
|
|
172
|
+
const effective_status = if (std.mem.eql(u8, manifest.status, "passed") and self.partial_failure_count > 0) "partial" else manifest.status;
|
|
173
|
+
try writer.writeAll(",\"status\":");
|
|
174
|
+
try writeJsonString(writer, effective_status);
|
|
175
|
+
try writer.print(",\"startedAtMs\":{d}", .{manifest.started_at_ms});
|
|
176
|
+
try writer.writeAll(",\"endedAtMs\":");
|
|
177
|
+
if (manifest.ended_at_ms) |value| {
|
|
178
|
+
try writer.print("{d}", .{value});
|
|
179
|
+
} else {
|
|
180
|
+
try writer.writeAll("null");
|
|
181
|
+
}
|
|
182
|
+
try writer.writeAll(",\"durationMs\":");
|
|
183
|
+
if (manifest.ended_at_ms) |ended| {
|
|
184
|
+
try writer.print("{d}", .{ended - manifest.started_at_ms});
|
|
185
|
+
} else {
|
|
186
|
+
try writer.writeAll("null");
|
|
187
|
+
}
|
|
188
|
+
try writer.writeAll(",\"failedStepIndex\":");
|
|
189
|
+
if (manifest.failed_step_index) |value| {
|
|
190
|
+
try writer.print("{d}", .{value});
|
|
191
|
+
} else {
|
|
192
|
+
try writer.writeAll("null");
|
|
193
|
+
}
|
|
194
|
+
try writer.writeAll(",\"error\":");
|
|
195
|
+
if (manifest.error_name) |value| {
|
|
196
|
+
try writeJsonString(writer, value);
|
|
197
|
+
} else {
|
|
198
|
+
try writer.writeAll("null");
|
|
199
|
+
}
|
|
200
|
+
try writer.writeAll(",\"eventsPath\":\"events.jsonl\"");
|
|
201
|
+
try writer.writeAll(",\"artifactsDir\":\"artifacts\"");
|
|
202
|
+
try writer.print(",\"eventCount\":{d}", .{self.event_count});
|
|
203
|
+
try writer.print(",\"snapshotCount\":{d}", .{self.snapshot_count});
|
|
204
|
+
try writer.print(",\"partialFailureCount\":{d}", .{self.partial_failure_count});
|
|
205
|
+
try writer.writeAll(",\"reportPath\":");
|
|
206
|
+
if (manifest.report_path) |value| {
|
|
207
|
+
try writeJsonString(writer, value);
|
|
208
|
+
} else {
|
|
209
|
+
try writer.writeAll("null");
|
|
210
|
+
}
|
|
211
|
+
try writer.writeAll("}\n");
|
|
212
|
+
try writer.flush();
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
fn isPartialFailureEvent(kind: []const u8, payload: []const u8) bool {
|
|
217
|
+
return std.mem.eql(u8, kind, "observe.snapshot.semanticExtraction") and
|
|
218
|
+
std.mem.indexOf(u8, payload, "\"status\":\"failed\"") != null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
fn resetTraceDirectory(allocator: std.mem.Allocator, root_dir: []const u8) !void {
|
|
222
|
+
const stale_files = [_][]const u8{ "events.jsonl", "trace.json", "report.html" };
|
|
223
|
+
for (stale_files) |name| {
|
|
224
|
+
const path = try std.fs.path.join(allocator, &.{ root_dir, name });
|
|
225
|
+
defer allocator.free(path);
|
|
226
|
+
std.fs.cwd().deleteFile(path) catch |err| switch (err) {
|
|
227
|
+
error.FileNotFound => {},
|
|
228
|
+
else => return err,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const artifacts_path = try std.fs.path.join(allocator, &.{ root_dir, "artifacts" });
|
|
233
|
+
defer allocator.free(artifacts_path);
|
|
234
|
+
var artifacts_exists = true;
|
|
235
|
+
std.fs.cwd().access(artifacts_path, .{}) catch |err| switch (err) {
|
|
236
|
+
error.FileNotFound => artifacts_exists = false,
|
|
237
|
+
else => return err,
|
|
238
|
+
};
|
|
239
|
+
if (artifacts_exists) try std.fs.cwd().deleteTree(artifacts_path);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const Manifest = struct {
|
|
243
|
+
scenario_name: []const u8,
|
|
244
|
+
app_id: ?[]const u8 = null,
|
|
245
|
+
status: []const u8,
|
|
246
|
+
started_at_ms: i64,
|
|
247
|
+
ended_at_ms: ?i64 = null,
|
|
248
|
+
failed_step_index: ?usize = null,
|
|
249
|
+
error_name: ?[]const u8 = null,
|
|
250
|
+
report_path: ?[]const u8 = null,
|
|
251
|
+
|
|
252
|
+
fn deinit(self: *Manifest, allocator: std.mem.Allocator) void {
|
|
253
|
+
allocator.free(self.scenario_name);
|
|
254
|
+
if (self.app_id) |value| allocator.free(value);
|
|
255
|
+
allocator.free(self.status);
|
|
256
|
+
if (self.error_name) |value| allocator.free(value);
|
|
257
|
+
if (self.report_path) |value| allocator.free(value);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
fn dupeOptional(allocator: std.mem.Allocator, value: ?[]const u8) !?[]const u8 {
|
|
262
|
+
if (value) |actual| return try allocator.dupe(u8, actual);
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
pub fn writeSnapshotJson(writer: anytype, snapshot: types.ObservationSnapshot) !void {
|
|
267
|
+
try trace_json.writeSnapshotJson(writer, snapshot);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
pub fn attachReportPath(allocator: std.mem.Allocator, root_dir: []const u8, report_path: []const u8) !void {
|
|
271
|
+
const manifest_path = try std.fs.path.join(allocator, &.{ root_dir, "trace.json" });
|
|
272
|
+
defer allocator.free(manifest_path);
|
|
273
|
+
|
|
274
|
+
const content = std.fs.cwd().readFileAlloc(allocator, manifest_path, 1024 * 1024) catch |err| switch (err) {
|
|
275
|
+
error.FileNotFound => return,
|
|
276
|
+
else => return err,
|
|
277
|
+
};
|
|
278
|
+
defer allocator.free(content);
|
|
279
|
+
|
|
280
|
+
var parsed = try std.json.parseFromSlice(std.json.Value, allocator, content, .{});
|
|
281
|
+
defer parsed.deinit();
|
|
282
|
+
if (parsed.value != .object) return error.InvalidTraceManifest;
|
|
283
|
+
|
|
284
|
+
const arena_allocator = parsed.arena.allocator();
|
|
285
|
+
const owned_report_path = try arena_allocator.dupe(u8, report_path);
|
|
286
|
+
try parsed.value.object.put("reportPath", .{ .string = owned_report_path });
|
|
287
|
+
|
|
288
|
+
var file = try std.fs.cwd().createFile(manifest_path, .{ .truncate = true });
|
|
289
|
+
defer file.close();
|
|
290
|
+
var write_buffer: [4096]u8 = undefined;
|
|
291
|
+
var file_writer = file.writer(&write_buffer);
|
|
292
|
+
try std.json.Stringify.value(parsed.value, .{}, &file_writer.interface);
|
|
293
|
+
try file_writer.interface.writeByte('\n');
|
|
294
|
+
try file_writer.interface.flush();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
pub fn writeNodeJson(writer: anytype, node: types.UiNode) !void {
|
|
298
|
+
try trace_json.writeNodeJson(writer, node);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
pub fn writeSelectorJson(writer: anytype, wanted: selector.Selector) !void {
|
|
302
|
+
try trace_json.writeSelectorJson(writer, wanted);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
pub fn writeRedactedJsonString(writer: anytype, value: []const u8) !void {
|
|
306
|
+
try trace_json.writeRedactedJsonString(writer, value);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
pub fn writeRedactedJsonStringForKey(writer: anytype, key: []const u8, value: []const u8, force_secret: bool) !void {
|
|
310
|
+
try trace_json.writeRedactedJsonStringForKey(writer, key, value, force_secret);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
pub fn writeJsonString(writer: anytype, value: []const u8) !void {
|
|
314
|
+
try trace_json.writeJsonString(writer, value);
|
|
315
|
+
}
|