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
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import argparse
|
|
3
|
+
import json
|
|
4
|
+
import math
|
|
5
|
+
import shlex
|
|
6
|
+
import statistics
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
from collections import defaultdict
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
CONTEXT_FIELDS = ("platform", "device", "appId", "scenario", "appBuild")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def parse_args():
|
|
16
|
+
parser = argparse.ArgumentParser(
|
|
17
|
+
description="Compare benchmark JSONL rows for two runner labels.",
|
|
18
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
19
|
+
epilog=(
|
|
20
|
+
"--evidence-out requires --min-candidate-pass-rate, "
|
|
21
|
+
"--max-candidate-failures, --min-mean-speedup, and "
|
|
22
|
+
"--min-p95-speedup so market-claim evidence includes explicit gates."
|
|
23
|
+
),
|
|
24
|
+
)
|
|
25
|
+
parser.add_argument("--results", required=True, help="Path to a benchmark results.jsonl file.")
|
|
26
|
+
parser.add_argument("--candidate", default="zmr", help="Candidate tool label. Default: zmr.")
|
|
27
|
+
parser.add_argument("--baseline", required=True, help="Baseline tool label to compare against.")
|
|
28
|
+
parser.add_argument("--format", choices=("markdown", "json"), default="markdown", help="Output format.")
|
|
29
|
+
parser.add_argument("--out", help="Optional output file. Defaults to stdout.")
|
|
30
|
+
parser.add_argument("--min-candidate-pass-rate", type=float, help="Minimum candidate pass rate percentage.")
|
|
31
|
+
parser.add_argument("--max-candidate-failures", type=int, help="Maximum allowed candidate failures.")
|
|
32
|
+
parser.add_argument("--min-mean-speedup", type=float, help="Minimum required mean speedup versus baseline.")
|
|
33
|
+
parser.add_argument("--min-p95-speedup", type=float, help="Minimum required p95 speedup versus baseline.")
|
|
34
|
+
parser.add_argument("--evidence-out", help="Optional JSONL file to append a market-claim readiness evidence row.")
|
|
35
|
+
args = parser.parse_args()
|
|
36
|
+
for name in (
|
|
37
|
+
"min_candidate_pass_rate",
|
|
38
|
+
"max_candidate_failures",
|
|
39
|
+
"min_mean_speedup",
|
|
40
|
+
"min_p95_speedup",
|
|
41
|
+
):
|
|
42
|
+
value = getattr(args, name)
|
|
43
|
+
if value is not None and value < 0:
|
|
44
|
+
parser.error(f"--{name.replace('_', '-')} must be non-negative")
|
|
45
|
+
if args.evidence_out:
|
|
46
|
+
missing_gate_args = [
|
|
47
|
+
f"--{name.replace('_', '-')}"
|
|
48
|
+
for name in (
|
|
49
|
+
"min_candidate_pass_rate",
|
|
50
|
+
"max_candidate_failures",
|
|
51
|
+
"min_mean_speedup",
|
|
52
|
+
"min_p95_speedup",
|
|
53
|
+
)
|
|
54
|
+
if getattr(args, name) is None
|
|
55
|
+
]
|
|
56
|
+
if missing_gate_args:
|
|
57
|
+
parser.error(
|
|
58
|
+
"; ".join(f"{name} is required with --evidence-out" for name in missing_gate_args)
|
|
59
|
+
)
|
|
60
|
+
return args
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def is_pass(row):
|
|
64
|
+
if row.get("status") != "ok":
|
|
65
|
+
return False
|
|
66
|
+
trace_status = row.get("traceStatus")
|
|
67
|
+
return trace_status in (None, "passed")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def p95(durations):
|
|
71
|
+
if not durations:
|
|
72
|
+
return 0
|
|
73
|
+
ordered = sorted(durations)
|
|
74
|
+
index = max(0, math.ceil(len(ordered) * 0.95) - 1)
|
|
75
|
+
return ordered[index]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def read_rows(path):
|
|
79
|
+
rows = []
|
|
80
|
+
with Path(path).open(encoding="utf-8") as handle:
|
|
81
|
+
for line_number, line in enumerate(handle, start=1):
|
|
82
|
+
line = line.strip()
|
|
83
|
+
if not line:
|
|
84
|
+
continue
|
|
85
|
+
try:
|
|
86
|
+
row = json.loads(line)
|
|
87
|
+
except json.JSONDecodeError as exc:
|
|
88
|
+
raise SystemExit(f"{path}:{line_number}: invalid json: {exc}") from exc
|
|
89
|
+
if not isinstance(row, dict):
|
|
90
|
+
raise SystemExit(f"{path}:{line_number}: expected object row")
|
|
91
|
+
rows.append(row)
|
|
92
|
+
return rows
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def summarize(tool, rows):
|
|
96
|
+
durations = [int(row.get("durationMs", 0)) for row in rows]
|
|
97
|
+
failures = [row for row in rows if not is_pass(row)]
|
|
98
|
+
passed = len(rows) - len(failures)
|
|
99
|
+
pass_rate = (passed / len(rows) * 100.0) if rows else 0.0
|
|
100
|
+
mean_ms = round(statistics.mean(durations)) if durations else 0
|
|
101
|
+
return {
|
|
102
|
+
"tool": tool,
|
|
103
|
+
"runs": len(rows),
|
|
104
|
+
"passed": passed,
|
|
105
|
+
"failures": len(failures),
|
|
106
|
+
"passRate": pass_rate,
|
|
107
|
+
"meanMs": mean_ms,
|
|
108
|
+
"p95Ms": p95(durations),
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def ratio(baseline_value, candidate_value):
|
|
113
|
+
if baseline_value <= 0 or candidate_value <= 0:
|
|
114
|
+
return None
|
|
115
|
+
return baseline_value / candidate_value
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def percent_delta(candidate_value, baseline_value):
|
|
119
|
+
if baseline_value <= 0:
|
|
120
|
+
return None
|
|
121
|
+
return ((candidate_value - baseline_value) / baseline_value) * 100.0
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def comparison(candidate, baseline):
|
|
125
|
+
return {
|
|
126
|
+
"candidate": candidate,
|
|
127
|
+
"baseline": baseline,
|
|
128
|
+
"meanSpeedup": ratio(baseline["meanMs"], candidate["meanMs"]),
|
|
129
|
+
"p95Speedup": ratio(baseline["p95Ms"], candidate["p95Ms"]),
|
|
130
|
+
"meanDeltaPct": percent_delta(candidate["meanMs"], baseline["meanMs"]),
|
|
131
|
+
"p95DeltaPct": percent_delta(candidate["p95Ms"], baseline["p95Ms"]),
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def benchmark_context(candidate_rows, baseline_rows):
|
|
136
|
+
rows = candidate_rows + baseline_rows
|
|
137
|
+
context = {}
|
|
138
|
+
problems = []
|
|
139
|
+
for field in CONTEXT_FIELDS:
|
|
140
|
+
values = [str(row.get(field, "")).strip() for row in rows]
|
|
141
|
+
concrete = [value for value in values if value]
|
|
142
|
+
unique = sorted(set(concrete))
|
|
143
|
+
if len(concrete) != len(values):
|
|
144
|
+
problems.append(f"{field} missing")
|
|
145
|
+
elif len(unique) != 1:
|
|
146
|
+
problems.append(f"{field} mismatch: {', '.join(unique)}")
|
|
147
|
+
else:
|
|
148
|
+
context[field] = unique[0]
|
|
149
|
+
return {
|
|
150
|
+
"sameContext": not problems,
|
|
151
|
+
"context": context,
|
|
152
|
+
"contextProblems": problems,
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def format_ratio(value):
|
|
157
|
+
return "n/a" if value is None else f"{value:.2f}x"
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def format_pct(value):
|
|
161
|
+
return "n/a" if value is None else f"{value:+.1f}%"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def markdown_report(data):
|
|
165
|
+
candidate = data["candidate"]
|
|
166
|
+
baseline = data["baseline"]
|
|
167
|
+
lines = [
|
|
168
|
+
"# Benchmark Comparison",
|
|
169
|
+
"",
|
|
170
|
+
"| Tool | Runs | Pass rate | Failures | Mean ms | P95 ms |",
|
|
171
|
+
"| --- | ---: | ---: | ---: | ---: | ---: |",
|
|
172
|
+
f"| {candidate['tool']} | {candidate['runs']} | {candidate['passRate']:.2f}% | {candidate['failures']} | {candidate['meanMs']} | {candidate['p95Ms']} |",
|
|
173
|
+
f"| {baseline['tool']} | {baseline['runs']} | {baseline['passRate']:.2f}% | {baseline['failures']} | {baseline['meanMs']} | {baseline['p95Ms']} |",
|
|
174
|
+
"",
|
|
175
|
+
f"- Mean speedup: {format_ratio(data['meanSpeedup'])} ({format_pct(data['meanDeltaPct'])} candidate vs baseline)",
|
|
176
|
+
f"- P95 speedup: {format_ratio(data['p95Speedup'])} ({format_pct(data['p95DeltaPct'])} candidate vs baseline)",
|
|
177
|
+
f"- Same benchmark context: {'yes' if data.get('sameContext') else 'no'}",
|
|
178
|
+
"",
|
|
179
|
+
"Interpretation: negative deltas mean the candidate was faster for that metric. Compare only runs collected on the same host, device state, app build, and scenario.",
|
|
180
|
+
]
|
|
181
|
+
return "\n".join(lines) + "\n"
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def gate_failures(data, args):
|
|
185
|
+
failures = []
|
|
186
|
+
candidate = data["candidate"]
|
|
187
|
+
baseline = data["baseline"]
|
|
188
|
+
if args.evidence_out and candidate["runs"] < 20:
|
|
189
|
+
failures.append(f"candidateRuns {candidate['runs']} below minimum 20")
|
|
190
|
+
if args.evidence_out and baseline["runs"] < 20:
|
|
191
|
+
failures.append(f"baselineRuns {baseline['runs']} below minimum 20")
|
|
192
|
+
if args.min_candidate_pass_rate is not None and candidate["passRate"] < args.min_candidate_pass_rate:
|
|
193
|
+
failures.append(
|
|
194
|
+
f"candidate passRate {candidate['passRate']:.2f}% below minimum {args.min_candidate_pass_rate:.2f}%"
|
|
195
|
+
)
|
|
196
|
+
if args.max_candidate_failures is not None and candidate["failures"] > args.max_candidate_failures:
|
|
197
|
+
failures.append(
|
|
198
|
+
f"candidate failures={candidate['failures']} above maximum {args.max_candidate_failures}"
|
|
199
|
+
)
|
|
200
|
+
if args.min_mean_speedup is not None:
|
|
201
|
+
speedup = data["meanSpeedup"]
|
|
202
|
+
if speedup is None or speedup < args.min_mean_speedup:
|
|
203
|
+
actual = "n/a" if speedup is None else f"{speedup:.2f}x"
|
|
204
|
+
failures.append(f"meanSpeedup {actual} below minimum {args.min_mean_speedup:.2f}x")
|
|
205
|
+
if args.min_p95_speedup is not None:
|
|
206
|
+
speedup = data["p95Speedup"]
|
|
207
|
+
if speedup is None or speedup < args.min_p95_speedup:
|
|
208
|
+
actual = "n/a" if speedup is None else f"{speedup:.2f}x"
|
|
209
|
+
failures.append(f"p95Speedup {actual} below minimum {args.min_p95_speedup:.2f}x")
|
|
210
|
+
if args.evidence_out and not data.get("sameContext"):
|
|
211
|
+
details = "; ".join(data.get("contextProblems", [])) or "missing context"
|
|
212
|
+
failures.append(f"same benchmark context evidence required ({details})")
|
|
213
|
+
return failures
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def write_evidence(args, data, failures, duration_ms):
|
|
217
|
+
if not args.evidence_out:
|
|
218
|
+
return
|
|
219
|
+
path = Path(args.evidence_out)
|
|
220
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
221
|
+
row = {
|
|
222
|
+
"name": "competitive benchmark comparison",
|
|
223
|
+
"status": "failed" if failures else "passed",
|
|
224
|
+
"durationMs": duration_ms,
|
|
225
|
+
"command": " ".join(shlex.quote(part) for part in sys.argv),
|
|
226
|
+
"candidate": args.candidate,
|
|
227
|
+
"baseline": args.baseline,
|
|
228
|
+
"results": args.results,
|
|
229
|
+
"minCandidatePassRate": args.min_candidate_pass_rate,
|
|
230
|
+
"maxCandidateFailures": args.max_candidate_failures,
|
|
231
|
+
"minMeanSpeedup": args.min_mean_speedup,
|
|
232
|
+
"minP95Speedup": args.min_p95_speedup,
|
|
233
|
+
"candidateRuns": data["candidate"]["runs"],
|
|
234
|
+
"baselineRuns": data["baseline"]["runs"],
|
|
235
|
+
"candidatePassRate": data["candidate"]["passRate"],
|
|
236
|
+
"candidateFailures": data["candidate"]["failures"],
|
|
237
|
+
"meanSpeedup": data["meanSpeedup"],
|
|
238
|
+
"p95Speedup": data["p95Speedup"],
|
|
239
|
+
"sameContext": data["sameContext"],
|
|
240
|
+
"context": data["context"],
|
|
241
|
+
}
|
|
242
|
+
if failures:
|
|
243
|
+
row["error"] = "; ".join(failures)
|
|
244
|
+
with path.open("a", encoding="utf-8") as handle:
|
|
245
|
+
handle.write(json.dumps(row, sort_keys=True, separators=(",", ":")) + "\n")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def main():
|
|
249
|
+
started = time.monotonic()
|
|
250
|
+
args = parse_args()
|
|
251
|
+
rows = read_rows(args.results)
|
|
252
|
+
by_tool = defaultdict(list)
|
|
253
|
+
for row in rows:
|
|
254
|
+
by_tool[str(row.get("tool", "unknown"))].append(row)
|
|
255
|
+
|
|
256
|
+
missing = [tool for tool in (args.candidate, args.baseline) if tool not in by_tool]
|
|
257
|
+
if missing:
|
|
258
|
+
print(f"missing benchmark rows for: {', '.join(missing)}", file=sys.stderr)
|
|
259
|
+
return 2
|
|
260
|
+
|
|
261
|
+
data = comparison(
|
|
262
|
+
summarize(args.candidate, by_tool[args.candidate]),
|
|
263
|
+
summarize(args.baseline, by_tool[args.baseline]),
|
|
264
|
+
)
|
|
265
|
+
data.update(benchmark_context(by_tool[args.candidate], by_tool[args.baseline]))
|
|
266
|
+
|
|
267
|
+
if args.format == "json":
|
|
268
|
+
output = json.dumps(data, sort_keys=True) + "\n"
|
|
269
|
+
else:
|
|
270
|
+
output = markdown_report(data)
|
|
271
|
+
|
|
272
|
+
if args.out:
|
|
273
|
+
Path(args.out).write_text(output, encoding="utf-8")
|
|
274
|
+
else:
|
|
275
|
+
sys.stdout.write(output)
|
|
276
|
+
|
|
277
|
+
failures = gate_failures(data, args)
|
|
278
|
+
duration_ms = round((time.monotonic() - started) * 1000)
|
|
279
|
+
write_evidence(args, data, failures, duration_ms)
|
|
280
|
+
if failures:
|
|
281
|
+
for failure in failures:
|
|
282
|
+
print(f"benchmark comparison gate failed: {failure}", file=sys.stderr)
|
|
283
|
+
return 1
|
|
284
|
+
return 0
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
if __name__ == "__main__":
|
|
288
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
5
|
+
OUT=""
|
|
6
|
+
APP_ID="com.example.mobiletest"
|
|
7
|
+
API="35"
|
|
8
|
+
BUILD_TOOLS="35.0.1"
|
|
9
|
+
ANDROID_SDK="${ANDROID_HOME:-$HOME/Library/Android/sdk}"
|
|
10
|
+
DRY_RUN=0
|
|
11
|
+
|
|
12
|
+
usage() {
|
|
13
|
+
cat <<'USAGE'
|
|
14
|
+
Usage:
|
|
15
|
+
scripts/create-android-demo-app.sh --out <dir> [options]
|
|
16
|
+
|
|
17
|
+
Creates a small public native Android demo app and a matching .zmr smoke
|
|
18
|
+
scenario. The generated app is intentionally generic and contains no private
|
|
19
|
+
app references. It uses Android SDK command-line tools directly, so it does not
|
|
20
|
+
need Gradle or network access.
|
|
21
|
+
|
|
22
|
+
Options:
|
|
23
|
+
--out <dir> Output app repository directory. Required.
|
|
24
|
+
--app-id <id> Android application id. Default: com.example.mobiletest.
|
|
25
|
+
--api <level> Android platform API level. Default: 35.
|
|
26
|
+
--build-tools <ver> Android build-tools version. Default: 35.0.1.
|
|
27
|
+
--android-sdk <path> Android SDK root. Default: ANDROID_HOME or ~/Library/Android/sdk.
|
|
28
|
+
--dry-run Print commands without executing them.
|
|
29
|
+
-h, --help Show this help.
|
|
30
|
+
|
|
31
|
+
After generation:
|
|
32
|
+
adb install -r <dir>/build/app-debug.apk
|
|
33
|
+
zmr run <dir>/.zmr/android-smoke.json --device emulator-5554 --app-id com.example.mobiletest --trace-dir <dir>/traces/android-demo
|
|
34
|
+
USAGE
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
die() {
|
|
38
|
+
echo "error: $*" >&2
|
|
39
|
+
exit 2
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
require_value() {
|
|
43
|
+
local flag="$1"
|
|
44
|
+
local value="${2-}"
|
|
45
|
+
if [[ -z "$value" || "$value" == --* ]]; then
|
|
46
|
+
die "$flag requires a value"
|
|
47
|
+
fi
|
|
48
|
+
printf '%s\n' "$value"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
quote_cmd() {
|
|
52
|
+
local quoted=()
|
|
53
|
+
local arg
|
|
54
|
+
for arg in "$@"; do
|
|
55
|
+
quoted+=("$(printf '%q' "$arg")")
|
|
56
|
+
done
|
|
57
|
+
printf '%s\n' "${quoted[*]}"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
run() {
|
|
61
|
+
echo "+ $(quote_cmd "$@")"
|
|
62
|
+
if [[ "$DRY_RUN" -eq 0 ]]; then
|
|
63
|
+
"$@"
|
|
64
|
+
fi
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
write_file() {
|
|
68
|
+
local path="$1"
|
|
69
|
+
local content="$2"
|
|
70
|
+
echo "+ write $path"
|
|
71
|
+
if [[ "$DRY_RUN" -eq 0 ]]; then
|
|
72
|
+
mkdir -p "$(dirname "$path")"
|
|
73
|
+
printf '%s' "$content" > "$path"
|
|
74
|
+
fi
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
while [[ $# -gt 0 ]]; do
|
|
78
|
+
case "$1" in
|
|
79
|
+
--out)
|
|
80
|
+
OUT="$(require_value "$1" "${2-}")"
|
|
81
|
+
shift 2
|
|
82
|
+
;;
|
|
83
|
+
--app-id)
|
|
84
|
+
APP_ID="$(require_value "$1" "${2-}")"
|
|
85
|
+
shift 2
|
|
86
|
+
;;
|
|
87
|
+
--api)
|
|
88
|
+
API="$(require_value "$1" "${2-}")"
|
|
89
|
+
shift 2
|
|
90
|
+
;;
|
|
91
|
+
--build-tools)
|
|
92
|
+
BUILD_TOOLS="$(require_value "$1" "${2-}")"
|
|
93
|
+
shift 2
|
|
94
|
+
;;
|
|
95
|
+
--android-sdk)
|
|
96
|
+
ANDROID_SDK="$(require_value "$1" "${2-}")"
|
|
97
|
+
shift 2
|
|
98
|
+
;;
|
|
99
|
+
--dry-run)
|
|
100
|
+
DRY_RUN=1
|
|
101
|
+
shift
|
|
102
|
+
;;
|
|
103
|
+
-h|--help)
|
|
104
|
+
usage
|
|
105
|
+
exit 0
|
|
106
|
+
;;
|
|
107
|
+
*)
|
|
108
|
+
die "unknown argument: $1"
|
|
109
|
+
;;
|
|
110
|
+
esac
|
|
111
|
+
done
|
|
112
|
+
|
|
113
|
+
[[ -n "$OUT" ]] || die "--out is required"
|
|
114
|
+
[[ "$APP_ID" =~ ^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)+$ ]] || die "--app-id must be a Java-style package id"
|
|
115
|
+
[[ "$API" =~ ^[0-9]+$ ]] || die "--api must be an integer"
|
|
116
|
+
[[ -n "$BUILD_TOOLS" ]] || die "--build-tools must be non-empty"
|
|
117
|
+
|
|
118
|
+
if [[ "$OUT" != /* ]]; then
|
|
119
|
+
OUT="$(pwd -P)/$OUT"
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
ANDROID_DIR="$OUT/android"
|
|
123
|
+
SRC_DIR="$ANDROID_DIR/src/dev/zmr/demo"
|
|
124
|
+
RES_DIR="$ANDROID_DIR/res"
|
|
125
|
+
BUILD_DIR="$OUT/build"
|
|
126
|
+
GEN_DIR="$BUILD_DIR/generated"
|
|
127
|
+
CLASSES_DIR="$BUILD_DIR/classes"
|
|
128
|
+
DEX_DIR="$BUILD_DIR/dex"
|
|
129
|
+
COMPILED_RES="$BUILD_DIR/compiled-res.zip"
|
|
130
|
+
UNSIGNED_APK="$BUILD_DIR/app-unsigned.apk"
|
|
131
|
+
SIGNED_APK="$BUILD_DIR/app-debug.apk"
|
|
132
|
+
KEYSTORE="$BUILD_DIR/debug.keystore"
|
|
133
|
+
ANDROID_JAR="$ANDROID_SDK/platforms/android-$API/android.jar"
|
|
134
|
+
BUILD_TOOLS_DIR="$ANDROID_SDK/build-tools/$BUILD_TOOLS"
|
|
135
|
+
AAPT2="$BUILD_TOOLS_DIR/aapt2"
|
|
136
|
+
D8="$BUILD_TOOLS_DIR/d8"
|
|
137
|
+
APKSIGNER="$BUILD_TOOLS_DIR/apksigner"
|
|
138
|
+
ZMR_BIN="${ZMR_BIN:-}"
|
|
139
|
+
|
|
140
|
+
echo "Android demo app: $OUT"
|
|
141
|
+
echo "Android demo APK: $SIGNED_APK"
|
|
142
|
+
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
143
|
+
echo "DRY RUN: commands will be printed but not executed"
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
if [[ "$DRY_RUN" -eq 0 ]]; then
|
|
147
|
+
[[ -f "$ANDROID_JAR" ]] || die "android.jar not found: $ANDROID_JAR"
|
|
148
|
+
[[ -x "$AAPT2" ]] || die "aapt2 not found: $AAPT2"
|
|
149
|
+
[[ -x "$D8" ]] || die "d8 not found: $D8"
|
|
150
|
+
[[ -x "$APKSIGNER" ]] || die "apksigner not found: $APKSIGNER"
|
|
151
|
+
command -v javac >/dev/null 2>&1 || die "javac is required"
|
|
152
|
+
command -v keytool >/dev/null 2>&1 || die "keytool is required"
|
|
153
|
+
command -v zip >/dev/null 2>&1 || die "zip is required"
|
|
154
|
+
fi
|
|
155
|
+
|
|
156
|
+
if [[ "$DRY_RUN" -eq 0 ]]; then
|
|
157
|
+
rm -rf "$BUILD_DIR"
|
|
158
|
+
fi
|
|
159
|
+
run mkdir -p "$SRC_DIR" "$RES_DIR/values" "$BUILD_DIR" "$GEN_DIR" "$CLASSES_DIR" "$DEX_DIR" "$OUT/.zmr"
|
|
160
|
+
|
|
161
|
+
write_file "$ANDROID_DIR/AndroidManifest.xml" "$(cat <<EOF
|
|
162
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
163
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="$APP_ID">
|
|
164
|
+
<uses-sdk android:minSdkVersion="23" android:targetSdkVersion="$API" />
|
|
165
|
+
<application android:theme="@style/AppTheme" android:label="ZMR Android Demo" android:allowBackup="false" android:supportsRtl="true">
|
|
166
|
+
<activity android:name="dev.zmr.demo.MainActivity" android:exported="true">
|
|
167
|
+
<intent-filter>
|
|
168
|
+
<action android:name="android.intent.action.MAIN" />
|
|
169
|
+
<category android:name="android.intent.category.LAUNCHER" />
|
|
170
|
+
</intent-filter>
|
|
171
|
+
<intent-filter>
|
|
172
|
+
<action android:name="android.intent.action.VIEW" />
|
|
173
|
+
<category android:name="android.intent.category.DEFAULT" />
|
|
174
|
+
<category android:name="android.intent.category.BROWSABLE" />
|
|
175
|
+
<data android:scheme="exampleapp" />
|
|
176
|
+
</intent-filter>
|
|
177
|
+
</activity>
|
|
178
|
+
</application>
|
|
179
|
+
</manifest>
|
|
180
|
+
EOF
|
|
181
|
+
)"
|
|
182
|
+
|
|
183
|
+
write_file "$RES_DIR/values/styles.xml" "$(cat <<'EOF'
|
|
184
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
185
|
+
<resources>
|
|
186
|
+
<style name="AppTheme" parent="android:style/Theme.Material.Light.NoActionBar">
|
|
187
|
+
<item name="android:fontFamily">sans</item>
|
|
188
|
+
<item name="android:windowLightStatusBar">true</item>
|
|
189
|
+
<item name="android:colorAccent">#2563EB</item>
|
|
190
|
+
</style>
|
|
191
|
+
</resources>
|
|
192
|
+
EOF
|
|
193
|
+
)"
|
|
194
|
+
|
|
195
|
+
write_file "$RES_DIR/values/ids.xml" "$(cat <<'EOF'
|
|
196
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
197
|
+
<resources>
|
|
198
|
+
<item name="demo_title" type="id" />
|
|
199
|
+
<item name="continue_button" type="id" />
|
|
200
|
+
<item name="demo_input" type="id" />
|
|
201
|
+
<item name="demo_status" type="id" />
|
|
202
|
+
</resources>
|
|
203
|
+
EOF
|
|
204
|
+
)"
|
|
205
|
+
|
|
206
|
+
write_file "$SRC_DIR/MainActivity.java" "$(cat <<EOF
|
|
207
|
+
package dev.zmr.demo;
|
|
208
|
+
|
|
209
|
+
import android.app.Activity;
|
|
210
|
+
import android.graphics.Color;
|
|
211
|
+
import android.net.Uri;
|
|
212
|
+
import android.os.Bundle;
|
|
213
|
+
import android.view.View;
|
|
214
|
+
import android.view.Gravity;
|
|
215
|
+
import android.view.inputmethod.InputMethodManager;
|
|
216
|
+
import android.content.Context;
|
|
217
|
+
import android.widget.Button;
|
|
218
|
+
import android.widget.EditText;
|
|
219
|
+
import android.widget.LinearLayout;
|
|
220
|
+
import android.widget.TextView;
|
|
221
|
+
|
|
222
|
+
public class MainActivity extends Activity {
|
|
223
|
+
private TextView status;
|
|
224
|
+
private EditText input;
|
|
225
|
+
|
|
226
|
+
@Override
|
|
227
|
+
protected void onCreate(Bundle savedInstanceState) {
|
|
228
|
+
super.onCreate(savedInstanceState);
|
|
229
|
+
|
|
230
|
+
LinearLayout layout = new LinearLayout(this);
|
|
231
|
+
layout.setOrientation(LinearLayout.VERTICAL);
|
|
232
|
+
layout.setGravity(Gravity.CENTER_HORIZONTAL);
|
|
233
|
+
int padding = dp(24);
|
|
234
|
+
layout.setPadding(padding, padding, padding, padding);
|
|
235
|
+
|
|
236
|
+
TextView title = new TextView(this);
|
|
237
|
+
title.setId(R.id.demo_title);
|
|
238
|
+
title.setText("ZMR Android Demo");
|
|
239
|
+
title.setTextSize(24);
|
|
240
|
+
title.setTextColor(Color.rgb(17, 24, 39));
|
|
241
|
+
title.setGravity(Gravity.CENTER);
|
|
242
|
+
layout.addView(title, new LinearLayout.LayoutParams(-1, -2));
|
|
243
|
+
|
|
244
|
+
Button button = new Button(this);
|
|
245
|
+
button.setId(R.id.continue_button);
|
|
246
|
+
button.setText("Continue");
|
|
247
|
+
layout.addView(button, new LinearLayout.LayoutParams(-1, dp(56)));
|
|
248
|
+
|
|
249
|
+
input = new EditText(this);
|
|
250
|
+
input.setId(R.id.demo_input);
|
|
251
|
+
input.setHint("Type here");
|
|
252
|
+
input.setSingleLine(true);
|
|
253
|
+
layout.addView(input, new LinearLayout.LayoutParams(-1, dp(56)));
|
|
254
|
+
|
|
255
|
+
status = new TextView(this);
|
|
256
|
+
status.setId(R.id.demo_status);
|
|
257
|
+
status.setText("Ready");
|
|
258
|
+
status.setTextSize(18);
|
|
259
|
+
status.setGravity(Gravity.CENTER);
|
|
260
|
+
layout.addView(status, new LinearLayout.LayoutParams(-1, -2));
|
|
261
|
+
|
|
262
|
+
button.setOnClickListener(new View.OnClickListener() {
|
|
263
|
+
@Override
|
|
264
|
+
public void onClick(View view) {
|
|
265
|
+
status.setText("Continue tapped");
|
|
266
|
+
input.requestFocus();
|
|
267
|
+
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
|
|
268
|
+
if (imm != null) {
|
|
269
|
+
imm.showSoftInput(input, InputMethodManager.SHOW_IMPLICIT);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
Uri data = getIntent().getData();
|
|
275
|
+
if (data != null) {
|
|
276
|
+
status.setText("Deep link opened");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
setContentView(layout);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private int dp(int value) {
|
|
283
|
+
return (int) (value * getResources().getDisplayMetrics().density + 0.5f);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
EOF
|
|
287
|
+
)"
|
|
288
|
+
|
|
289
|
+
write_file "$OUT/.zmr/android-smoke.json" "$(cat <<EOF
|
|
290
|
+
{
|
|
291
|
+
"name": "ZMR Android demo smoke",
|
|
292
|
+
"appId": "$APP_ID",
|
|
293
|
+
"steps": [
|
|
294
|
+
{ "action": "launch" },
|
|
295
|
+
{ "action": "waitVisible", "selector": { "text": "ZMR Android Demo" }, "timeoutMs": 30000 },
|
|
296
|
+
{ "action": "tap", "selector": { "resourceId": "$APP_ID:id/continue_button" } },
|
|
297
|
+
{ "action": "waitVisible", "selector": { "text": "Continue tapped" }, "timeoutMs": 10000 },
|
|
298
|
+
{ "action": "typeText", "selector": { "resourceId": "$APP_ID:id/demo_input" }, "text": "hello from zmr" },
|
|
299
|
+
{ "action": "snapshot" }
|
|
300
|
+
]
|
|
301
|
+
}
|
|
302
|
+
EOF
|
|
303
|
+
)"
|
|
304
|
+
|
|
305
|
+
run "$AAPT2" compile --dir "$RES_DIR" -o "$COMPILED_RES"
|
|
306
|
+
run "$AAPT2" link -o "$UNSIGNED_APK" -I "$ANDROID_JAR" --manifest "$ANDROID_DIR/AndroidManifest.xml" -R "$COMPILED_RES" --java "$GEN_DIR" --custom-package dev.zmr.demo --auto-add-overlay
|
|
307
|
+
run javac -source 1.8 -target 1.8 -bootclasspath "$ANDROID_JAR" -d "$CLASSES_DIR" "$GEN_DIR/dev/zmr/demo/R.java" "$SRC_DIR/MainActivity.java"
|
|
308
|
+
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
309
|
+
CLASS_FILES=(
|
|
310
|
+
"$CLASSES_DIR/dev/zmr/demo/R.class"
|
|
311
|
+
"$CLASSES_DIR/dev/zmr/demo/MainActivity.class"
|
|
312
|
+
"$CLASSES_DIR/dev/zmr/demo/MainActivity\$1.class"
|
|
313
|
+
)
|
|
314
|
+
else
|
|
315
|
+
CLASS_FILES=()
|
|
316
|
+
while IFS= read -r class_file; do
|
|
317
|
+
CLASS_FILES+=("$class_file")
|
|
318
|
+
done < <(find "$CLASSES_DIR" -name '*.class' -print | sort)
|
|
319
|
+
[[ "${#CLASS_FILES[@]}" -gt 0 ]] || die "no compiled Java classes found in $CLASSES_DIR"
|
|
320
|
+
fi
|
|
321
|
+
|
|
322
|
+
run "$D8" --lib "$ANDROID_JAR" --min-api 23 --output "$DEX_DIR" "${CLASS_FILES[@]}"
|
|
323
|
+
run zip -j "$UNSIGNED_APK" "$DEX_DIR/classes.dex"
|
|
324
|
+
run keytool -genkeypair -keystore "$KEYSTORE" -storepass android -keypass android -alias zmrdebug -keyalg RSA -keysize 2048 -validity 10000 -dname "CN=ZMR Android Demo,O=ZMR,C=US"
|
|
325
|
+
run "$APKSIGNER" sign --ks "$KEYSTORE" --ks-key-alias zmrdebug --ks-pass pass:android --key-pass pass:android --out "$SIGNED_APK" "$UNSIGNED_APK"
|
|
326
|
+
if [[ -z "$ZMR_BIN" ]]; then
|
|
327
|
+
if [[ -x "$ROOT/zig-out/bin/zmr" ]]; then
|
|
328
|
+
ZMR_BIN="$ROOT/zig-out/bin/zmr"
|
|
329
|
+
elif command -v zmr >/dev/null 2>&1; then
|
|
330
|
+
ZMR_BIN="$(command -v zmr)"
|
|
331
|
+
fi
|
|
332
|
+
fi
|
|
333
|
+
|
|
334
|
+
if [[ -n "$ZMR_BIN" ]]; then
|
|
335
|
+
run "$ZMR_BIN" validate "$OUT/.zmr/android-smoke.json"
|
|
336
|
+
else
|
|
337
|
+
echo "warning: skipped scenario validation because zmr was not found; run 'zmr validate $OUT/.zmr/android-smoke.json' after installation" >&2
|
|
338
|
+
fi
|
|
339
|
+
|
|
340
|
+
echo "created Android demo app at $OUT"
|
|
341
|
+
echo "apk: $SIGNED_APK"
|
|
342
|
+
echo "scenario: $OUT/.zmr/android-smoke.json"
|