@aarwitz/tapp 0.16.5 → 0.17.0-rc.10
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/AGENTS.md +35 -21
- package/Harness/OCQAHarnessUITests/ExplorerTests.swift +100 -31
- package/README.md +70 -67
- package/bin/tapp.js +265 -74
- package/browser/app.js +12 -6
- package/docs/BROWSER-PRODUCT.md +75 -0
- package/docs/PRODUCT-ENGINE.md +107 -0
- package/docs/application-model.md +278 -0
- package/docs/scenarios.md +95 -0
- package/mcp-server/src/android-driver.js +88 -3
- package/mcp-server/src/android-explorer.js +83 -14
- package/mcp-server/src/application-model.js +13 -9
- package/mcp-server/src/browser-product.js +1 -1
- package/mcp-server/src/ci-report.js +84 -62
- package/mcp-server/src/ci-setup.js +35 -5
- package/mcp-server/src/enrich.js +1 -1
- package/mcp-server/src/html-report.js +41 -7
- package/mcp-server/src/index.js +198 -72
- package/mcp-server/src/pr-selection.js +4 -3
- package/mcp-server/src/product-execution.js +1 -1
- package/mcp-server/src/product-operations.js +96 -7
- package/mcp-server/src/project-config.js +1 -2
- package/mcp-server/src/project-paths.js +5 -17
- package/mcp-server/src/release-contract.js +3 -3
- package/mcp-server/src/report.js +185 -51
- package/mcp-server/src/task-runtime.js +1 -1
- package/mcp-server/src/web-explorer.js +1 -1
- package/package.json +2 -2
- package/scripts/ci-gate.sh +11 -9
- package/scripts/flow_ai_judge.py +1 -1
- package/scripts/platform-gate.js +11 -5
- package/scripts/quick-capture.sh +72 -38
- package/scripts/run-flow.sh +1 -1
package/scripts/ci-gate.sh
CHANGED
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
#
|
|
4
4
|
# Boots a simulator if needed, installs the app build under test, runs the autonomous exploration
|
|
5
5
|
# harness, replays every committed Flow (deterministic E2E tests), diffs the findings against a
|
|
6
|
-
# stored baseline, writes a GitHub Actions step summary, and exits
|
|
7
|
-
#
|
|
8
|
-
#
|
|
6
|
+
# stored baseline, writes a GitHub Actions step summary, and exits with the public outcome contract:
|
|
7
|
+
# pass 0, deterministic fail 1, infrastructure/usage error 2, inconclusive evidence 3. Absolute
|
|
8
|
+
# blockers and reviewed Flow/Scenario/Contract failures are enforced even without a baseline;
|
|
9
|
+
# baseline comparisons additionally catch new high/critical regressions. Wrapped by ../action.yml
|
|
10
|
+
# for GitHub Actions; equally usable from any other CI or locally.
|
|
9
11
|
#
|
|
10
12
|
# Usage:
|
|
11
13
|
# scripts/ci-gate.sh [--platform ios] --app <path/to/App.app> [--bundle-id <com.example.app>]
|
|
12
|
-
# scripts/ci-gate.sh --platform android --apk <path/to/app.apk> --app-id <com.example.app>
|
|
14
|
+
# scripts/ci-gate.sh --platform android --apk <path/to/app.apk> --app-id <com.example.app> [--serial <adb-serial>]
|
|
13
15
|
# scripts/ci-gate.sh --platform web [--url <http(s)://owned-app>]
|
|
14
16
|
# # omit --url with --project-dir to detect/build/start/stop one owned web target
|
|
15
17
|
# # bundle id is detected from the .app when omitted
|
|
@@ -25,17 +27,18 @@
|
|
|
25
27
|
# [--pr-plan-out <file.json>] # persist the reviewable selection plan
|
|
26
28
|
# [--baseline <file.json>] # prior report to diff against (skipped if absent)
|
|
27
29
|
# [--target-key <stable-id>] # isolates target-specific baselines in monorepos
|
|
28
|
-
# [--fail-on gate|
|
|
30
|
+
# [--fail-on gate|absolute|any] # gate policy (default gate; see ci-report.js)
|
|
29
31
|
# [--json-out <file.json>] # write the full report (use as the next baseline)
|
|
30
32
|
# [--md-out <file.md>] # write the rendered markdown report (for a PR comment)
|
|
31
33
|
# [--device <name>] # simulator device to boot if none is (default "iPhone 16 Pro")
|
|
34
|
+
# [--serial <adb-serial>] # Android emulator/device (default: first connected device)
|
|
32
35
|
#
|
|
33
36
|
# The app must be a SIMULATOR build (xcodebuild ... -destination 'generic/platform=iOS Simulator').
|
|
34
37
|
set -uo pipefail
|
|
35
38
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
36
39
|
|
|
37
40
|
usage() {
|
|
38
|
-
sed -n '2
|
|
41
|
+
sed -n '2,/^set -uo pipefail$/p' "$0" | sed '$d; s/^# \{0,1\}//'
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
PLATFORM="ios" APP_PATH="" BUNDLE_ID="" APK_PATH="" APP_ID="" URL="" WEB_TARGET="" TARGET_KEY="" SERIAL="" ACTIONS=40 TIMEOUT=600 FLOWS="" SCENARIOS="" CONTRACTS="" PROJECT_DIR="" BASELINE="" FAIL_ON="gate" JSON_OUT="" MD_OUT="" DEVICE="iPhone 16 Pro" PR_BASE="" PR_HEAD="HEAD" CHANGED_FILES_FILE="" PR_PLAN_OUT=""
|
|
@@ -74,7 +77,7 @@ done
|
|
|
74
77
|
[[ "$PLATFORM" == "ios" || "$PLATFORM" == "android" || "$PLATFORM" == "web" ]] || { echo "❌ --platform must be ios|android|web" >&2; exit 2; }
|
|
75
78
|
[[ "$ACTIONS" =~ ^[1-9][0-9]*$ ]] || { echo "❌ --actions must be a positive integer" >&2; exit 2; }
|
|
76
79
|
[[ "$TIMEOUT" =~ ^[1-9][0-9]*$ ]] || { echo "❌ --timeout must be a positive integer" >&2; exit 2; }
|
|
77
|
-
[[ "$FAIL_ON" == "gate" || "$FAIL_ON" == "
|
|
80
|
+
[[ "$FAIL_ON" == "gate" || "$FAIL_ON" == "absolute" || "$FAIL_ON" == "any" ]] || { echo "❌ --fail-on must be gate|absolute|any" >&2; exit 2; }
|
|
78
81
|
if [[ -n "$PROJECT_DIR" ]]; then
|
|
79
82
|
[[ -d "$PROJECT_DIR" ]] || { echo "❌ Project directory not found: $PROJECT_DIR" >&2; exit 2; }
|
|
80
83
|
PROJECT_DIR="$(cd "$PROJECT_DIR" && pwd)"
|
|
@@ -82,7 +85,6 @@ fi
|
|
|
82
85
|
TAPP_PROJECT_ARTIFACTS=""
|
|
83
86
|
if [[ -n "$PROJECT_DIR" ]]; then
|
|
84
87
|
[[ -d "$PROJECT_DIR/.tapp" ]] && TAPP_PROJECT_ARTIFACTS="$PROJECT_DIR/.tapp"
|
|
85
|
-
[[ -z "$TAPP_PROJECT_ARTIFACTS" && -d "$PROJECT_DIR/.autotap" ]] && TAPP_PROJECT_ARTIFACTS="$PROJECT_DIR/.autotap"
|
|
86
88
|
fi
|
|
87
89
|
[[ -z "$FLOWS" && -n "$TAPP_PROJECT_ARTIFACTS" && -d "$TAPP_PROJECT_ARTIFACTS/flows" ]] && FLOWS="$TAPP_PROJECT_ARTIFACTS/flows/*.yml"
|
|
88
90
|
[[ "$PLATFORM" == "web" && -z "$SCENARIOS" && -n "$TAPP_PROJECT_ARTIFACTS" && -d "$TAPP_PROJECT_ARTIFACTS/scenarios" ]] && SCENARIOS="$TAPP_PROJECT_ARTIFACTS/scenarios/*.yml"
|
|
@@ -274,7 +276,7 @@ xcrun simctl install "$UDID" "$APP_PATH" || { echo "❌ simctl install failed
|
|
|
274
276
|
# ── Autonomous exploration (quick-capture builds the harness itself if needed).
|
|
275
277
|
step "Explore ($ACTIONS actions, ${TIMEOUT}s watchdog)"
|
|
276
278
|
set +e
|
|
277
|
-
CAPTURE_ROOT="${TAPP_HOME:-$
|
|
279
|
+
CAPTURE_ROOT="${TAPP_HOME:-$ROOT}/captures"
|
|
278
280
|
mkdir -p "$CAPTURE_ROOT"
|
|
279
281
|
CAPTURE_DIR="$(mktemp -d "$CAPTURE_ROOT/ci.XXXXXX")"
|
|
280
282
|
TAPP_CAPTURE_DIR="$CAPTURE_DIR" OCQA_PR_TARGET_JSON="$IOS_PR_TARGET_JSON" "$ROOT/scripts/quick-capture.sh" explore "$BUNDLE_ID" --actions "$ACTIONS" --timeout "$TIMEOUT"
|
package/scripts/flow_ai_judge.py
CHANGED
|
@@ -26,7 +26,7 @@ def _ctx():
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
SSL_CTX = _ctx()
|
|
29
|
-
MODEL = os.environ.get("TAPP_VISION_MODEL"
|
|
29
|
+
MODEL = os.environ.get("TAPP_VISION_MODEL", "claude-haiku-4-5-20251001")
|
|
30
30
|
SYSTEM = (
|
|
31
31
|
"You are a QA test oracle. You are shown one iOS app screenshot and a CLAIM the test asserts about "
|
|
32
32
|
"it. Decide if the claim is TRUE of what is actually visible. Be strict and literal — only pass if "
|
package/scripts/platform-gate.js
CHANGED
|
@@ -37,7 +37,10 @@ for (let i = 2; i < process.argv.length; i += 1) {
|
|
|
37
37
|
else if (key === "--json-out") args.jsonOut = value;
|
|
38
38
|
else if (key === "--md-out") args.mdOut = value;
|
|
39
39
|
else if (key === "--pr-plan") args.prPlan = value;
|
|
40
|
-
else
|
|
40
|
+
else {
|
|
41
|
+
console.error(`Unknown argument: ${key}`);
|
|
42
|
+
process.exit(2);
|
|
43
|
+
}
|
|
41
44
|
}
|
|
42
45
|
if (!["web", "android"].includes(args.platform)) throw new Error("--platform must be web|android");
|
|
43
46
|
if (args.platform === "web" && !args.url && !args.projectDir) throw new Error("Web gate requires --url or --project-dir for managed build/start");
|
|
@@ -94,7 +97,7 @@ if (args.prPlan) {
|
|
|
94
97
|
}
|
|
95
98
|
|
|
96
99
|
let managedRuntime = null;
|
|
97
|
-
let exitCode =
|
|
100
|
+
let exitCode = 2;
|
|
98
101
|
try {
|
|
99
102
|
if (args.platform === "web" && !args.url) {
|
|
100
103
|
const started = await startManagedWebTarget({
|
|
@@ -106,7 +109,7 @@ try {
|
|
|
106
109
|
if (started.error) {
|
|
107
110
|
console.error(`❌ ${started.error}`);
|
|
108
111
|
if (started.details?.remediation) console.error(` ${started.details.remediation}`);
|
|
109
|
-
|
|
112
|
+
exitCode = 2;
|
|
110
113
|
} else {
|
|
111
114
|
managedRuntime = started;
|
|
112
115
|
args.url = started.url;
|
|
@@ -114,7 +117,7 @@ try {
|
|
|
114
117
|
}
|
|
115
118
|
}
|
|
116
119
|
if (args.platform === "web" && !args.url) {
|
|
117
|
-
exitCode =
|
|
120
|
+
exitCode = 2;
|
|
118
121
|
} else {
|
|
119
122
|
const qa = args.platform === "web"
|
|
120
123
|
? await runQaWeb({ url: args.url, maxActions: args.actions, timeout: args.timeout, testEmail: process.env.OCQA_TEST_EMAIL, testPassword: process.env.OCQA_TEST_PASSWORD, seedTargets: prExplorationTargets })
|
|
@@ -122,7 +125,7 @@ try {
|
|
|
122
125
|
testEmail: process.env.OCQA_TEST_EMAIL, testPassword: process.env.OCQA_TEST_PASSWORD, seedTargets: prExplorationTargets });
|
|
123
126
|
if (qa.error) {
|
|
124
127
|
console.error(`❌ ${qa.error}`);
|
|
125
|
-
exitCode =
|
|
128
|
+
exitCode = 2;
|
|
126
129
|
} else {
|
|
127
130
|
const captureDir = qa.structured.capture.path;
|
|
128
131
|
const markers = path.join(captureDir, "ocqa-markers.txt");
|
|
@@ -177,6 +180,9 @@ try {
|
|
|
177
180
|
exitCode = report.status ?? 1;
|
|
178
181
|
}
|
|
179
182
|
}
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.error(`❌ ${error?.message || String(error)}`);
|
|
185
|
+
exitCode = 2;
|
|
180
186
|
} finally {
|
|
181
187
|
if (managedRuntime) {
|
|
182
188
|
await stopManagedWebTarget(managedRuntime);
|
package/scripts/quick-capture.sh
CHANGED
|
@@ -19,7 +19,7 @@ PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
|
19
19
|
# accidentally reuse stale markers). TAPP_HOME redirects all writable output otherwise —
|
|
20
20
|
# set by the `tapp` CLI when running as an installed npm package, where the package dir
|
|
21
21
|
# must stay read-only. Unset (repo dev flow), everything lands in the repo as before.
|
|
22
|
-
TAPP_RUNTIME_HOME="${TAPP_HOME
|
|
22
|
+
TAPP_RUNTIME_HOME="${TAPP_HOME:-}"
|
|
23
23
|
if [[ -n "${TAPP_CAPTURE_DIR:-}" ]]; then
|
|
24
24
|
CAPTURE_DIR="$TAPP_CAPTURE_DIR"
|
|
25
25
|
elif [[ -n "$TAPP_RUNTIME_HOME" ]]; then
|
|
@@ -262,58 +262,92 @@ case "$MODE" in
|
|
|
262
262
|
echo "Running autonomous exploration ($MAX_ACTIONS actions, timeout: ${EXPLORE_TIMEOUT}s, app: $APP_BUNDLE)..."
|
|
263
263
|
ensure_harness_built "$SIM_NAME"
|
|
264
264
|
|
|
265
|
-
#
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
if
|
|
273
|
-
|
|
274
|
-
|
|
265
|
+
# XCUITest can block inside `app.launch()` until the outer watchdog when the target dies in its
|
|
266
|
+
# initializer, producing a misleading timeout after minutes. For an ordinary unconfigured
|
|
267
|
+
# launch, probe the target directly first and verify that the returned PID survives a short
|
|
268
|
+
# settle window. (Configured launch args/env skip this probe because simctl would not reproduce
|
|
269
|
+
# that launch contract.) The harness still performs its own fresh launch for healthy apps.
|
|
270
|
+
PREFLIGHT_CRASH=0
|
|
271
|
+
PREFLIGHT_OUTPUT=""
|
|
272
|
+
if [[ -z "${OCQA_APP_LAUNCH_ARGS_JSON:-}" && -z "${OCQA_APP_LAUNCH_ENV_JSON:-}" ]]; then
|
|
273
|
+
xcrun simctl terminate "$UDID" "$APP_BUNDLE" >/dev/null 2>&1 || true
|
|
274
|
+
PREFLIGHT_OUTPUT="$(xcrun simctl launch "$UDID" "$APP_BUNDLE" 2>&1 || true)"
|
|
275
|
+
PREFLIGHT_PID="$(echo "$PREFLIGHT_OUTPUT" | sed -n 's/.*: \([0-9][0-9]*\)$/\1/p' | tail -1)"
|
|
276
|
+
if [[ -n "$PREFLIGHT_PID" ]]; then
|
|
277
|
+
sleep 2
|
|
278
|
+
if ! kill -0 "$PREFLIGHT_PID" 2>/dev/null; then PREFLIGHT_CRASH=1; fi
|
|
279
|
+
fi
|
|
280
|
+
xcrun simctl terminate "$UDID" "$APP_BUNDLE" >/dev/null 2>&1 || true
|
|
275
281
|
fi
|
|
276
282
|
|
|
277
|
-
# Run exploration with watchdog timeout to avoid silent hangs.
|
|
278
283
|
local_output_file="$CAPTURE_DIR/harness-output.txt"
|
|
279
|
-
run_harness_test "testAutonomousExploration" "$SIM_NAME" "$APP_BUNDLE" "$MAX_ACTIONS" "$EXPLORE_TIMEOUT" > "$local_output_file" 2>&1 &
|
|
280
|
-
HARNESS_PID=$!
|
|
281
|
-
|
|
282
|
-
START_TS=$(date +%s)
|
|
283
284
|
TIMED_OUT=0
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
285
|
+
RECORD_PID=""
|
|
286
|
+
if [[ "$PREFLIGHT_CRASH" -eq 1 ]]; then
|
|
287
|
+
OUTPUT="OCQA_ISSUE:{\"type\":\"crash\",\"severity\":\"critical\",\"title\":\"App crashed during launch preflight\",\"screen\":\"Launch\",\"step\":0}
|
|
288
|
+
OCQA_COMPLETE:{\"actions\":0,\"states\":0,\"issues\":1,\"screens\":\"\",\"outcome\":\"launch_crash\"}"
|
|
289
|
+
printf '%s\n%s\n' "$PREFLIGHT_OUTPUT" "$OUTPUT" > "$local_output_file"
|
|
290
|
+
echo "WARNING: Target process exited during launch preflight; recorded a crash instead of waiting for the exploration timeout." >&2
|
|
291
|
+
else
|
|
292
|
+
|
|
293
|
+
# Start video recording in background
|
|
294
|
+
cleanup_stale_recorders "$UDID"
|
|
295
|
+
if xcrun simctl io "$UDID" recordVideo --codec=h264 "$CAPTURE_DIR/exploration.mov" & then
|
|
296
|
+
RECORD_PID=$!
|
|
297
|
+
fi
|
|
298
|
+
sleep 0.5
|
|
299
|
+
if ! kill -0 "$RECORD_PID" 2>/dev/null; then
|
|
300
|
+
echo "WARNING: Could not start simulator video recording. Continuing without video." >&2
|
|
301
|
+
RECORD_PID=""
|
|
293
302
|
fi
|
|
294
|
-
sleep 2
|
|
295
|
-
done
|
|
296
303
|
|
|
297
|
-
|
|
298
|
-
|
|
304
|
+
# Run exploration with watchdog timeout to avoid silent hangs.
|
|
305
|
+
run_harness_test "testAutonomousExploration" "$SIM_NAME" "$APP_BUNDLE" "$MAX_ACTIONS" "$EXPLORE_TIMEOUT" > "$local_output_file" 2>&1 &
|
|
306
|
+
HARNESS_PID=$!
|
|
307
|
+
|
|
308
|
+
START_TS=$(date +%s)
|
|
309
|
+
while kill -0 "$HARNESS_PID" 2>/dev/null; do
|
|
310
|
+
NOW_TS=$(date +%s)
|
|
311
|
+
ELAPSED=$((NOW_TS - START_TS))
|
|
312
|
+
if [[ "$ELAPSED" -ge "$EXPLORE_TIMEOUT" ]]; then
|
|
313
|
+
TIMED_OUT=1
|
|
314
|
+
kill -TERM "$HARNESS_PID" 2>/dev/null || true
|
|
315
|
+
sleep 2
|
|
316
|
+
kill -KILL "$HARNESS_PID" 2>/dev/null || true
|
|
317
|
+
break
|
|
318
|
+
fi
|
|
319
|
+
sleep 2
|
|
320
|
+
done
|
|
321
|
+
|
|
322
|
+
wait "$HARNESS_PID" 2>/dev/null || true
|
|
323
|
+
OUTPUT="$(cat "$local_output_file" 2>/dev/null || true)"
|
|
299
324
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
325
|
+
# Stop recording
|
|
326
|
+
if [[ -n "$RECORD_PID" ]]; then
|
|
327
|
+
kill -INT "$RECORD_PID" 2>/dev/null || true
|
|
328
|
+
wait "$RECORD_PID" 2>/dev/null || true
|
|
329
|
+
fi
|
|
330
|
+
sleep 1
|
|
304
331
|
fi
|
|
305
|
-
sleep 1
|
|
306
332
|
|
|
307
333
|
# Parse OCQA_ markers
|
|
308
334
|
echo "$OUTPUT" | grep "^OCQA_" > "$CAPTURE_DIR/ocqa-markers.txt" || true
|
|
309
335
|
if [[ "$TIMED_OUT" -eq 1 ]]; then
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
336
|
+
# The caller's wall-clock budget expiring means the evidence is partial; it is not proof that
|
|
337
|
+
# the app itself is slow or hung. Preserve observed coverage in the terminal marker and let
|
|
338
|
+
# report.js classify the run as inconclusive instead of inventing an app finding.
|
|
339
|
+
TIMED_ACTIONS="$(echo "$OUTPUT" | sed -n 's/^OCQA_PROGRESS:{"action":\([0-9][0-9]*\).*/\1/p' | tail -1)"
|
|
340
|
+
TIMED_STATES="$(echo "$OUTPUT" | sed -n 's/^OCQA_PROGRESS:.*"states":\([0-9][0-9]*\).*/\1/p' | tail -1)"
|
|
341
|
+
TIMED_ISSUES="$(echo "$OUTPUT" | grep -c '^OCQA_ISSUE:' || true)"
|
|
342
|
+
TIMED_ACTIONS="${TIMED_ACTIONS:-0}"
|
|
343
|
+
TIMED_STATES="${TIMED_STATES:-0}"
|
|
344
|
+
TIMED_ISSUES="${TIMED_ISSUES:-0}"
|
|
345
|
+
echo "OCQA_COMPLETE:{\"actions\":$TIMED_ACTIONS,\"states\":$TIMED_STATES,\"issues\":$TIMED_ISSUES,\"screens\":\"\",\"timedOut\":true,\"timeoutSeconds\":$EXPLORE_TIMEOUT}" >> "$CAPTURE_DIR/ocqa-markers.txt"
|
|
346
|
+
echo "WARNING: Exploration reached its ${EXPLORE_TIMEOUT}s time budget; evidence is partial" >&2
|
|
313
347
|
fi
|
|
314
348
|
echo "$OUTPUT" > "$CAPTURE_DIR/full-output.txt"
|
|
315
349
|
|
|
316
|
-
COMPLETE_LINE=$(
|
|
350
|
+
COMPLETE_LINE=$(grep "OCQA_COMPLETE" "$CAPTURE_DIR/ocqa-markers.txt" | tail -1 || true)
|
|
317
351
|
if [[ -n "$COMPLETE_LINE" ]]; then
|
|
318
352
|
echo ""
|
|
319
353
|
echo "Exploration complete: $COMPLETE_LINE"
|
package/scripts/run-flow.sh
CHANGED
|
@@ -26,7 +26,7 @@ UDID="$(xcrun simctl list devices booted -j 2>/dev/null | python3 -c 'import sys
|
|
|
26
26
|
[ -z "$UDID" ] && { echo "❌ No booted simulator."; exit 2; }
|
|
27
27
|
# When run through the installed `tapp` CLI, the harness cache lives under TAPP_HOME.
|
|
28
28
|
XCTR=""
|
|
29
|
-
TAPP_RUNTIME_HOME="${TAPP_HOME
|
|
29
|
+
TAPP_RUNTIME_HOME="${TAPP_HOME:-}"
|
|
30
30
|
[ -n "$TAPP_RUNTIME_HOME" ] && XCTR="$(find "$TAPP_RUNTIME_HOME/harness-derived/Build/Products" -name '*.xctestrun' 2>/dev/null | head -1)"
|
|
31
31
|
[ -z "$XCTR" ] && XCTR="$(find "$HOME/Library/Developer/Xcode/DerivedData/OCQAHarness-"*/Build/Products -name '*.xctestrun' 2>/dev/null | head -1)"
|
|
32
32
|
[ -z "$XCTR" ] && XCTR="$(find /tmp/tapp-harness-derived/Build/Products -name '*.xctestrun' 2>/dev/null | head -1)"
|