mirror of
https://github.com/mattermost/mattermost-mobile.git
synced 2026-11-06 10:50:13 +00:00
test(e2e): fix markdown image precondition, shard-completeness gate, and Detox/Maestro flakes
547 lines
21 KiB
Bash
Executable File
547 lines
21 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Run Maestro flows in isolated batches for CI.
|
|
#
|
|
# PR exclusions: detox/maestro/config/exclude_tags.json (Zephyr tiers: README.md)
|
|
#
|
|
# One batch failure (e.g. iOS SFSafariViewController wedging the driver) must not
|
|
# prevent unrelated flows from running. Each batch is a separate `maestro test`
|
|
# invocation; JUnit XML files are merged afterward.
|
|
#
|
|
# Usage (from repo root):
|
|
# source detox/maestro/.maestro-test-env.sh
|
|
# detox/maestro/scripts/run_ci_batches.sh --platform ios --device "$SIMULATOR_ID"
|
|
# detox/maestro/scripts/run_ci_batches.sh --platform android
|
|
#
|
|
# Environment:
|
|
# MAESTRO_BIN — default ~/.maestro/bin/maestro
|
|
# MAESTRO_APP_ID — default com.mattermost.rnbeta
|
|
# FLOW_PATH — optional; space-separated flow dirs (default: detox/maestro/flows/* categories)
|
|
# SITE_1_URL, TEST_* — passed through to maestro --env
|
|
# ADMIN_TOKEN — re-applies the server config baseline at startup and enables
|
|
# Calls in the test channel; passed through to maestro --env
|
|
# MAESTRO_DRIVER_STARTUP_TIMEOUT — default 180000 (Maestro CI recommendation)
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
|
cd "$REPO_ROOT"
|
|
|
|
PLATFORM=""
|
|
DEVICE_ARGS=()
|
|
OUTPUT_DIR="build"
|
|
ARTIFACTS_DIR="build/maestro-artifacts"
|
|
# NOT maestro-report.xml. The workflow's parse step builds that file itself with
|
|
# mergeMaestroBatchReportsFromDir, which globs maestro-batch-*.xml AND
|
|
# maestro-report-*.xml so the config-gated flows run from dedicated steps
|
|
# (MM-T67856_4, MM-T3261_1, MM-T3261_2) are counted in the gate and in TSIO.
|
|
# That helper returns early if its output already exists, so writing
|
|
# maestro-report.xml here silently excluded those flows from the pass/fail
|
|
# decision. This name is outside the helper's glob, so the batches are merged
|
|
# exactly once.
|
|
MERGED_XML="$OUTPUT_DIR/maestro-batches-merged.xml"
|
|
MAESTRO_BIN="${MAESTRO_BIN:-$HOME/.maestro/bin/maestro}"
|
|
MAESTRO_APP_ID="${MAESTRO_APP_ID:-com.mattermost.rnbeta}"
|
|
export MAESTRO_DRIVER_STARTUP_TIMEOUT="${MAESTRO_DRIVER_STARTUP_TIMEOUT:-180000}"
|
|
|
|
usage() {
|
|
echo "Usage: $0 --platform ios|android [--device UDID]"
|
|
exit 1
|
|
}
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--platform) PLATFORM="$2"; shift 2 ;;
|
|
--device) DEVICE_ARGS=(--device "$2"); shift 2 ;;
|
|
-h|--help) usage ;;
|
|
*) echo "Unknown arg: $1"; usage ;;
|
|
esac
|
|
done
|
|
|
|
[[ -n "$PLATFORM" ]] || usage
|
|
[[ -x "$MAESTRO_BIN" ]] || { echo "Maestro not found at $MAESTRO_BIN"; exit 1; }
|
|
[[ -n "${SITE_1_URL:-}" ]] || { echo "SITE_1_URL is required for Maestro server connect" >&2; exit 1; }
|
|
[[ -n "${TEST_USER_EMAIL:-}" ]] || { echo "TEST_USER_EMAIL is required" >&2; exit 1; }
|
|
[[ -n "${TEST_USER_PASSWORD:-}" ]] || { echo "TEST_USER_PASSWORD is required" >&2; exit 1; }
|
|
|
|
mkdir -p "$OUTPUT_DIR" "$ARTIFACTS_DIR"
|
|
|
|
# shellcheck source=detox/maestro/lib/timezone_region.sh
|
|
source "$REPO_ROOT/detox/maestro/lib/timezone_region.sh"
|
|
SIMULATOR_TIMEZONE="${SIMULATOR_TIMEZONE:-America/New_York}"
|
|
EXPECTED_TIMEZONE_REGION="${EXPECTED_TIMEZONE_REGION:-$(timezone_region_from_iana "$SIMULATOR_TIMEZONE")}"
|
|
|
|
EXCLUDE_TAGS_FILE="$REPO_ROOT/detox/maestro/config/exclude_tags.json"
|
|
load_exclude_tags() {
|
|
# Emit comma-joined exclude tags from default + platform key in exclude_tags.json.
|
|
[[ -f "$EXCLUDE_TAGS_FILE" ]] || { echo ""; return 0; }
|
|
PLATFORM="$PLATFORM" node -e "
|
|
const cfg = require('$EXCLUDE_TAGS_FILE');
|
|
const platform = process.env.PLATFORM || '';
|
|
const tags = [...(cfg.default || []), ...(cfg[platform] || [])];
|
|
process.stdout.write(tags.join(','));
|
|
"
|
|
}
|
|
|
|
build_maestro_env_args() {
|
|
maestro_env_args=(
|
|
--env "SITE_1_URL=${SITE_1_URL:-}"
|
|
--env "TEST_USER_EMAIL=${TEST_USER_EMAIL:-}"
|
|
--env "TEST_USER_PASSWORD=${TEST_USER_PASSWORD:-}"
|
|
--env "TEST_CHANNEL_NAME=${TEST_CHANNEL_NAME:-}"
|
|
--env "TEST_CHANNEL_ID=${TEST_CHANNEL_ID:-}"
|
|
--env "TEST_TEAM_NAME=${TEST_TEAM_NAME:-}"
|
|
--env "ADMIN_TOKEN=${ADMIN_TOKEN:-}"
|
|
--env "MAESTRO_APP_ID=${MAESTRO_APP_ID}"
|
|
--env "MAESTRO_CI=true"
|
|
--env "SIMULATOR_TIMEZONE=${SIMULATOR_TIMEZONE}"
|
|
--env "EXPECTED_TIMEZONE_REGION=${EXPECTED_TIMEZONE_REGION}"
|
|
--env "SYNC_TOKEN=${SYNC_TOKEN:-}"
|
|
)
|
|
}
|
|
|
|
build_maestro_env_args
|
|
|
|
platform_args=()
|
|
[[ "$PLATFORM" == "android" ]] && platform_args=(--platform android)
|
|
|
|
DEFAULT_FLOW_PATH="detox/maestro/flows/timezone detox/maestro/flows/channels detox/maestro/flows/account detox/maestro/flows/calls"
|
|
|
|
flow_sort_key() {
|
|
case "$1" in
|
|
detox/maestro/flows/timezone/*) echo "1:$1" ;;
|
|
detox/maestro/flows/channels/*) echo "2:$1" ;;
|
|
detox/maestro/flows/account/*) echo "3:$1" ;;
|
|
detox/maestro/flows/calls/*) echo "4:$1" ;;
|
|
*) echo "9:$1" ;;
|
|
esac
|
|
}
|
|
|
|
EXCLUDE_TAGS_STR=""
|
|
EXCLUDE_TAGS_STR=$(load_exclude_tags)
|
|
|
|
should_skip_flow() {
|
|
local flow=$1
|
|
local base=${flow##*/}
|
|
[[ "$flow" == *"/multi_device/"* ]] && return 0
|
|
[[ "$base" == _* ]] && return 0
|
|
[[ "$base" == *_picker.yml ]] && return 0
|
|
[[ "$base" == "attach_logs_disabled_when_download_logs_off.yml" ]] && return 0
|
|
[[ "$base" == "file_type_preview.yml" ]] && return 0
|
|
[[ "$base" == "start_call.yml" ]] && return 0
|
|
# iOS Simulator cannot reliably run CallKit + WebRTC (Maestro CI run 28306039412:
|
|
# call_ui_permission, leave_call, mute_unmute fail waiting for id mute-unmute).
|
|
# See CallKitProvider.swift — "RTCAudioSession may never activate on CI simulators
|
|
# and peer connection times out before CurrentCallBar appears."
|
|
# libraries/@mattermost/calls-native/ios/Source/Managers/CallKitProvider.swift
|
|
[[ "$PLATFORM" == "ios" && "$flow" == *"/calls/"* ]] && return 0
|
|
# Skip flows whose tags appear in the platform-specific exclude list.
|
|
if [[ -n "$EXCLUDE_TAGS_STR" && -f "$flow" ]]; then
|
|
local flow_tags
|
|
flow_tags=$(awk '/^tags:/{found=1; next} found{if (!/^ - /) exit; print $2}' "$flow" 2>/dev/null || true)
|
|
local tag
|
|
for tag in $flow_tags; do
|
|
[[ ",$EXCLUDE_TAGS_STR," == *",$tag,"* ]] && return 0
|
|
done
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
BATCHES=()
|
|
flow_path="${FLOW_PATH:-}"
|
|
[[ -z "$flow_path" ]] && flow_path="$DEFAULT_FLOW_PATH"
|
|
read -r -a flow_entries <<< "$flow_path"
|
|
for entry in "${flow_entries[@]}"; do
|
|
if [[ -d "$entry" ]]; then
|
|
while IFS= read -r flow; do
|
|
should_skip_flow "$flow" && continue
|
|
BATCHES+=("$(flow_sort_key "$flow")")
|
|
done < <(find "$entry" -maxdepth 1 -name '*.yml' | sort)
|
|
elif [[ -f "$entry" ]]; then
|
|
should_skip_flow "$entry" || BATCHES+=("$(flow_sort_key "$entry")")
|
|
else
|
|
echo "Flow path not found: $entry" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
# Sort by category order, then strip sort prefix.
|
|
if ((${#BATCHES[@]})); then
|
|
sorted=()
|
|
while IFS= read -r line; do
|
|
sorted+=("${line#*:}")
|
|
done < <(printf '%s\n' "${BATCHES[@]}" | sort)
|
|
BATCHES=("${sorted[@]}")
|
|
fi
|
|
|
|
if ((${#BATCHES[@]} == 0)); then
|
|
echo "No Maestro flows to run (FLOW_PATH=$flow_path)" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "==> Running ${#BATCHES[@]} Maestro flow(s)"
|
|
|
|
ensure_ios_simulator_healthy() {
|
|
[[ "$PLATFORM" != "ios" ]] && return 0
|
|
local udid="${DEVICE_ARGS[1]:-}"
|
|
[[ -n "$udid" ]] || return 0
|
|
|
|
if xcrun simctl listapps "$udid" &>/dev/null; then
|
|
return 0
|
|
fi
|
|
|
|
echo "==> iOS simulator unhealthy — rebooting $udid"
|
|
xcrun simctl shutdown "$udid" 2>/dev/null || true
|
|
xcrun simctl boot "$udid" 2>/dev/null || true
|
|
set +e
|
|
PREBOOT_SKIP_PREWARM="${PREBOOT_SKIP_PREWARM:-1}" \
|
|
DEVICE_NAME="${E2E_IOS_DEVICE_NAME:-iPhone 17 Pro}" \
|
|
DEVICE_OS_VERSION="${E2E_IOS_DEVICE_OS_VERSION:-iOS 26.2}" \
|
|
bash detox/scripts/preboot_ios_simulator.sh
|
|
set -e
|
|
for _ in $(seq 1 30); do
|
|
xcrun simctl listapps "$udid" &>/dev/null && return 0
|
|
sleep 2
|
|
done
|
|
echo "Warning: simulator may still be unhealthy after reboot"
|
|
}
|
|
|
|
ensure_android_app_launchable() {
|
|
[[ "$PLATFORM" != "android" ]] && return 0
|
|
command -v adb >/dev/null 2>&1 || return 0
|
|
|
|
echo "==> Preparing Android app $MAESTRO_APP_ID for Maestro launch"
|
|
adb shell am force-stop "$MAESTRO_APP_ID" 2>/dev/null || true
|
|
adb reverse tcp:8081 tcp:8081 2>/dev/null || true
|
|
# Re-assert after adb root (timezone prep) — Google Autofill overlays hide form testIDs.
|
|
adb shell settings put secure autofill_service null 2>/dev/null || true
|
|
}
|
|
|
|
grant_ios_calls_permissions() {
|
|
[[ "$PLATFORM" != "ios" ]] && return 0
|
|
local udid="${DEVICE_ARGS[1]:-}"
|
|
[[ -n "$udid" ]] || return 0
|
|
|
|
echo "==> Re-granting iOS microphone/camera for Calls ($MAESTRO_APP_ID)"
|
|
xcrun simctl privacy "$udid" grant microphone "$MAESTRO_APP_ID" 2>/dev/null || true
|
|
xcrun simctl privacy "$udid" grant camera "$MAESTRO_APP_ID" 2>/dev/null || true
|
|
}
|
|
|
|
grant_android_calls_permissions() {
|
|
[[ "$PLATFORM" != "android" ]] && return 0
|
|
command -v adb >/dev/null 2>&1 || return 0
|
|
|
|
echo "==> Re-granting Android microphone/camera for Calls ($MAESTRO_APP_ID)"
|
|
adb shell pm grant "$MAESTRO_APP_ID" android.permission.RECORD_AUDIO 2>/dev/null || true
|
|
adb shell pm grant "$MAESTRO_APP_ID" android.permission.CAMERA 2>/dev/null || true
|
|
}
|
|
|
|
# Idempotent and non-destructive: re-asserts only the state a run requires, so a shared
|
|
# server or a crashed earlier job cannot leak config into this one. Called once before any
|
|
# batch runs. AllowDownloadLogs is intentionally NOT included here: MM-T67856_4 flips it in
|
|
# its own onFlowStart/onFlowComplete hooks (fixtures/set_allow_download_logs.js) so that flow
|
|
# is self-contained whether it runs in CI or locally.
|
|
apply_config_baseline() {
|
|
[[ -n "${ADMIN_TOKEN:-}" && -n "${SITE_1_URL:-}" ]] || {
|
|
echo "Warning: missing ADMIN_TOKEN/SITE_1_URL; skipping config baseline" >&2
|
|
return 0
|
|
}
|
|
|
|
echo "==> Re-applying config baseline on ${SITE_1_URL}"
|
|
if ! curl -f -sS --show-error --connect-timeout 10 --max-time 30 --retry 3 --retry-delay 2 --retry-connrefused -X PUT \
|
|
-H "Authorization: Bearer $ADMIN_TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"SupportSettings":{"AllowDownloadLogs":true},"ServiceSettings":{"EnableSignInWithEmail":true,"EnableSignInWithUsername":true}}' \
|
|
"${SITE_1_URL}/api/v4/config/patch" >/dev/null; then
|
|
echo "Warning: could not PUT config baseline on ${SITE_1_URL}" >&2
|
|
fi
|
|
}
|
|
|
|
ensure_calls_channel_enabled() {
|
|
[[ -n "${TEST_CHANNEL_ID:-}" && -n "${ADMIN_TOKEN:-}" && -n "${SITE_1_URL:-}" ]] || {
|
|
echo "Warning: missing TEST_CHANNEL_ID/ADMIN_TOKEN/SITE_1_URL; skipping calls channel enable" >&2
|
|
return 0
|
|
}
|
|
|
|
echo "==> Ensuring Calls enabled in channel ${TEST_CHANNEL_ID}"
|
|
if ! curl -fsS -X POST \
|
|
-H "Authorization: Bearer $ADMIN_TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"enabled":true}' \
|
|
"${SITE_1_URL}/plugins/com.mattermost.calls/${TEST_CHANNEL_ID}" >/dev/null; then
|
|
echo "Warning: could not POST calls enable for channel ${TEST_CHANNEL_ID}" >&2
|
|
fi
|
|
}
|
|
|
|
reset_android_app_state() {
|
|
[[ "$PLATFORM" != "android" ]] && return 0
|
|
command -v adb >/dev/null 2>&1 || return 0
|
|
|
|
echo "==> Resetting Android cross-app state ($MAESTRO_APP_ID)"
|
|
adb shell am force-stop com.android.chrome 2>/dev/null || true
|
|
adb shell am force-stop "$MAESTRO_APP_ID" 2>/dev/null || true
|
|
adb reverse tcp:8081 tcp:8081 2>/dev/null || true
|
|
sleep 2
|
|
}
|
|
|
|
# macOS CI uses bash 3.2; with `set -u`, expanding an empty array via "${arr[@]}"
|
|
# throws "unbound variable". Build the maestro argv explicitly instead.
|
|
run_maestro_batch() {
|
|
build_maestro_env_args
|
|
local batch_xml=$1
|
|
shift
|
|
local -a flows=("$@")
|
|
local -a cmd=( "$MAESTRO_BIN" test )
|
|
|
|
if ((${#DEVICE_ARGS[@]})); then
|
|
cmd+=("${DEVICE_ARGS[@]}")
|
|
fi
|
|
if ((${#platform_args[@]})); then
|
|
cmd+=("${platform_args[@]}")
|
|
fi
|
|
|
|
# Keep Maestro's debug output (maestro.log, per-command timings, the failure screenshot
|
|
# and hierarchy dump, and the XCUITest runner's console via `simctl launch --console`)
|
|
# inside the uploaded build/ tree. With --flatten-debug-output and no --debug-output,
|
|
# Maestro 2.6.1 writes all of it to $HOME (TestDebugReporter.getDebugOutputPath), which
|
|
# CI never collects. That is why run 34185558418 reported attach_logs_toggle_visible as
|
|
# "Unknown error": the runner process (the app's XPC peer) exited mid-flow and the only
|
|
# record of why was in /Users/runner. One directory per batch, so the flattened files of
|
|
# one batch never overwrite another's.
|
|
local debug_dir
|
|
debug_dir="$ARTIFACTS_DIR/debug/$(basename "${batch_xml%.xml}")"
|
|
mkdir -p "$debug_dir"
|
|
|
|
cmd+=(
|
|
--format junit
|
|
--output "$batch_xml"
|
|
--test-output-dir "$ARTIFACTS_DIR"
|
|
--debug-output "$debug_dir"
|
|
--flatten-debug-output
|
|
)
|
|
local exclude_tags
|
|
exclude_tags=$(load_exclude_tags)
|
|
if [[ -n "$exclude_tags" ]]; then
|
|
cmd+=("--exclude-tags=$exclude_tags")
|
|
fi
|
|
cmd+=("${maestro_env_args[@]}")
|
|
cmd+=("${flows[@]}")
|
|
|
|
# Tee so the batch output is still in the CI log verbatim while also being greppable for
|
|
# driver-startup failures (see the retry in the batch loop). PIPESTATUS keeps maestro's
|
|
# exit code rather than tee's.
|
|
local batch_log="${batch_xml%.xml}.log"
|
|
"${cmd[@]}" 2>&1 | tee "$batch_log"
|
|
return "${PIPESTATUS[0]}"
|
|
}
|
|
|
|
# True when the driver died before driving the app: Android then writes JUnit with every
|
|
# time="0.0". Any digit 1-9 means non-zero, so "0.0" is zero and "0.5" is not.
|
|
driver_startup_failed() {
|
|
local batch_log=$1 batch_xml=${2:-}
|
|
{ [[ -f "$batch_log" ]] && grep -qE 'IOSDriverTimeoutException|iOS driver not ready in time|StatusRuntimeException: UNAVAILABLE|Command failed \(tcp:' "$batch_log"; } || return 1
|
|
[[ -s "$batch_xml" ]] || return 0
|
|
! grep -qE '<testcase\b[^>]*\btime="[0-9.]*[1-9]' "$batch_xml"
|
|
}
|
|
|
|
ensure_android_driver_healthy() {
|
|
[[ "$PLATFORM" != "android" ]] && return 0
|
|
command -v adb >/dev/null 2>&1 || return 0
|
|
|
|
echo "==> Reconnecting adb after Maestro driver loss"
|
|
adb start-server >/dev/null 2>&1 || true
|
|
# Never returns when no device comes back, and `|| true` cannot help, so bound it.
|
|
timeout 60 adb wait-for-device || echo "==> adb wait-for-device timed out — continuing"
|
|
reset_android_app_state
|
|
ensure_android_app_launchable
|
|
}
|
|
|
|
# Flow paths come from the filesystem, so `&`, `<` or `"` in a name would otherwise
|
|
# emit XML the report parser cannot read.
|
|
xml_escape() {
|
|
printf '%s' "$1" | sed -e 's/&/\&/g' -e 's/</\</g' -e 's/>/\>/g' -e 's/"/\"/g'
|
|
}
|
|
|
|
# Failures, not skips: a flow that never ran must not shrink the suite and still pass. One
|
|
# <testcase> per flow, not per batch, so the report names which flows died and why.
|
|
write_driver_failure_junit() {
|
|
local batch_xml=$1
|
|
shift
|
|
local msg
|
|
msg="$(xml_escape 'Maestro driver died before the flow started (gRPC UNAVAILABLE / tcp closed / iOS driver timeout) and did not recover after one retry')"
|
|
{
|
|
printf "<?xml version='1.0' encoding='UTF-8'?>\n<testsuites>\n"
|
|
printf ' <testsuite name="maestro-driver-unavailable" tests="%d" failures="%d" errors="0" skipped="0" time="0">\n' "$#" "$#"
|
|
local flow base id flow_xml id_xml
|
|
for flow in "$@"; do
|
|
base="${flow##*/}"; id="${base%.yml}"
|
|
flow_xml="$(xml_escape "$flow")"; id_xml="$(xml_escape "$id")"
|
|
printf ' <testcase id="%s" name="%s" classname="%s" file="%s" time="0" status="ERROR">\n' "$id_xml" "$id_xml" "$flow_xml" "$flow_xml"
|
|
printf ' <failure>%s</failure>\n </testcase>\n' "$msg"
|
|
done
|
|
printf ' </testsuite>\n</testsuites>\n'
|
|
} > "$batch_xml"
|
|
}
|
|
|
|
BATCH_XMLS=()
|
|
BATCH_FAILED=0
|
|
batch_idx=0
|
|
MERGE_DONE=0
|
|
|
|
merge_batch_reports() {
|
|
[[ "$MERGE_DONE" == "1" ]] && return 0
|
|
[[ ${#BATCH_XMLS[@]} -eq 0 ]] && return 0
|
|
MERGE_DONE=1
|
|
echo ""
|
|
echo "==> Merging ${#BATCH_XMLS[@]} JUnit reports -> $MERGED_XML"
|
|
local xml_json
|
|
xml_json=$(printf '"%s",' "${BATCH_XMLS[@]}")
|
|
xml_json="[${xml_json%,}]"
|
|
node -e "
|
|
const {mergeMaestroJunitReports} = require('./detox/utils/maestro_report');
|
|
mergeMaestroJunitReports(${xml_json}, '${MERGED_XML}');
|
|
"
|
|
}
|
|
|
|
trap merge_batch_reports EXIT
|
|
|
|
ensure_android_timezone() {
|
|
[[ "$PLATFORM" != "android" ]] && return 0
|
|
command -v adb >/dev/null 2>&1 || return 0
|
|
|
|
echo "==> Setting Android emulator timezone to America/New_York (MM-T1325)"
|
|
adb root 2>/dev/null || true
|
|
adb shell cmd alarm set-timezone "America/New_York" 2>/dev/null || true
|
|
adb shell setprop persist.sys.timezone "America/New_York" 2>/dev/null || true
|
|
adb shell setprop sys.timezone "America/New_York" 2>/dev/null || true
|
|
adb shell am force-stop "$MAESTRO_APP_ID" 2>/dev/null || true
|
|
sleep 2
|
|
}
|
|
|
|
# Log CPU/RAM snapshot before each batch (temporary diagnostic for runner sizing).
|
|
log_resource_snapshot() {
|
|
local label=$1
|
|
if [[ "$PLATFORM" == "ios" ]]; then
|
|
local load mem swap
|
|
load=$(uptime | sed -E 's/.*load averages?: //')
|
|
mem=$(vm_stat 2>/dev/null | awk '
|
|
/page size of/ {ps=$8}
|
|
/Pages free/ {free=$3+0}
|
|
/Pages active/ {active=$3+0}
|
|
/Pages inactive/ {inactive=$3+0}
|
|
/Pages wired down/ {wired=$4+0}
|
|
/Pageouts/ {pageouts=$2+0}
|
|
/Swapins/ {swapins=$2+0}
|
|
/Swapouts/ {swapouts=$2+0}
|
|
END {
|
|
gb=1024*1024*1024
|
|
printf "free=%.1fGB active=%.1fGB wired=%.1fGB pageouts=%d swapins=%d swapouts=%d",
|
|
free*ps/gb, active*ps/gb, wired*ps/gb, pageouts, swapins, swapouts
|
|
}')
|
|
swap=$(sysctl -n vm.swapusage 2>/dev/null | tr -s ' ')
|
|
echo "[RES ${label}] load=${load} ${mem} ${swap}"
|
|
elif [[ "$PLATFORM" == "android" ]]; then
|
|
local load mem
|
|
load=$(uptime | sed -E 's/.*load average: //')
|
|
mem=$(free -h 2>/dev/null | awk '/^Mem:/ {printf "total=%s used=%s free=%s avail=%s", $2, $3, $4, $7} /^Swap:/ {printf " swap_used=%s", $3}')
|
|
echo "[RES ${label}] load=${load} ${mem}"
|
|
fi
|
|
}
|
|
|
|
apply_config_baseline
|
|
|
|
for batch_paths in "${BATCHES[@]}"; do
|
|
batch_idx=$((batch_idx + 1))
|
|
read -r -a path_arr <<< "$batch_paths"
|
|
batch_xml="$OUTPUT_DIR/maestro-batch-${batch_idx}.xml"
|
|
BATCH_XMLS+=("$batch_xml")
|
|
|
|
echo ""
|
|
echo "==> Maestro batch $batch_idx/${#BATCHES[@]}: ${path_arr[*]}"
|
|
log_resource_snapshot "batch-${batch_idx}-pre"
|
|
[[ "$batch_paths" == *"/timezone/"* ]] && ensure_android_timezone
|
|
ensure_ios_simulator_healthy
|
|
if [[ "$batch_paths" == *"/calls/"* ]]; then
|
|
ensure_calls_channel_enabled
|
|
grant_ios_calls_permissions
|
|
grant_android_calls_permissions
|
|
fi
|
|
[[ "$PLATFORM" == "android" ]] && reset_android_app_state
|
|
ensure_android_app_launchable
|
|
|
|
set +e
|
|
batch_start_epoch=$(date +%s)
|
|
run_maestro_batch "$batch_xml" "${path_arr[@]}"
|
|
rc=$?
|
|
batch_end_epoch=$(date +%s)
|
|
echo "[BATCH-TIME] batch ${batch_idx} wall=$((batch_end_epoch - batch_start_epoch))s exit=${rc}"
|
|
log_resource_snapshot "batch-${batch_idx}-post"
|
|
|
|
# Retry once when the driver never drove the app. A flow that ran and failed writes
|
|
# real elapsed times and is reported as-is.
|
|
if [[ $rc -ne 0 ]] && driver_startup_failed "${batch_xml%.xml}.log" "$batch_xml"; then
|
|
echo "==> Batch ${batch_idx}: Maestro driver never drove the app. Recovering and retrying this batch once."
|
|
if [[ "$PLATFORM" == "ios" ]]; then
|
|
ensure_ios_simulator_healthy
|
|
else
|
|
ensure_android_driver_healthy
|
|
fi
|
|
rm -f "$batch_xml"
|
|
batch_start_epoch=$(date +%s)
|
|
run_maestro_batch "$batch_xml" "${path_arr[@]}"
|
|
rc=$?
|
|
batch_end_epoch=$(date +%s)
|
|
echo "[BATCH-TIME] batch ${batch_idx} retry wall=$((batch_end_epoch - batch_start_epoch))s exit=${rc}"
|
|
|
|
if [[ $rc -ne 0 ]] && driver_startup_failed "${batch_xml%.xml}.log" "$batch_xml"; then
|
|
echo "==> Batch ${batch_idx}: driver still unavailable — recording ${#path_arr[@]} flow(s) as failed"
|
|
write_driver_failure_junit "$batch_xml" "${path_arr[@]}"
|
|
# Recover the device for the next batch, but leave rc non-zero so the batch stays red.
|
|
[[ "$PLATFORM" == "ios" ]] && ensure_ios_simulator_healthy
|
|
[[ "$PLATFORM" == "android" ]] && ensure_android_driver_healthy
|
|
fi
|
|
fi
|
|
|
|
if [[ $rc -ne 0 ]]; then
|
|
echo "==> Batch $batch_idx failed (exit $rc) — continuing with remaining batches"
|
|
BATCH_FAILED=1
|
|
|
|
if [[ ! -s "$batch_xml" ]]; then
|
|
flow_label="${path_arr[0]:-unknown_flow}"
|
|
flow_base="${flow_label##*/}"
|
|
flow_id="${flow_base%.yml}"
|
|
flow_label_xml="$(xml_escape "$flow_label")"
|
|
flow_id_xml="$(xml_escape "$flow_id")"
|
|
cat > "$batch_xml" <<EOF
|
|
<?xml version='1.0' encoding='UTF-8'?>
|
|
<testsuites>
|
|
<testsuite name="${flow_label_xml}" tests="1" failures="1" errors="0" skipped="0" time="0">
|
|
<testcase id="${flow_id_xml}" name="${flow_id_xml}" classname="${flow_label_xml}" file="${flow_label_xml}" time="0" status="ERROR">
|
|
<failure>Maestro batch ${batch_idx} exited ${rc} without writing JUnit XML (${flow_label_xml})</failure>
|
|
</testcase>
|
|
</testsuite>
|
|
</testsuites>
|
|
EOF
|
|
echo "==> Wrote synthetic failure JUnit for missing ${batch_xml}"
|
|
fi
|
|
if [[ "$PLATFORM" == "ios" ]]; then
|
|
ensure_ios_simulator_healthy
|
|
fi
|
|
fi
|
|
|
|
# Browser hand-off flows can wedge the driver; recover before the next batch.
|
|
if [[ "$batch_paths" == *help_url* ]]; then
|
|
[[ "$PLATFORM" == "ios" ]] && ensure_ios_simulator_healthy
|
|
[[ "$PLATFORM" == "android" ]] && ensure_android_app_launchable
|
|
fi
|
|
done
|
|
|
|
merge_batch_reports
|
|
|
|
if [[ $BATCH_FAILED -ne 0 ]]; then
|
|
echo "==> One or more Maestro batches failed"
|
|
exit 1
|
|
fi
|
|
|
|
echo "==> All Maestro batches passed"
|