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/runner.zig
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
const runner_actions = @import("runner_actions.zig");
|
|
3
|
+
const runner_config = @import("runner_config.zig");
|
|
4
|
+
const runner_events = @import("runner_events.zig");
|
|
5
|
+
const runner_waits = @import("runner_waits.zig");
|
|
6
|
+
const scenario = @import("scenario.zig");
|
|
7
|
+
const selector = @import("selector.zig");
|
|
8
|
+
const trace = @import("trace.zig");
|
|
9
|
+
|
|
10
|
+
pub const RunOptions = runner_config.RunOptions;
|
|
11
|
+
|
|
12
|
+
pub fn runScenario(
|
|
13
|
+
allocator: std.mem.Allocator,
|
|
14
|
+
device: anytype,
|
|
15
|
+
script: scenario.Scenario,
|
|
16
|
+
writer: ?*trace.TraceWriter,
|
|
17
|
+
options: RunOptions,
|
|
18
|
+
) !void {
|
|
19
|
+
if (writer) |tw| {
|
|
20
|
+
try tw.startManifest(script.name, script.app_id);
|
|
21
|
+
const payload = try runner_events.eventString(tw.allocator, script.name);
|
|
22
|
+
defer tw.allocator.free(payload);
|
|
23
|
+
try tw.recordEvent("scenario.start", payload);
|
|
24
|
+
}
|
|
25
|
+
for (script.steps, 0..) |step, index| {
|
|
26
|
+
executeStep(allocator, device, step, writer, options) catch |err| {
|
|
27
|
+
if (writer) |tw| {
|
|
28
|
+
try runner_events.recordStepError(tw, index, err);
|
|
29
|
+
try runner_events.recordScenarioEnd(tw, script.name, "failed", index, err);
|
|
30
|
+
try tw.finishManifest(.{
|
|
31
|
+
.status = "failed",
|
|
32
|
+
.failed_step_index = index,
|
|
33
|
+
.error_name = @errorName(err),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return err;
|
|
37
|
+
};
|
|
38
|
+
if (writer) |tw| {
|
|
39
|
+
const payload = try std.fmt.allocPrint(tw.allocator, "{{\"index\":{d}}}", .{index});
|
|
40
|
+
defer tw.allocator.free(payload);
|
|
41
|
+
try tw.recordEvent("step.done", payload);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (writer) |tw| {
|
|
45
|
+
try runner_events.recordScenarioEnd(tw, script.name, "passed", null, null);
|
|
46
|
+
try tw.finishManifest(.{ .status = "passed" });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
pub fn executeStep(
|
|
51
|
+
allocator: std.mem.Allocator,
|
|
52
|
+
device: anytype,
|
|
53
|
+
step: scenario.Step,
|
|
54
|
+
writer: ?*trace.TraceWriter,
|
|
55
|
+
options: RunOptions,
|
|
56
|
+
) !void {
|
|
57
|
+
switch (step) {
|
|
58
|
+
.launch => {
|
|
59
|
+
device.launch() catch |err| {
|
|
60
|
+
if (writer) |tw| try runner_events.recordActionStatus(tw, "app.launch", "failed", err, null);
|
|
61
|
+
return err;
|
|
62
|
+
};
|
|
63
|
+
if (writer) |tw| try runner_events.recordActionStatus(tw, "app.launch", "ok", null, null);
|
|
64
|
+
try settleDevice(device, options);
|
|
65
|
+
},
|
|
66
|
+
.stop => try device.stop(),
|
|
67
|
+
.clear_state => try device.clearState(),
|
|
68
|
+
.snapshot => {
|
|
69
|
+
var snap = try device.snapshot(writer);
|
|
70
|
+
defer snap.deinit(device.allocator);
|
|
71
|
+
if (writer) |tw| {
|
|
72
|
+
const path = try tw.writeSnapshot(snap);
|
|
73
|
+
defer tw.allocator.free(path);
|
|
74
|
+
const payload = try runner_events.eventString(tw.allocator, path);
|
|
75
|
+
defer tw.allocator.free(payload);
|
|
76
|
+
try tw.recordEvent("observe.snapshot", payload);
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
.open_link => |url| {
|
|
80
|
+
device.openLink(url) catch |err| {
|
|
81
|
+
if (writer) |tw| try runner_events.recordActionStatus(tw, "app.openLink", "failed", err, url);
|
|
82
|
+
return err;
|
|
83
|
+
};
|
|
84
|
+
if (writer) |tw| try runner_events.recordActionStatus(tw, "app.openLink", "ok", null, url);
|
|
85
|
+
try settleDevice(device, options);
|
|
86
|
+
},
|
|
87
|
+
.tap => |wanted| try tapSelector(device, wanted, writer, options),
|
|
88
|
+
.type_text => |input| {
|
|
89
|
+
if (input.selector) |wanted| return try typeTextSelector(device, wanted, input.text, writer, options);
|
|
90
|
+
try device.typeText(input.text);
|
|
91
|
+
try settleDevice(device, options);
|
|
92
|
+
},
|
|
93
|
+
.erase_text => |input| {
|
|
94
|
+
if (input.selector) |wanted| return try eraseTextSelector(device, wanted, input.max_chars, writer, options);
|
|
95
|
+
try device.eraseText(input.max_chars);
|
|
96
|
+
if (writer) |tw| {
|
|
97
|
+
const payload = try std.fmt.allocPrint(tw.allocator, "{{\"maxChars\":{d}}}", .{input.max_chars});
|
|
98
|
+
defer tw.allocator.free(payload);
|
|
99
|
+
try tw.recordEvent("ui.eraseText", payload);
|
|
100
|
+
}
|
|
101
|
+
try settleDevice(device, options);
|
|
102
|
+
},
|
|
103
|
+
.press_back => {
|
|
104
|
+
try device.pressBack();
|
|
105
|
+
try settleDevice(device, options);
|
|
106
|
+
},
|
|
107
|
+
.hide_keyboard => {
|
|
108
|
+
try device.hideKeyboard();
|
|
109
|
+
if (writer) |tw| try tw.recordEvent("ui.hideKeyboard", "{\"status\":\"ok\"}");
|
|
110
|
+
try settleDevice(device, options);
|
|
111
|
+
},
|
|
112
|
+
.swipe => |swipe| {
|
|
113
|
+
try device.swipe(swipe.x1, swipe.y1, swipe.x2, swipe.y2, swipe.duration_ms);
|
|
114
|
+
try settleDevice(device, options);
|
|
115
|
+
},
|
|
116
|
+
.wait_visible => |wait| {
|
|
117
|
+
if (!try waitUntilVisible(device, wait.selector, wait.timeout_ms, writer, options)) return error.WaitTimeout;
|
|
118
|
+
},
|
|
119
|
+
.wait_not_visible => |wait| {
|
|
120
|
+
if (!try waitUntilNotVisible(device, wait.selector, wait.timeout_ms, writer, options)) return error.WaitTimeout;
|
|
121
|
+
},
|
|
122
|
+
.wait_any => |wait| {
|
|
123
|
+
if (try waitUntilAnyVisible(device, wait.selectors, wait.timeout_ms, writer, options) == null) return error.WaitTimeout;
|
|
124
|
+
},
|
|
125
|
+
.assert_visible => |wanted| {
|
|
126
|
+
if (!try waitUntilVisible(device, wanted, options.default_timeout_ms, writer, options)) return error.AssertionFailed;
|
|
127
|
+
},
|
|
128
|
+
.assert_not_visible => |wanted| {
|
|
129
|
+
if (!try waitUntilNotVisible(device, wanted, options.default_timeout_ms, writer, options)) return error.AssertionFailed;
|
|
130
|
+
},
|
|
131
|
+
.assert_none_visible => |assertion| {
|
|
132
|
+
if (!try assertNoneVisible(device, assertion.selectors, assertion.timeout_ms, writer, options)) return error.AssertionFailed;
|
|
133
|
+
},
|
|
134
|
+
.assert_healthy_timeout_ms => |timeout_ms| {
|
|
135
|
+
if (!try assertHealthy(device, timeout_ms, writer, options)) return error.AssertionFailed;
|
|
136
|
+
},
|
|
137
|
+
.optional => |inner| {
|
|
138
|
+
executeStep(allocator, device, inner.*, writer, options) catch |err| {
|
|
139
|
+
if (writer) |tw| {
|
|
140
|
+
const payload = try std.fmt.allocPrint(tw.allocator, "{{\"status\":\"skipped\",\"error\":\"{s}\"}}", .{@errorName(err)});
|
|
141
|
+
defer tw.allocator.free(payload);
|
|
142
|
+
try tw.recordEvent("step.optional", payload);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
.when_visible => |block| {
|
|
147
|
+
const visible = if (block.timeout_ms == 0)
|
|
148
|
+
try isVisibleNow(device, block.selector, writer)
|
|
149
|
+
else
|
|
150
|
+
try waitUntilVisible(device, block.selector, block.timeout_ms, writer, options);
|
|
151
|
+
if (visible) {
|
|
152
|
+
for (block.steps) |inner| try executeStep(allocator, device, inner, writer, options);
|
|
153
|
+
} else if (writer) |tw| {
|
|
154
|
+
try runner_events.recordSelectorEvent(tw, "step.whenVisible.skipped", block.selector);
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
.repeat => |block| {
|
|
158
|
+
var iteration: u32 = 0;
|
|
159
|
+
while (iteration < block.times) : (iteration += 1) {
|
|
160
|
+
if (writer) |tw| {
|
|
161
|
+
const payload = try std.fmt.allocPrint(tw.allocator, "{{\"iteration\":{d},\"times\":{d}}}", .{ iteration + 1, block.times });
|
|
162
|
+
defer tw.allocator.free(payload);
|
|
163
|
+
try tw.recordEvent("step.repeat.iteration", payload);
|
|
164
|
+
}
|
|
165
|
+
for (block.steps) |inner| try executeStep(allocator, device, inner, writer, options);
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
.scroll_until_visible => |scroll| {
|
|
169
|
+
if (!try scrollUntilVisible(device, scroll.selector, scroll.timeout_ms, scroll.direction, writer, options)) return error.WaitTimeout;
|
|
170
|
+
},
|
|
171
|
+
.sleep_ms => |ms| try sleepMs(ms),
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
pub fn tapSelector(
|
|
176
|
+
device: anytype,
|
|
177
|
+
wanted: selector.Selector,
|
|
178
|
+
writer: ?*trace.TraceWriter,
|
|
179
|
+
options: RunOptions,
|
|
180
|
+
) !void {
|
|
181
|
+
return try runner_actions.tapSelector(device, wanted, writer, options);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
pub fn typeTextSelector(
|
|
185
|
+
device: anytype,
|
|
186
|
+
wanted: selector.Selector,
|
|
187
|
+
text: []const u8,
|
|
188
|
+
writer: ?*trace.TraceWriter,
|
|
189
|
+
options: RunOptions,
|
|
190
|
+
) !void {
|
|
191
|
+
return try runner_actions.typeTextSelector(device, wanted, text, writer, options);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
pub fn eraseTextSelector(
|
|
195
|
+
device: anytype,
|
|
196
|
+
wanted: selector.Selector,
|
|
197
|
+
max_chars: u32,
|
|
198
|
+
writer: ?*trace.TraceWriter,
|
|
199
|
+
options: RunOptions,
|
|
200
|
+
) !void {
|
|
201
|
+
return try runner_actions.eraseTextSelector(device, wanted, max_chars, writer, options);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
pub fn waitUntilVisible(
|
|
205
|
+
device: anytype,
|
|
206
|
+
wanted: selector.Selector,
|
|
207
|
+
timeout_ms: u64,
|
|
208
|
+
writer: ?*trace.TraceWriter,
|
|
209
|
+
options: RunOptions,
|
|
210
|
+
) !bool {
|
|
211
|
+
return try runner_waits.waitUntilVisible(device, wanted, timeout_ms, writer, options);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
pub fn waitUntilNotVisible(
|
|
215
|
+
device: anytype,
|
|
216
|
+
wanted: selector.Selector,
|
|
217
|
+
timeout_ms: u64,
|
|
218
|
+
writer: ?*trace.TraceWriter,
|
|
219
|
+
options: RunOptions,
|
|
220
|
+
) !bool {
|
|
221
|
+
return try runner_waits.waitUntilNotVisible(device, wanted, timeout_ms, writer, options);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
pub fn waitUntilAnyVisible(
|
|
225
|
+
device: anytype,
|
|
226
|
+
selectors: []const selector.Selector,
|
|
227
|
+
timeout_ms: u64,
|
|
228
|
+
writer: ?*trace.TraceWriter,
|
|
229
|
+
options: RunOptions,
|
|
230
|
+
) !?usize {
|
|
231
|
+
return try runner_waits.waitUntilAnyVisible(device, selectors, timeout_ms, writer, options);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
pub fn assertNoneVisible(
|
|
235
|
+
device: anytype,
|
|
236
|
+
selectors: []const selector.Selector,
|
|
237
|
+
timeout_ms: u64,
|
|
238
|
+
writer: ?*trace.TraceWriter,
|
|
239
|
+
options: RunOptions,
|
|
240
|
+
) !bool {
|
|
241
|
+
return try runner_waits.assertNoneVisible(device, selectors, timeout_ms, writer, options);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
pub fn assertHealthy(
|
|
245
|
+
device: anytype,
|
|
246
|
+
timeout_ms: u64,
|
|
247
|
+
writer: ?*trace.TraceWriter,
|
|
248
|
+
options: RunOptions,
|
|
249
|
+
) !bool {
|
|
250
|
+
return try runner_waits.assertHealthy(device, timeout_ms, writer, options);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
pub fn scrollUntilVisible(
|
|
254
|
+
device: anytype,
|
|
255
|
+
wanted: selector.Selector,
|
|
256
|
+
timeout_ms: u64,
|
|
257
|
+
direction: scenario.ScrollDirection,
|
|
258
|
+
writer: ?*trace.TraceWriter,
|
|
259
|
+
options: RunOptions,
|
|
260
|
+
) !bool {
|
|
261
|
+
return try runner_waits.scrollUntilVisible(device, wanted, timeout_ms, direction, writer, options);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
fn isVisibleNow(
|
|
265
|
+
device: anytype,
|
|
266
|
+
wanted: selector.Selector,
|
|
267
|
+
writer: ?*trace.TraceWriter,
|
|
268
|
+
) !bool {
|
|
269
|
+
var snap = try device.snapshot(writer);
|
|
270
|
+
defer snap.deinit(device.allocator);
|
|
271
|
+
return selector.find(snap.nodes, wanted) != null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
fn sleepMs(ms: u64) !void {
|
|
275
|
+
std.Thread.sleep(ms * std.time.ns_per_ms);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
fn settleDevice(device: anytype, options: RunOptions) !void {
|
|
279
|
+
try device.settle(options.settle_ms);
|
|
280
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
const runner_config = @import("runner_config.zig");
|
|
3
|
+
const runner_events = @import("runner_events.zig");
|
|
4
|
+
const runner_native = @import("runner_native.zig");
|
|
5
|
+
const selector = @import("selector.zig");
|
|
6
|
+
const trace = @import("trace.zig");
|
|
7
|
+
const types = @import("types.zig");
|
|
8
|
+
|
|
9
|
+
const RunOptions = runner_config.RunOptions;
|
|
10
|
+
|
|
11
|
+
pub fn tapSelector(
|
|
12
|
+
device: anytype,
|
|
13
|
+
wanted: selector.Selector,
|
|
14
|
+
writer: ?*trace.TraceWriter,
|
|
15
|
+
options: RunOptions,
|
|
16
|
+
) !void {
|
|
17
|
+
if (try runner_native.tryTapSelector(device, wanted, writer, options.settle_ms)) return;
|
|
18
|
+
|
|
19
|
+
const deadline = std.time.milliTimestamp() + @as(i64, @intCast(options.action_timeout_ms));
|
|
20
|
+
var attempts: u32 = 0;
|
|
21
|
+
while (true) {
|
|
22
|
+
attempts += 1;
|
|
23
|
+
var snap = try device.snapshot(writer);
|
|
24
|
+
defer snap.deinit(device.allocator);
|
|
25
|
+
if (findActionable(snap, wanted)) |node| {
|
|
26
|
+
try device.tap(node.bounds.centerX(), node.bounds.centerY());
|
|
27
|
+
if (writer) |tw| {
|
|
28
|
+
var payload = std.ArrayList(u8).empty;
|
|
29
|
+
defer payload.deinit(tw.allocator);
|
|
30
|
+
try payload.writer(tw.allocator).print("{{\"snapshotId\":\"{s}\",\"target\":\"{s}\",\"x\":{d},\"y\":{d},\"attempts\":{d},\"selector\":", .{
|
|
31
|
+
snap.id,
|
|
32
|
+
node.stable_id,
|
|
33
|
+
node.bounds.centerX(),
|
|
34
|
+
node.bounds.centerY(),
|
|
35
|
+
attempts,
|
|
36
|
+
});
|
|
37
|
+
try trace.writeSelectorJson(payload.writer(tw.allocator), wanted);
|
|
38
|
+
try payload.writer(tw.allocator).writeAll("}");
|
|
39
|
+
try tw.recordEvent("ui.tap", payload.items);
|
|
40
|
+
}
|
|
41
|
+
try settleDevice(device, options);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (std.time.milliTimestamp() >= deadline) {
|
|
45
|
+
if (writer) |tw| {
|
|
46
|
+
try runner_events.recordSelectorMiss(tw, "ui.tap.notFound", wanted, snap);
|
|
47
|
+
}
|
|
48
|
+
return error.SelectorNotFound;
|
|
49
|
+
}
|
|
50
|
+
try sleepMs(options.poll_ms);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
pub fn typeTextSelector(
|
|
55
|
+
device: anytype,
|
|
56
|
+
wanted: selector.Selector,
|
|
57
|
+
text: []const u8,
|
|
58
|
+
writer: ?*trace.TraceWriter,
|
|
59
|
+
options: RunOptions,
|
|
60
|
+
) !void {
|
|
61
|
+
if (try runner_native.tryTypeTextSelector(device, wanted, text, writer, options.settle_ms)) return;
|
|
62
|
+
try tapSelector(device, wanted, writer, options);
|
|
63
|
+
try device.typeText(text);
|
|
64
|
+
try settleDevice(device, options);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
pub fn eraseTextSelector(
|
|
68
|
+
device: anytype,
|
|
69
|
+
wanted: selector.Selector,
|
|
70
|
+
max_chars: u32,
|
|
71
|
+
writer: ?*trace.TraceWriter,
|
|
72
|
+
options: RunOptions,
|
|
73
|
+
) !void {
|
|
74
|
+
if (try runner_native.tryEraseTextSelector(device, wanted, max_chars, writer, options.settle_ms)) return;
|
|
75
|
+
try tapSelector(device, wanted, writer, options);
|
|
76
|
+
try device.eraseText(max_chars);
|
|
77
|
+
if (writer) |tw| {
|
|
78
|
+
const payload = try std.fmt.allocPrint(tw.allocator, "{{\"maxChars\":{d}}}", .{max_chars});
|
|
79
|
+
defer tw.allocator.free(payload);
|
|
80
|
+
try tw.recordEvent("ui.eraseText", payload);
|
|
81
|
+
}
|
|
82
|
+
try settleDevice(device, options);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
fn findActionable(snap: types.ObservationSnapshot, wanted: selector.Selector) ?types.UiNode {
|
|
86
|
+
for (snap.nodes) |node| {
|
|
87
|
+
if (!selector.matches(node, wanted)) continue;
|
|
88
|
+
if (!node.enabled) continue;
|
|
89
|
+
if (!isInViewport(node, snap.viewport)) continue;
|
|
90
|
+
return node;
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
fn isInViewport(node: types.UiNode, viewport: types.Viewport) bool {
|
|
96
|
+
if (node.bounds.width <= 0 or node.bounds.height <= 0) return false;
|
|
97
|
+
if (viewport.width == 0 or viewport.height == 0) return true;
|
|
98
|
+
const right = node.bounds.x + node.bounds.width;
|
|
99
|
+
const bottom = node.bounds.y + node.bounds.height;
|
|
100
|
+
return right > 0 and bottom > 0 and node.bounds.x < @as(i32, @intCast(viewport.width)) and node.bounds.y < @as(i32, @intCast(viewport.height));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
fn settleDevice(device: anytype, options: RunOptions) !void {
|
|
104
|
+
try device.settle(options.settle_ms);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
fn sleepMs(ms: u64) !void {
|
|
108
|
+
std.Thread.sleep(ms * std.time.ns_per_ms);
|
|
109
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
const selector = @import("selector.zig");
|
|
3
|
+
const trace = @import("trace.zig");
|
|
4
|
+
const types = @import("types.zig");
|
|
5
|
+
|
|
6
|
+
pub fn record(
|
|
7
|
+
tw: *trace.TraceWriter,
|
|
8
|
+
kind: []const u8,
|
|
9
|
+
status: []const u8,
|
|
10
|
+
strategy: ?[]const u8,
|
|
11
|
+
selectors: []const selector.Selector,
|
|
12
|
+
snap: types.ObservationSnapshot,
|
|
13
|
+
) !void {
|
|
14
|
+
var payload = std.ArrayList(u8).empty;
|
|
15
|
+
defer payload.deinit(tw.allocator);
|
|
16
|
+
try writeSelectorDiagnosticJson(payload.writer(tw.allocator), status, strategy, selectors, snap);
|
|
17
|
+
try tw.recordEvent(kind, payload.items);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
pub fn writeSelectorDiagnosticJson(
|
|
21
|
+
writer: anytype,
|
|
22
|
+
status: []const u8,
|
|
23
|
+
strategy: ?[]const u8,
|
|
24
|
+
selectors: []const selector.Selector,
|
|
25
|
+
snap: types.ObservationSnapshot,
|
|
26
|
+
) !void {
|
|
27
|
+
try writer.print("{{\"status\":\"{s}\"", .{status});
|
|
28
|
+
if (strategy) |value| {
|
|
29
|
+
try writer.writeAll(",\"strategy\":");
|
|
30
|
+
try trace.writeJsonString(writer, value);
|
|
31
|
+
}
|
|
32
|
+
try writer.print(",\"snapshotId\":\"{s}\",\"selectors\":[", .{snap.id});
|
|
33
|
+
for (selectors, 0..) |wanted, index| {
|
|
34
|
+
if (index > 0) try writer.writeAll(",");
|
|
35
|
+
try trace.writeSelectorJson(writer, wanted);
|
|
36
|
+
}
|
|
37
|
+
try writer.writeAll("],\"activePackage\":");
|
|
38
|
+
if (snap.active_package) |value| {
|
|
39
|
+
try trace.writeJsonString(writer, value);
|
|
40
|
+
} else {
|
|
41
|
+
try writer.writeAll("null");
|
|
42
|
+
}
|
|
43
|
+
try writer.writeAll(",\"activeActivity\":");
|
|
44
|
+
if (snap.active_activity) |value| {
|
|
45
|
+
try trace.writeJsonString(writer, value);
|
|
46
|
+
} else {
|
|
47
|
+
try writer.writeAll("null");
|
|
48
|
+
}
|
|
49
|
+
try writer.writeAll(",\"visibleTexts\":[");
|
|
50
|
+
var count: usize = 0;
|
|
51
|
+
for (snap.nodes) |node| {
|
|
52
|
+
const text = node.text orelse node.content_desc orelse continue;
|
|
53
|
+
if (!node.visible or text.len == 0) continue;
|
|
54
|
+
if (count > 0) try writer.writeAll(",");
|
|
55
|
+
try trace.writeJsonString(writer, text);
|
|
56
|
+
count += 1;
|
|
57
|
+
if (count >= 20) break;
|
|
58
|
+
}
|
|
59
|
+
try writer.writeAll("]");
|
|
60
|
+
try writeCandidateList(writer, "hiddenCandidates", selectors, snap, .hidden);
|
|
61
|
+
try writeCandidateList(writer, "disabledCandidates", selectors, snap, .disabled);
|
|
62
|
+
try writeCandidateList(writer, "offscreenCandidates", selectors, snap, .offscreen);
|
|
63
|
+
try writeNearestTextMatches(writer, selectors, snap);
|
|
64
|
+
try writer.writeAll("}");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const CandidateKind = enum {
|
|
68
|
+
hidden,
|
|
69
|
+
disabled,
|
|
70
|
+
offscreen,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
fn writeCandidateList(
|
|
74
|
+
writer: anytype,
|
|
75
|
+
field_name: []const u8,
|
|
76
|
+
selectors: []const selector.Selector,
|
|
77
|
+
snap: types.ObservationSnapshot,
|
|
78
|
+
candidate_kind: CandidateKind,
|
|
79
|
+
) !void {
|
|
80
|
+
try writer.print(",\"{s}\":[", .{field_name});
|
|
81
|
+
var count: usize = 0;
|
|
82
|
+
for (snap.nodes) |node| {
|
|
83
|
+
var matched = false;
|
|
84
|
+
for (selectors) |wanted| {
|
|
85
|
+
if (nodeMatchesSelectorFields(node, wanted)) {
|
|
86
|
+
matched = true;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (!matched) continue;
|
|
91
|
+
const include = switch (candidate_kind) {
|
|
92
|
+
.hidden => !node.visible,
|
|
93
|
+
.disabled => node.visible and !node.enabled,
|
|
94
|
+
.offscreen => node.visible and node.enabled and !isInViewport(node, snap.viewport),
|
|
95
|
+
};
|
|
96
|
+
if (!include) continue;
|
|
97
|
+
if (count > 0) try writer.writeAll(",");
|
|
98
|
+
try writeNodeDiagnostic(writer, node);
|
|
99
|
+
count += 1;
|
|
100
|
+
if (count >= 10) break;
|
|
101
|
+
}
|
|
102
|
+
try writer.writeAll("]");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
fn writeNearestTextMatches(writer: anytype, selectors: []const selector.Selector, snap: types.ObservationSnapshot) !void {
|
|
106
|
+
try writer.writeAll(",\"nearestTextMatches\":[");
|
|
107
|
+
var written: usize = 0;
|
|
108
|
+
for (selectors) |wanted| {
|
|
109
|
+
const target = selectorTextTarget(wanted) orelse continue;
|
|
110
|
+
var best: [5]NearestCandidate = undefined;
|
|
111
|
+
var best_len: usize = 0;
|
|
112
|
+
for (snap.nodes) |node| {
|
|
113
|
+
const label = nodeLabel(node) orelse continue;
|
|
114
|
+
if (label.len == 0 or nodeMatchesSelectorFields(node, wanted)) continue;
|
|
115
|
+
const score = textDistance(target, label);
|
|
116
|
+
if (score > @max(target.len, label.len)) continue;
|
|
117
|
+
insertNearest(&best, &best_len, .{ .node = node, .text = label, .score = score });
|
|
118
|
+
}
|
|
119
|
+
for (best[0..best_len]) |candidate| {
|
|
120
|
+
if (written > 0) try writer.writeAll(",");
|
|
121
|
+
try writer.writeAll("{\"stableId\":");
|
|
122
|
+
try trace.writeJsonString(writer, candidate.node.stable_id);
|
|
123
|
+
try writer.writeAll(",\"text\":");
|
|
124
|
+
try trace.writeJsonString(writer, candidate.text);
|
|
125
|
+
try writer.print(",\"score\":{d}", .{candidate.score});
|
|
126
|
+
try writer.writeAll(",\"enabled\":");
|
|
127
|
+
try writer.writeAll(if (candidate.node.enabled) "true" else "false");
|
|
128
|
+
try writer.writeAll(",\"visible\":");
|
|
129
|
+
try writer.writeAll(if (candidate.node.visible) "true" else "false");
|
|
130
|
+
try writer.writeAll("}");
|
|
131
|
+
written += 1;
|
|
132
|
+
if (written >= 10) break;
|
|
133
|
+
}
|
|
134
|
+
if (written >= 10) break;
|
|
135
|
+
}
|
|
136
|
+
try writer.writeAll("]");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const NearestCandidate = struct {
|
|
140
|
+
node: types.UiNode,
|
|
141
|
+
text: []const u8,
|
|
142
|
+
score: usize,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
fn insertNearest(candidates: *[5]NearestCandidate, len: *usize, candidate: NearestCandidate) void {
|
|
146
|
+
const insert_limit = candidates.len;
|
|
147
|
+
if (len.* == insert_limit and candidate.score >= candidates[len.* - 1].score) return;
|
|
148
|
+
var index: usize = 0;
|
|
149
|
+
while (index < len.* and candidates[index].score <= candidate.score) : (index += 1) {}
|
|
150
|
+
if (len.* < insert_limit) len.* += 1;
|
|
151
|
+
var move_index = len.* - 1;
|
|
152
|
+
while (move_index > index) : (move_index -= 1) {
|
|
153
|
+
candidates[move_index] = candidates[move_index - 1];
|
|
154
|
+
}
|
|
155
|
+
candidates[index] = candidate;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
fn writeNodeDiagnostic(writer: anytype, node: types.UiNode) !void {
|
|
159
|
+
try writer.writeAll("{\"stableId\":");
|
|
160
|
+
try trace.writeJsonString(writer, node.stable_id);
|
|
161
|
+
try writer.writeAll(",\"className\":");
|
|
162
|
+
try trace.writeJsonString(writer, node.class_name);
|
|
163
|
+
try writer.writeAll(",\"text\":");
|
|
164
|
+
if (node.text) |value| {
|
|
165
|
+
try trace.writeJsonString(writer, value);
|
|
166
|
+
} else {
|
|
167
|
+
try writer.writeAll("null");
|
|
168
|
+
}
|
|
169
|
+
try writer.writeAll(",\"contentDesc\":");
|
|
170
|
+
if (node.content_desc) |value| {
|
|
171
|
+
try trace.writeJsonString(writer, value);
|
|
172
|
+
} else {
|
|
173
|
+
try writer.writeAll("null");
|
|
174
|
+
}
|
|
175
|
+
try writer.writeAll(",\"resourceId\":");
|
|
176
|
+
if (node.resource_id) |value| {
|
|
177
|
+
try trace.writeJsonString(writer, value);
|
|
178
|
+
} else {
|
|
179
|
+
try writer.writeAll("null");
|
|
180
|
+
}
|
|
181
|
+
try writer.print(
|
|
182
|
+
",\"bounds\":{{\"x\":{d},\"y\":{d},\"width\":{d},\"height\":{d}}}",
|
|
183
|
+
.{ node.bounds.x, node.bounds.y, node.bounds.width, node.bounds.height },
|
|
184
|
+
);
|
|
185
|
+
try writer.writeAll(",\"enabled\":");
|
|
186
|
+
try writer.writeAll(if (node.enabled) "true" else "false");
|
|
187
|
+
try writer.writeAll(",\"visible\":");
|
|
188
|
+
try writer.writeAll(if (node.visible) "true" else "false");
|
|
189
|
+
try writer.writeAll("}");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
fn nodeMatchesSelectorFields(node: types.UiNode, wanted: selector.Selector) bool {
|
|
193
|
+
if (wanted.id) |id| {
|
|
194
|
+
if (node.resource_id == null or !std.mem.eql(u8, node.resource_id.?, id)) return false;
|
|
195
|
+
}
|
|
196
|
+
if (wanted.text) |text| {
|
|
197
|
+
if (node.text == null or !std.mem.eql(u8, node.text.?, text)) return false;
|
|
198
|
+
}
|
|
199
|
+
if (wanted.text_contains) |needle| {
|
|
200
|
+
if (node.text == null or std.mem.indexOf(u8, node.text.?, needle) == null) return false;
|
|
201
|
+
}
|
|
202
|
+
if (wanted.content_desc) |desc| {
|
|
203
|
+
if (node.content_desc == null or !std.mem.eql(u8, node.content_desc.?, desc)) return false;
|
|
204
|
+
}
|
|
205
|
+
if (wanted.content_desc_contains) |needle| {
|
|
206
|
+
if (node.content_desc == null or std.mem.indexOf(u8, node.content_desc.?, needle) == null) return false;
|
|
207
|
+
}
|
|
208
|
+
if (wanted.class_name) |class_name| {
|
|
209
|
+
if (!std.mem.eql(u8, node.class_name, class_name)) return false;
|
|
210
|
+
}
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
fn selectorTextTarget(wanted: selector.Selector) ?[]const u8 {
|
|
215
|
+
if (wanted.text) |value| return value;
|
|
216
|
+
if (wanted.text_contains) |value| return value;
|
|
217
|
+
if (wanted.content_desc) |value| return value;
|
|
218
|
+
if (wanted.content_desc_contains) |value| return value;
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
fn nodeLabel(node: types.UiNode) ?[]const u8 {
|
|
223
|
+
if (node.text) |value| return value;
|
|
224
|
+
if (node.content_desc) |value| return value;
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
fn isInViewport(node: types.UiNode, viewport: types.Viewport) bool {
|
|
229
|
+
if (node.bounds.width <= 0 or node.bounds.height <= 0) return false;
|
|
230
|
+
if (viewport.width == 0 or viewport.height == 0) return true;
|
|
231
|
+
const right = node.bounds.x + node.bounds.width;
|
|
232
|
+
const bottom = node.bounds.y + node.bounds.height;
|
|
233
|
+
return right > 0 and bottom > 0 and node.bounds.x < @as(i32, @intCast(viewport.width)) and node.bounds.y < @as(i32, @intCast(viewport.height));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
fn textDistance(left: []const u8, right: []const u8) usize {
|
|
237
|
+
if (containsIgnoreCase(left, right) or containsIgnoreCase(right, left)) {
|
|
238
|
+
return if (left.len > right.len) left.len - right.len else right.len - left.len;
|
|
239
|
+
}
|
|
240
|
+
const prefix = commonPrefixIgnoreCase(left, right);
|
|
241
|
+
return @max(left.len, right.len) - prefix;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
fn containsIgnoreCase(haystack: []const u8, needle: []const u8) bool {
|
|
245
|
+
if (needle.len == 0) return true;
|
|
246
|
+
if (needle.len > haystack.len) return false;
|
|
247
|
+
var index: usize = 0;
|
|
248
|
+
while (index + needle.len <= haystack.len) : (index += 1) {
|
|
249
|
+
var matched = true;
|
|
250
|
+
for (needle, 0..) |needle_ch, offset| {
|
|
251
|
+
if (std.ascii.toLower(haystack[index + offset]) != std.ascii.toLower(needle_ch)) {
|
|
252
|
+
matched = false;
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (matched) return true;
|
|
257
|
+
}
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
fn commonPrefixIgnoreCase(left: []const u8, right: []const u8) usize {
|
|
262
|
+
const limit = @min(left.len, right.len);
|
|
263
|
+
var index: usize = 0;
|
|
264
|
+
while (index < limit) : (index += 1) {
|
|
265
|
+
if (std.ascii.toLower(left[index]) != std.ascii.toLower(right[index])) break;
|
|
266
|
+
}
|
|
267
|
+
return index;
|
|
268
|
+
}
|