diff --git a/.github/actions/setup-minimum-xcode-16/action.yml b/.github/actions/setup-minimum-xcode-16/action.yml new file mode 100644 index 0000000..8a89d62 --- /dev/null +++ b/.github/actions/setup-minimum-xcode-16/action.yml @@ -0,0 +1,71 @@ +name: Set up minimum Xcode 16 environment +description: Select Xcode 16.0 and install the pinned RootFS, XcodeGen, and iOS 18 Simulator runtime. + +inputs: + install-simulator-runtime: + description: Install and verify the iOS 18.0 Simulator runtime during setup. + required: false + default: "true" + +runs: + using: composite + steps: + - name: Configure public package submodule checkout + shell: bash + run: | + git config --global \ + url."https://github.com/".insteadOf \ + "ssh://git@ssh.github.com:443/" + + - name: Verify minimum Apple toolchain + shell: bash + run: | + test "$(uname -m)" = "arm64" + test -d /Applications/Xcode_16.0.app/Contents/Developer + sudo xcode-select \ + --switch /Applications/Xcode_16.0.app/Contents/Developer + test "$(xcodebuild -version | sed -n '1p')" = "Xcode 16.0" + test "$(xcrun --sdk iphoneos --show-sdk-version)" = "18.0" + test "$(xcrun --sdk iphonesimulator --show-sdk-version)" = "18.0" + xcodebuild -version + swift --version + + - name: Download and verify pinned RootFS release archive + shell: bash + env: + ROOTFS_SHA256: "be0f3c133f78f28b023288459b33dc28fa253a6ef29f7123bc5f3892edf90ad4" + ROOTFS_BYTE_COUNT: "6581376" + ROOTFS_URL: "https://github.com/Lolendor/ish-arm64-pkg/releases/download/v0.3.3/fs.tar.gz" + run: | + archive="$RUNNER_TEMP/pocketroot-fs-v0.3.3.tar.gz" + curl --fail --location --retry 3 \ + --output "$archive" \ + "$ROOTFS_URL" + test "$(stat -f '%z' "$archive")" = "$ROOTFS_BYTE_COUNT" + echo "$ROOTFS_SHA256 $archive" | shasum -a 256 --check + echo "POCKETROOT_ROOTFS_ARCHIVE=$archive" >> "$GITHUB_ENV" + + - name: Install XcodeGen + shell: bash + env: + XCODEGEN_VERSION: "2.46.0" + XCODEGEN_SHA256: "4d9e34b62172d645eed6457cac13fc222569974098ef4ee9c3368bedf0196806" + run: | + archive="$RUNNER_TEMP/xcodegen.zip" + install_root="$RUNNER_TEMP/xcodegen-install" + curl --fail --location --retry 3 \ + --output "$archive" \ + "https://github.com/yonaskolb/XcodeGen/releases/download/$XCODEGEN_VERSION/xcodegen.zip" + echo "$XCODEGEN_SHA256 $archive" | shasum -a 256 --check + unzip -q "$archive" -d "$install_root" + echo "$install_root/xcodegen/bin" >> "$GITHUB_PATH" + + - name: Install iOS 18.0 Simulator runtime + if: ${{ inputs.install-simulator-runtime == 'true' }} + shell: bash + run: | + xcrun simctl list >/dev/null + xcodebuild -downloadPlatform iOS + xcrun simctl list runtimes available + xcrun simctl list runtimes available \ + | grep -F "iOS 18.0" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c45905..acf73e4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -291,66 +291,25 @@ jobs: "$POCKETROOT_SPDX_SCHEMA" \ "$evidence/SBOM.spdx.json" - minimum-xcode-16: - name: Minimum Xcode 16.0 + minimum-xcode-16-runtime: + name: Minimum Xcode 16.0 / Native runtime runs-on: macos-15 - timeout-minutes: 90 + timeout-minutes: 35 steps: - name: Check out repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Configure public package submodule checkout - run: | - git config --global \ - url."https://github.com/".insteadOf \ - "ssh://git@ssh.github.com:443/" - - - name: Verify minimum Apple toolchain - run: | - test "$(uname -m)" = "arm64" - test -d /Applications/Xcode_16.0.app/Contents/Developer - sudo xcode-select \ - --switch /Applications/Xcode_16.0.app/Contents/Developer - test "$(xcodebuild -version | sed -n '1p')" = "Xcode 16.0" - test "$(xcrun --sdk iphoneos --show-sdk-version)" = "18.0" - test "$(xcrun --sdk iphonesimulator --show-sdk-version)" = "18.0" - xcodebuild -version - swift --version - - - name: Download and verify pinned RootFS release archive - env: - ROOTFS_SHA256: "be0f3c133f78f28b023288459b33dc28fa253a6ef29f7123bc5f3892edf90ad4" - ROOTFS_BYTE_COUNT: "6581376" - ROOTFS_URL: "https://github.com/Lolendor/ish-arm64-pkg/releases/download/v0.3.3/fs.tar.gz" - run: | - archive="$RUNNER_TEMP/pocketroot-fs-v0.3.3.tar.gz" - curl --fail --location --retry 3 \ - --output "$archive" \ - "$ROOTFS_URL" - test "$(stat -f '%z' "$archive")" = "$ROOTFS_BYTE_COUNT" - echo "$ROOTFS_SHA256 $archive" | shasum -a 256 --check - echo "POCKETROOT_ROOTFS_ARCHIVE=$archive" >> "$GITHUB_ENV" + - name: Set up minimum Xcode 16 environment + uses: ./.github/actions/setup-minimum-xcode-16 + with: + install-simulator-runtime: "false" - name: Validate real RootFS install with Xcode 16.0 run: | swift test \ --filter testPinnedReleaseArchiveWhenProvidedByEnvironment - - name: Install XcodeGen - env: - XCODEGEN_VERSION: "2.46.0" - XCODEGEN_SHA256: "4d9e34b62172d645eed6457cac13fc222569974098ef4ee9c3368bedf0196806" - run: | - archive="$RUNNER_TEMP/xcodegen.zip" - install_root="$RUNNER_TEMP/xcodegen-install" - curl --fail --location --retry 3 \ - --output "$archive" \ - "https://github.com/yonaskolb/XcodeGen/releases/download/$XCODEGEN_VERSION/xcodegen.zip" - echo "$XCODEGEN_SHA256 $archive" | shasum -a 256 --check - unzip -q "$archive" -d "$install_root" - echo "$install_root/xcodegen/bin" >> "$GITHUB_PATH" - - name: Install iOS 18.0 Simulator runtime run: | xcrun simctl list >/dev/null @@ -370,52 +329,80 @@ jobs: POCKETROOT_SMOKE_STABILITY_INTERVAL_MILLISECONDS: "250" run: ./Scripts/run-runtime-smoke.sh - - name: Run public-SHA External Consumer UI acceptance + minimum-xcode-16-ui: + name: Minimum Xcode 16.0 / UI (${{ matrix.label }}) + runs-on: macos-15 + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + include: + - suite: external-consumer + label: External consumer + - suite: quick-start-iphone + label: Quick Start iPhone + - suite: quick-start-ipad + label: Quick Start iPad + - suite: host-iphone + label: Host App iPhone + - suite: host-ipad + label: Host App iPad + + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Set up minimum Xcode 16 environment + uses: ./.github/actions/setup-minimum-xcode-16 + + - name: Run ${{ matrix.label }} UI smoke with Xcode 16.0 env: + POCKETROOT_UI_SUITE: ${{ matrix.suite }} + POCKETROOT_UI_FAILURE_ROOT: >- + ${{ runner.temp }}/pocketroot-ui-artifacts/${{ matrix.suite }} POCKETROOT_EXTERNAL_CONSUMER_REPOSITORY_URL: >- ${{ github.event.pull_request.head.repo.clone_url || format('{0}/{1}.git', github.server_url, github.repository) }} POCKETROOT_EXTERNAL_CONSUMER_REVISION: >- ${{ github.event.pull_request.head.sha || github.sha }} POCKETROOT_EXTERNAL_CONSUMER_REQUIRE_REMOTE: "1" - run: ./Scripts/run-external-consumer-ui-smoke.sh - - - name: Run Quick Start two-entry UI smoke with Xcode 16.0 - env: - POCKETROOT_UI_FAILURE_ARTIFACTS_DIR: >- - ${{ runner.temp }}/pocketroot-ui-artifacts/quick-start-iphone - run: ./Scripts/run-quick-start-ui-smoke.sh - - - name: Run Quick Start iPad UI smoke with Xcode 16.0 - env: - POCKETROOT_QUICK_START_UI_DEVICE_TYPE: >- - com.apple.CoreSimulator.SimDeviceType.iPad-10th-generation - POCKETROOT_QUICK_START_UI_DEVICE_NAME: >- - PocketRoot-Quick-Start-iPad-UI-Smoke - POCKETROOT_UI_FAILURE_ARTIFACTS_DIR: >- - ${{ runner.temp }}/pocketroot-ui-artifacts/quick-start-ipad - run: ./Scripts/run-quick-start-ui-smoke.sh - - - name: Run Host App PTY and Files UI smoke with Xcode 16.0 - env: - POCKETROOT_HOST_UI_FAILURE_ARTIFACTS_DIR: >- - ${{ runner.temp }}/pocketroot-ui-artifacts/host-iphone - run: ./Scripts/run-host-app-ui-smoke.sh - - - name: Run Host App iPad UI smoke with Xcode 16.0 - env: - POCKETROOT_HOST_UI_DEVICE_TYPE: >- - com.apple.CoreSimulator.SimDeviceType.iPad-10th-generation - POCKETROOT_HOST_UI_DEVICE_NAME: PocketRoot-Host-iPad-UI-Smoke - POCKETROOT_HOST_UI_FAILURE_ARTIFACTS_DIR: >- - ${{ runner.temp }}/pocketroot-ui-artifacts/host-ipad - run: ./Scripts/run-host-app-ui-smoke.sh + run: | + case "$POCKETROOT_UI_SUITE" in + external-consumer) + export POCKETROOT_UI_FAILURE_ARTIFACTS_DIR="$POCKETROOT_UI_FAILURE_ROOT" + ./Scripts/run-external-consumer-ui-smoke.sh + ;; + quick-start-iphone) + export POCKETROOT_UI_FAILURE_ARTIFACTS_DIR="$POCKETROOT_UI_FAILURE_ROOT" + ./Scripts/run-quick-start-ui-smoke.sh + ;; + quick-start-ipad) + export POCKETROOT_QUICK_START_UI_DEVICE_TYPE="com.apple.CoreSimulator.SimDeviceType.iPad-10th-generation" + export POCKETROOT_QUICK_START_UI_DEVICE_NAME="PocketRoot-Quick-Start-iPad-UI-Smoke" + export POCKETROOT_UI_FAILURE_ARTIFACTS_DIR="$POCKETROOT_UI_FAILURE_ROOT" + ./Scripts/run-quick-start-ui-smoke.sh + ;; + host-iphone) + export POCKETROOT_HOST_UI_FAILURE_ARTIFACTS_DIR="$POCKETROOT_UI_FAILURE_ROOT" + ./Scripts/run-host-app-ui-smoke.sh + ;; + host-ipad) + export POCKETROOT_HOST_UI_DEVICE_TYPE="com.apple.CoreSimulator.SimDeviceType.iPad-10th-generation" + export POCKETROOT_HOST_UI_DEVICE_NAME="PocketRoot-Host-iPad-UI-Smoke" + export POCKETROOT_HOST_UI_FAILURE_ARTIFACTS_DIR="$POCKETROOT_UI_FAILURE_ROOT" + ./Scripts/run-host-app-ui-smoke.sh + ;; + *) + echo "Unknown minimum-Xcode UI suite: $POCKETROOT_UI_SUITE" >&2 + exit 2 + ;; + esac - name: Upload UI failure diagnostics if: failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: pocketroot-ui-failure-${{ github.run_id }}-${{ github.run_attempt }} - path: ${{ runner.temp }}/pocketroot-ui-artifacts + name: pocketroot-ui-failure-${{ matrix.suite }}-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ runner.temp }}/pocketroot-ui-artifacts/${{ matrix.suite }} if-no-files-found: warn retention-days: 7 diff --git a/CHANGELOG.en.md b/CHANGELOG.en.md index cf3c2cb..5298dde 100644 --- a/CHANGELOG.en.md +++ b/CHANGELOG.en.md @@ -6,7 +6,18 @@ All notable PocketRoot changes are recorded here. Semantic Versioning begins wit ## Unreleased -There are currently no changes outside the release candidate. +### Changed + +- Split the minimum-Xcode 16 gate into an independent native-runtime job and a + five-way `fail-fast: false` UI matrix for the public-SHA external consumer, + Quick Start iPhone/iPad, and Host App iPhone/iPad. Every isolated runner + re-verifies the RootFS, XcodeGen, Xcode 16, and iOS 18 runtime through one + repository-owned composite action without transferring an unreviewed App or + DerivedData; each lane has a collision-free failure artifact. A runner-owned + Simulator that explicitly disappears from Xcode destinations is recreated + with the same runtime/device type for one bounded retry; caller-owned devices + and assertion failures are never retried. Coverage is unchanged while PR + wall-clock time no longer accumulates every UI smoke serially. ## 0.2.0 - Unreleased @@ -73,13 +84,28 @@ There are currently no changes outside the release candidate. independent Simulators and `xcodebuild` invocations, retaining phase checkpoints, `.xcresult`, test output, and PocketRoot Simulator logs on failure while keeping a ten-minute hard limit per test. The system-file - round trip now waits within one 30-second window for either the local device - location or a restored Host folder. The shared UI runner restarts and retries + round trip now uses one 60-second state loop for the Host fixture, a local + location, or an interactive `Browse` transition instead of mistaking an early + navigation bar for readiness. If the picker is still on Recents, it taps the + `Browse` tab, which can enter the Xcode 16 accessibility tree late; a picker + already in the Host folder does not return until the fixture file is actually + visible. Fixture selection uses a verified current frame and retries once if + iOS 18.0 leaves the picker open after reporting a synthesized tap. The helper + returns an explicit success state so a failed navigation or selection ends + the test case without cascading taps and secondary failures. The PTY size + probe likewise refocuses and resends only once when the first synthesized + command's unique marker remains absent for 15 seconds; the final 30-second + assertion still exposes a real terminal-input or resize synchronization + failure. The shared + UI runner restarts and retries exactly once only when its own temporary Simulator test runner was not registered with FrontBoard; caller-supplied shared Simulators are not restarted, and assertion, timeout, and other build failures still fail immediately. If retry or restart fails, both attempt logs and available `.xcresult` bundles are retained. +- The native Xcode 16 lane validates the real RootFS install before downloading + the multi-gigabyte Simulator runtime, preserving the disk headroom enforced + by the installer while UI lanes retain the shared one-step setup. ## 0.1.0 - 2026-07-31 diff --git a/CHANGELOG.md b/CHANGELOG.md index e84f6c9..61f64d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,15 @@ PocketRoot 的重要变化记录在这里,并从首个公开版本开始遵循 ## Unreleased -当前暂无未归入候选版本的变更。 +### Changed + +- 将最低 Xcode 16 门禁拆为独立 native runtime job 和五路 `fail-fast: false` UI + matrix,分别并行验证公开 SHA 外部消费者、Quick Start iPhone/iPad 与 Host App + iPhone/iPad。全部 job 复用仓库内 composite action,但各自在隔离 runner 上重新 + 校验 RootFS、XcodeGen、Xcode 16 与 iOS 18 runtime,不传递未审计 App 或 DerivedData; + 每路失败诊断使用独立 artifact 名称。runner 自建 Simulator 明确从 Xcode destination + 消失时可按相同 runtime/device type 重建并有界重试一次;调用方设备和断言失败不重试。 + 测试覆盖不减少,PR 墙钟时间不再由全部 UI smoke 串行累加。 ## 0.2.0 - Unreleased @@ -53,12 +61,21 @@ PocketRoot 的重要变化记录在这里,并从首个公开版本开始遵循 - 将 Host App 的 Files/Workspace 与 PTY lifecycle UI smoke 拆分到独立模拟器和 `xcodebuild` 调用;失败时记录阶段检查点,并保留 `.xcresult`、测试输出和 PocketRoot 模拟器日志供 CI 下载分析,同时维持每项测试 10 分钟硬上限; - 系统文件 round-trip 测试也会在同一个 30 秒窗口内等待本机位置或已恢复的 - Host 目录,兼容文件选择器保存上次目录及慢速模拟器的合法状态。通用 UI runner + 系统文件 round-trip 测试也会在一个 60 秒状态循环中等待 Host fixture、本机位置 + 或可交互的 `Browse/浏览`,而不会把提前出现的 picker 导航栏误判为就绪; + 当 picker 停在 Recents 时,点击可能延迟进入 accessibility tree 的 Browse, + 而已经位于 Host 目录时会等到 fixture 文件真正可见才返回。fixture 选择使用当前 + 已验证 frame;若 iOS 18.0 在报告合成 tap 后仍保留 picker,则有界重试一次。辅助函数 + 返回明确的成功状态,导航或选择失败后立即停止当前用例,避免后续点击产生连锁误报。 + PTY 尺寸探针同样只在首条合成输入的唯一标记 15 秒内未出现时重新聚焦并补发一次, + 最终 30 秒断言仍会暴露真实的终端输入或尺寸同步故障。 + 通用 UI runner 只对 test runner 未注册到 FrontBoard 的模拟器基础设施错误重启自己创建的临时设备 并重试一次,调用方指定的共享 Simulator 不会被重启;测试 断言、超时和其他构建错误仍立即失败,重试或重启失败时同时保留首次与最终日志及 可用的两次 `.xcresult`。 +- Xcode 16 native lane 会先验证真实 RootFS 安装,再下载数 GB 的 Simulator runtime, + 为安装器的磁盘余量门禁保留空间;UI lanes 继续使用统一的一步 setup。 ## 0.1.0 - 2026-07-31 diff --git a/Compliance/Release/experimental-v0.2.0/COMPOSITION.json b/Compliance/Release/experimental-v0.2.0/COMPOSITION.json index 5f01d94..1f6c41c 100644 --- a/Compliance/Release/experimental-v0.2.0/COMPOSITION.json +++ b/Compliance/Release/experimental-v0.2.0/COMPOSITION.json @@ -24,7 +24,7 @@ "Scripts/inject-demo-rootfs.sh": "3982b5382b0d1e13e0c8e8a5bb5404c5bad1dfc4d6e9cd23a39e3395a83087bb", "Scripts/scan-release-artifact.rb": "48f27ab280491864228393e1675fe3a2889cbd616f79e3cb16bae7efedf647c0", "Scripts/run-host-app-ui-smoke.sh": "c8b11b1bf3a467fd10ed2c4124a72396b0a803e98f155d1d709fab46b59eede1", - "Scripts/run-ios-example-ui-smoke.sh": "071fa3336479f1849eb3be3f1f6b6516025197a0cf30640e64cd902282d9ed4d", + "Scripts/run-ios-example-ui-smoke.sh": "147fa58dc255e28fd63b0a7c62ed747b2215df652df3a0bdf5585eee4b49d395", "Scripts/run-quick-start-ui-smoke.sh": "acf26d68fc37250e24911c2d23791a0d12ff06a624dedcefee0ed7905bc31af2", "Scripts/run-external-consumer-ui-smoke.sh": "d9544ede3ecbad8d42a114015fa77071d31aa84aafc1cbe0f0f001453b1a4d0d", "Scripts/run-host-app-device-ui-smoke.sh": "2f934265cb46145b27bb26e0d7e84acebc14290602e23cd1ab8f222aef68ed1f", @@ -120,7 +120,7 @@ "Examples/PocketRootDemo/Sources/PocketRootDemo/Shared/UI/PlaceholderViewController.swift": "48e0f6462228a66face2aef51f9bd39ec02923c4d1c01b6b34f920948998f2b8", "Examples/PocketRootHostApp/Sources/HostApp.swift": "0c36bd3104ddc5bebb9e23675a44a92e3ce4aec2c1461b3e182d86d1a75159c9", "Examples/PocketRootHostApp/Sources/Info.plist": "711b38923f11fdb4ceb7d28cb90a51674afec0a64786a20f9bd68e3f406f8d26", - "Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift": "bc99a66278cd3476f7b4e854d85ec975d839362a386e8b5a455496f63e14c491", + "Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift": "cfa59f0489f94b14b242ffa1aa9f75e0eeda99c74ca487318e0ea096bbef8d61", "Examples/PocketRootQuickStartApp/Sources/Info.plist": "b3c0c96e89db99c72c9f020590b1c5cdc0c927fd3af4b6ae7865abfdc81049b8", "Examples/PocketRootQuickStartApp/Sources/QuickStartApp.swift": "1c77c9229281d0d1f9fbe3678bcc9995fff8c7d2189f4c6152daaa2999683fb6", "Examples/PocketRootQuickStartApp/UITests/PocketRootQuickStartAppUITests.swift": "828471c84386dc8ddba23ecadc3618ccd559d92084ab762f4ef0832c3e3d5f59", diff --git a/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json b/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json index 4bdaf49..e95a18f 100644 --- a/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json +++ b/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json @@ -3,7 +3,7 @@ "dataLicense": "CC0-1.0", "SPDXID": "SPDXRef-DOCUMENT", "name": "PocketRoot experimental full-graph composition 0.2.0", - "documentNamespace": "https://github.com/jacklv-coder/PocketRoot/sbom/release/experimental-v0.2.0/ac8d5bb757a49cc8c7c154c43a223a39caf7d05ec81c3a315f1afe08513c26be", + "documentNamespace": "https://github.com/jacklv-coder/PocketRoot/sbom/release/experimental-v0.2.0/44f6d771dd3c6d83d7f5ee81ad4ffe83a50ef3efdafa24a26b1e8a70fc5f53d3", "creationInfo": { "created": "2026-08-03T00:00:00Z", "creators": [ diff --git a/Compliance/Release/experimental-v0.2.0/SHA256SUMS b/Compliance/Release/experimental-v0.2.0/SHA256SUMS index 9c6f265..1ec07e0 100644 --- a/Compliance/Release/experimental-v0.2.0/SHA256SUMS +++ b/Compliance/Release/experimental-v0.2.0/SHA256SUMS @@ -1,5 +1,5 @@ -ac8d5bb757a49cc8c7c154c43a223a39caf7d05ec81c3a315f1afe08513c26be COMPOSITION.json +44f6d771dd3c6d83d7f5ee81ad4ffe83a50ef3efdafa24a26b1e8a70fc5f53d3 COMPOSITION.json 9fe270ba11160051d13f99cfe4ce6ee03c52882b88339dd6e4c1b4923ae30755 READINESS.json 6d41126608b019b46332a7957889f41e3c3ec6c6e9e2b884dca5ffc6d405e943 README.md fab07621b5188930bfd2a38117e206815d94f755e667d65fc182ee55d14d2d40 RELEASE-CHECKLIST.md -0c88032bfa74974a774165e49a2ebf5f4fbfed6632436f1660c16209d7a7f773 SBOM.spdx.json +c56b8eec15f2112bcd38b8f1d8f0c56da52967538284e12cd749422565076b8f SBOM.spdx.json diff --git a/Docs/Testing.md b/Docs/Testing.md index beda5ae..431e574 100644 --- a/Docs/Testing.md +++ b/Docs/Testing.md @@ -560,9 +560,15 @@ RootFS。这只是兼容性证据,不授权 RootFS 分发,也不改变正式 SPDX 2.3 SBOM,要求无 private framework、private entitlement、JIT entitlement、 `MAP_JIT` 或无效签名信号,再用同一固定 schema 校验;App 和证据均不上传。 -最低工具链 job 另外固定选择 Xcode 16.0 / iOS 18.0 SDK,验证真实 RootFS install、 -安装 iOS 18.0 Simulator runtime、完成 Simulator/device final-link,执行 17 项原生 -smoke,并分别在 iPhone 16 与 iPad(第 10 代)Simulator 运行最小 Quick Start 的 +最低工具链门禁拆为一个 native runtime job 和五路并行 UI matrix job。每个 job 都通过 +仓库内固定的 composite action 独立选择 Xcode 16.0 / iOS 18.0 SDK、校验并取得同一 +RootFS、安装同一 XcodeGen,并取得同一 iOS 18.0 Simulator runtime;job 之间不传递 +未审计的 App、RootFS 或 DerivedData。Native job 在下载数 GB 的 Simulator runtime +之前验证真实 RootFS install,为安装器的磁盘余量门禁保留空间,然后完成 +Simulator/device final-link 并执行 17 项原生 smoke。UI matrix 设置 +`fail-fast: false`,分别运行公开 SHA 外部消费者、iPhone/iPad Quick Start 和 +iPhone/iPad Host App,因此单路失败不会取消其他证据;每路使用不冲突的失败制品名。 +这些 UI job 在 iPhone 16 与 iPad(第 10 代)Simulator 运行最小 Quick Start 的 Files/Terminal 冷启动与 PTY-to-Files 文件闭环,以及 Host App 的 PTY、Files、 Workspace、系统 document picker 导入、share sheet 保存、guest 删除后再次导入并 复验内容的完整 UI 闭环。测试 fixture 只在显式 `-PocketRootUITesting` @@ -573,9 +579,10 @@ Workspace、系统 document picker 导入、share sheet 保存、guest 删除后 `POCKETROOT_KEEP_UI_RESULT=1` 可保留临时 DerivedData 与 `.xcresult`。两个 wrapper 复用 `run-ios-example-ui-smoke.sh` 的相同 RootFS、超时、xcresult 和安全清理边界。 当 Xcode 明确报告 Simulator test runner 未注册到 FrontBoard 时,通用 runner 会重启 -同一个由 runner 创建的临时 Simulator 并且只重试一次;调用方通过 -`POCKETROOT_UI_SMOKE_DEVICE` 提供的共享 Simulator 不会被自动关闭或重启。断言失败、 -测试超时和其他构建错误不会重试。 +同一个由 runner 创建的临时 Simulator;当 Xcode 明确报告目标设备已从可用 destination +消失时,会以相同 runtime 与 device type 重建该临时 Simulator。两种基础设施恢复合计 +最多重试一次;调用方通过 `POCKETROOT_UI_SMOKE_DEVICE` 提供的共享 Simulator 不会被 +自动关闭、重启或重建。断言失败、测试超时和其他构建错误不会重试。 可设置 `POCKETROOT_UI_INFRASTRUCTURE_RETRY_LIMIT=0` 关闭该恢复路径。若第二次仍失败, 失败 artifact 会同时包含首次/最终测试日志与可用的两次 `.xcresult`;即使重启本身 失败,也保留首次 `xcodebuild` 的诊断结果。 diff --git a/Docs/en/Testing.md b/Docs/en/Testing.md index 280a3a2..4db2ccd 100644 --- a/Docs/en/Testing.md +++ b/Docs/en/Testing.md @@ -499,9 +499,18 @@ private-framework, private-entitlement, JIT-entitlement, `MAP_JIT`, or invalid signature signal, and validates the generated SBOM with the same schema. It uploads neither the App nor scan evidence. -The minimum-toolchain job explicitly selects Xcode 16.0 / iOS 18.0 SDK, -validates real RootFS installation, installs the iOS 18.0 Simulator runtime, -final-links Simulator/device Apps, runs the 17-check native smoke, and executes +The minimum-toolchain gate is split into one native-runtime job and a five-way +parallel UI matrix. Every job independently uses the repository-owned +composite action to select Xcode 16.0 / iOS 18.0 SDK, verify and obtain the +same RootFS, install the same XcodeGen, and obtain the same iOS 18.0 Simulator +runtime. Jobs do not transfer an unreviewed App, RootFS, or DerivedData between +runners. The native job validates real RootFS installation before downloading +the multi-gigabyte Simulator runtime, preserving the installer's required disk +headroom, then final-links Simulator/device Apps and runs the 17-check native +smoke. The `fail-fast: false` UI matrix runs +the public-SHA external consumer, iPhone/iPad Quick Start, and iPhone/iPad Host +App independently, so one failure does not cancel the remaining evidence and +each lane has a collision-free diagnostic artifact name. These lanes execute the minimal Quick Start cold Files/Terminal and PTY-to-Files file closure plus the full Host App PTY, Files, Workspace, system document-picker import, share-sheet save, guest deletion, re-import, and content-verification UI @@ -517,10 +526,13 @@ Use `POCKETROOT_QUICK_START_UI_DEVICE_TYPE` and cleanup boundaries. For failure diagnosis, `POCKETROOT_KEEP_UI_RESULT=1` retains temporary DerivedData and the `.xcresult`. When Xcode explicitly reports that the Simulator test runner was not registered with FrontBoard, the -shared runner restarts the same temporary Simulator and retries exactly once. -This recovery applies only to a Simulator created by the runner; a shared -Simulator supplied through `POCKETROOT_UI_SMOKE_DEVICE` is never automatically -shut down or restarted. Assertions, test timeouts, and other build failures are not retried. Set +shared runner restarts the same temporary Simulator. When Xcode explicitly +reports that the destination has disappeared, it recreates that temporary +Simulator with the same runtime and device type. These infrastructure paths +share one total retry. Recovery applies only to a Simulator created by the +runner; a shared Simulator supplied through `POCKETROOT_UI_SMOKE_DEVICE` is +never automatically shut down, restarted, or recreated. Assertions, test +timeouts, and other build failures are not retried. Set `POCKETROOT_UI_INFRASTRUCTURE_RETRY_LIMIT=0` to disable this recovery path. If the retry or restart also fails, the failure artifact retains the first and final logs plus both available `.xcresult` bundles. This Simulator evidence diff --git a/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift b/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift index 4478a3a..839ab62 100644 --- a/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift +++ b/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift @@ -104,7 +104,10 @@ final class PocketRootHostAppUITests: XCTestCase { XCTAssertTrue(disclosure.waitForExistence(timeout: 10)) waitForEnabled(disclosure) disclosure.tap() - waitForEnabled(folder) + guard revealFileEntry(folder, in: app) else { + shutdownRuntime(in: app) + return + } folder.tap() waitForEnabled(actions) @@ -115,8 +118,14 @@ final class PocketRootHostAppUITests: XCTestCase { app.buttons["Create"].tap() waitForEnabled(actions) let childNavigationBar = app.navigationBars[folderName] - XCTAssertTrue(childNavigationBar.waitForExistence(timeout: 10)) - childNavigationBar.buttons.element(boundBy: 0).tap() + guard childNavigationBar.waitForExistence(timeout: 30) else { + XCTFail("child folder navigation bar to exist") + shutdownRuntime(in: app) + return + } + let backButton = childNavigationBar.buttons.element(boundBy: 0) + XCTAssertTrue(waitForHittable(backButton)) + backButton.tap() let nestedFile = app.descendants(matching: .any)[ "PocketRootFiles.entry./root/\(folderName)/\(nestedFileName)" @@ -206,17 +215,16 @@ final class PocketRootHostAppUITests: XCTestCase { } importFile.tap() - openHostDocuments(in: app) - let fixture = app.cells[ - "\(Self.systemImportFixtureDisplayName), txt" - ] - XCTAssertTrue(fixture.waitForExistence(timeout: 30)) - fixture.tap() + guard openHostDocuments(in: app) else { + return + } + guard importHostFixture(in: app) else { + return + } let imported = app.descendants(matching: .any)[ "PocketRootFiles.entry./root/\(Self.systemImportFixtureName)" ] - XCTAssertTrue(imported.waitForExistence(timeout: 30)) guard revealFileEntry(imported, in: app) else { return } @@ -243,7 +251,9 @@ final class PocketRootHostAppUITests: XCTestCase { withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5) ).tap() - openHostDocuments(in: app) + guard openHostDocuments(in: app) else { + return + } let save = app.buttons.matching( NSPredicate(format: "label IN %@", ["Save", "存储"]) ).firstMatch @@ -292,17 +302,16 @@ final class PocketRootHostAppUITests: XCTestCase { return } importFile.tap() - openHostDocuments(in: app) - let exported = app.cells[ - "\(Self.systemImportFixtureDisplayName), txt" - ] - XCTAssertTrue(exported.waitForExistence(timeout: 30)) - exported.tap() + guard openHostDocuments(in: app) else { + return + } + guard importHostFixture(in: app) else { + return + } let reimported = app.descendants(matching: .any)[ "PocketRootFiles.entry./root/\(Self.systemImportFixtureName)" ] - XCTAssertTrue(reimported.waitForExistence(timeout: 30)) guard revealFileEntry(reimported, in: app) else { return } @@ -913,23 +922,10 @@ final class PocketRootHostAppUITests: XCTestCase { ) } - private func openHostDocuments(in app: XCUIApplication) { + private func openHostDocuments(in app: XCUIApplication) -> Bool { let browse = app.buttons.matching( NSPredicate(format: "label IN %@", ["Browse", "浏览"]) ).firstMatch - if browse.waitForExistence(timeout: 10), - let frames = waitForInteractionFrames( - of: browse, - in: app, - timeout: 3 - ) - { - tapFrame( - frames.elementFrame, - in: frames.appFrame, - using: app - ) - } // The iOS document picker preserves its last visited directory. On a // later import or export in this test it can reopen directly inside @@ -972,18 +968,60 @@ final class PocketRootHostAppUITests: XCTestCase { localLocationLabels ) ).firstMatch - let localLocation = sidebarLocalLocation.waitForExistence(timeout: 5) - ? sidebarLocalLocation - : fallbackLocalLocation - let pickerLanding = app.descendants(matching: .any).matching( - NSPredicate( - format: "label IN %@", - hostDocumentLabels + localLocationLabels - ) - ).firstMatch - XCTAssertTrue(pickerLanding.waitForExistence(timeout: 30)) - if currentHostDocuments.exists { - return + let hostFixture = app.cells[ + "\(Self.systemImportFixtureDisplayName), txt" + ] + // The minimum Xcode 16 runner can present the document manager on + // Recents before its Browse tab enters the accessibility tree, while a + // restored Host location can also expose its navigation bar before its + // file list. Spend one deadline waiting for either the fixture itself, + // a local location, or a usable Browse transition. + let pickerDeadline = Date().addingTimeInterval(60) + var localLocation: XCUIElement? + var didTapBrowse = false + while Date() < pickerDeadline { + if currentHostDocuments.exists { + if hostFixture.exists { + return true + } + // An iPad sidebar can keep the local location visible while + // the restored Host file list is still loading. Once the Host + // navigation state is present, stay there instead of letting + // the sidebar take us back out of the destination. + RunLoop.current.run( + until: Date().addingTimeInterval(0.2) + ) + continue + } + if sidebarLocalLocation.exists { + localLocation = sidebarLocalLocation + break + } + if fallbackLocalLocation.exists { + localLocation = fallbackLocalLocation + break + } + if !didTapBrowse, browse.exists { + guard let frames = waitForInteractionFrames( + of: browse, + in: app, + timeout: 10 + ) else { + XCTFail("document picker Browse to become hittable") + return false + } + tapFrame( + frames.elementFrame, + in: frames.appFrame, + using: app + ) + didTapBrowse = true + } + RunLoop.current.run(until: Date().addingTimeInterval(0.2)) + } + guard let localLocation else { + XCTFail("document picker to expose a usable Host or local destination") + return false } let hostDocuments = app.cells.matching( @@ -1026,17 +1064,55 @@ final class PocketRootHostAppUITests: XCTestCase { "local document location to reveal the host container" ) guard openedHostDestination else { - return + return false } - if currentHostDocuments.exists { - return + if !currentHostDocuments.exists { + XCTAssertTrue(hostDocuments.exists) + guard hostDocuments.exists else { + return false + } + hostDocuments.tap() + } + guard hostFixture.waitForExistence(timeout: 30) else { + XCTFail("Host Documents fixture to become visible") + return false } + return true + } - XCTAssertTrue(hostDocuments.exists) - guard hostDocuments.exists else { - return + private func importHostFixture(in app: XCUIApplication) -> Bool { + let fixture = app.cells[ + "\(Self.systemImportFixtureDisplayName), txt" + ] + let imported = app.descendants(matching: .any)[ + "PocketRootFiles.entry./root/\(Self.systemImportFixtureName)" + ] + + // Xcode 16 can synthesize a successful tap while the iOS 18.0 + // document picker keeps the file cell open and never completes the + // selection. Re-query the current frame and retry once only while the + // guest file is still absent. + for _ in 0..<2 { + guard let frames = waitForInteractionFrames( + of: fixture, + in: app, + timeout: 10 + ) else { + XCTFail("Host Documents fixture to expose an interaction frame") + return false + } + tapFrame( + frames.elementFrame, + in: frames.appFrame, + using: app + ) + if imported.waitForExistence(timeout: 15) { + return true + } } - hostDocuments.tap() + + XCTFail("Host Documents fixture selection to import the guest file") + return false } private func dismissShareSheetIfNeeded( @@ -1293,8 +1369,7 @@ final class PocketRootHostAppUITests: XCTestCase { _ terminal: XCUIElement, marker: String ) -> (rows: Int, columns: Int) { - terminal.typeText("printf '\(marker)'; stty size\n") - + let command = "printf '\(marker)'; stty size\n" var result: (rows: Int, columns: Int)? let expression = try! NSRegularExpression( pattern: NSRegularExpression.escapedPattern(for: marker) @@ -1321,7 +1396,22 @@ final class PocketRootHostAppUITests: XCTestCase { result = (rows, columns) return true } - wait(for: predicate, evaluatedWith: terminal, timeout: 30) + terminal.typeText(command) + if !waitWithoutAssertion( + for: predicate, + evaluatedWith: terminal, + timeout: 15 + ) { + terminal.tap() + terminal.typeText(command) + } + guard wait( + for: predicate, + evaluatedWith: terminal, + timeout: 30 + ) else { + return (0, 0) + } return result ?? (0, 0) } diff --git a/Scripts/generate-release-compliance.rb b/Scripts/generate-release-compliance.rb index 6c099a9..7b50e58 100644 --- a/Scripts/generate-release-compliance.rb +++ b/Scripts/generate-release-compliance.rb @@ -281,7 +281,7 @@ module PocketRootReleaseCompliance "Scripts/run-host-app-ui-smoke.sh" => "c8b11b1bf3a467fd10ed2c4124a72396b0a803e98f155d1d709fab46b59eede1", "Scripts/run-ios-example-ui-smoke.sh" => - "071fa3336479f1849eb3be3f1f6b6516025197a0cf30640e64cd902282d9ed4d", + "147fa58dc255e28fd63b0a7c62ed747b2215df652df3a0bdf5585eee4b49d395", "Scripts/run-quick-start-ui-smoke.sh" => "acf26d68fc37250e24911c2d23791a0d12ff06a624dedcefee0ed7905bc31af2", "Scripts/run-external-consumer-ui-smoke.sh" => diff --git a/Scripts/run-ios-example-ui-smoke.sh b/Scripts/run-ios-example-ui-smoke.sh index 8dd045d..edee8d7 100755 --- a/Scripts/run-ios-example-ui-smoke.sh +++ b/Scripts/run-ios-example-ui-smoke.sh @@ -31,6 +31,8 @@ FIRST_ATTEMPT_RESULT_BUNDLE_PATH="$DERIVED_DATA_ROOT/$TEST_BUNDLE-attempt-1.xcre RETRY_SHUTDOWN_EXIT_CODE="" RETRY_RESTART_EXIT_CODE="" RETRY_RESTART_FAILURE_STAGE="" +RETRY_FAILURE_KIND="" +FIRST_ATTEMPT_DEVICE_UDID="" cleanup() { if [[ "$CREATED_DEVICE" == "true" && @@ -130,28 +132,66 @@ run_ui_tests() { return "${PIPESTATUS[0]}" } -is_retryable_simulator_launch_failure() { - grep -Fq 'Simulator device failed to launch' "$TEST_LOG_PATH" && - grep -Fq 'is unknown to FrontBoard' "$TEST_LOG_PATH" +retryable_simulator_failure_kind() { + if grep -Fq 'Simulator device failed to launch' "$TEST_LOG_PATH" && + grep -Fq 'is unknown to FrontBoard' "$TEST_LOG_PATH"; then + printf 'frontboard-launch\n' + return 0 + fi + if grep -Fq 'Unable to find a destination matching the provided destination specifier' \ + "$TEST_LOG_PATH" && + grep -Fq 'no available devices matched the request' "$TEST_LOG_PATH"; then + printf 'missing-destination\n' + return 0 + fi + return 1 } set +e run_ui_tests test_exit_code="$?" set -e +if [[ "$test_exit_code" -ne 0 ]]; then + RETRY_FAILURE_KIND="$(retryable_simulator_failure_kind || true)" +fi if [[ "$test_exit_code" -ne 0 && "$INFRASTRUCTURE_RETRY_LIMIT" -eq 1 && - "$CREATED_DEVICE" == "true" ]] && - is_retryable_simulator_launch_failure; then + "$CREATED_DEVICE" == "true" && + -n "$RETRY_FAILURE_KIND" ]]; then + FIRST_ATTEMPT_DEVICE_UDID="$DEVICE_UDID" mv "$TEST_LOG_PATH" "$FIRST_ATTEMPT_LOG_PATH" if [[ -d "$RESULT_BUNDLE_PATH" ]]; then mv "$RESULT_BUNDLE_PATH" "$FIRST_ATTEMPT_RESULT_BUNDLE_PATH" fi - echo "$ARTIFACT_LABEL UI test runner was not registered with FrontBoard; restarting the Simulator and retrying once." >&2 + if [[ "$RETRY_FAILURE_KIND" == "missing-destination" ]]; then + echo "$ARTIFACT_LABEL Simulator destination disappeared; recreating the runner-owned Simulator and retrying once." >&2 + else + echo "$ARTIFACT_LABEL UI test runner was not registered with FrontBoard; restarting the Simulator and retrying once." >&2 + fi set +e xcrun simctl shutdown "$DEVICE_UDID" >/dev/null 2>&1 RETRY_SHUTDOWN_EXIT_CODE="$?" + if [[ "$RETRY_FAILURE_KIND" == "missing-destination" ]]; then + xcrun simctl delete "$DEVICE_UDID" >/dev/null 2>&1 + replacement_device="$(xcrun simctl create \ + "$SIMULATOR_NAME" \ + "$SIMULATOR_DEVICE_TYPE" \ + "$RUNTIME_ID")" + RETRY_RESTART_EXIT_CODE="$?" + if [[ "$RETRY_RESTART_EXIT_CODE" -eq 0 && + -n "$replacement_device" ]]; then + DEVICE_UDID="$replacement_device" + else + RETRY_RESTART_FAILURE_STAGE="create" + if [[ "$RETRY_RESTART_EXIT_CODE" -eq 0 ]]; then + RETRY_RESTART_EXIT_CODE=1 + fi + fi + else + RETRY_RESTART_EXIT_CODE=0 + fi + if [[ "$RETRY_RESTART_EXIT_CODE" -eq 0 ]]; then xcrun simctl boot "$DEVICE_UDID" >/dev/null 2>&1 RETRY_RESTART_EXIT_CODE="$?" if [[ "$RETRY_RESTART_EXIT_CODE" -ne 0 ]]; then @@ -163,6 +203,7 @@ if [[ "$test_exit_code" -ne 0 && RETRY_RESTART_FAILURE_STAGE="bootstatus" fi fi + fi set -e if [[ "$RETRY_RESTART_EXIT_CODE" -eq 0 ]]; then RETRY_RESTART_FAILURE_STAGE="" @@ -203,6 +244,8 @@ if [[ "$test_exit_code" -ne 0 ]]; then echo "default_allowance=$DEFAULT_TEST_ALLOWANCE" echo "maximum_allowance=$MAXIMUM_TEST_ALLOWANCE" echo "infrastructure_retry_limit=$INFRASTRUCTURE_RETRY_LIMIT" + echo "retry_failure_kind=${RETRY_FAILURE_KIND:-}" + echo "first_attempt_device=$FIRST_ATTEMPT_DEVICE_UDID" echo "retry_shutdown_exit_code=$RETRY_SHUTDOWN_EXIT_CODE" echo "retry_restart_exit_code=$RETRY_RESTART_EXIT_CODE" echo "retry_restart_failure_stage=$RETRY_RESTART_FAILURE_STAGE" diff --git a/Tests/Scripts/ReleaseComplianceTests.rb b/Tests/Scripts/ReleaseComplianceTests.rb index d44c10c..b2a6f59 100644 --- a/Tests/Scripts/ReleaseComplianceTests.rb +++ b/Tests/Scripts/ReleaseComplianceTests.rb @@ -1023,8 +1023,77 @@ def test_external_consumer_profile_resolves_public_products_and_lifecycle assert_includes runner, "POCKETROOT_EXTERNAL_CONSUMER_REVISION" assert_includes runner, "POCKETROOT_EXTERNAL_CONSUMER_REPOSITORY_URL" - assert_includes workflow, - "Run public-SHA External Consumer UI acceptance" + assert_includes workflow, "suite: external-consumer" + assert_includes workflow, "./Scripts/run-external-consumer-ui-smoke.sh" + end + + def test_minimum_xcode_ci_parallelizes_native_and_ui_coverage + workflow = REPOSITORY_ROOT.join(".github/workflows/ci.yml").binread + setup_action = + REPOSITORY_ROOT + .join(".github/actions/setup-minimum-xcode-16/action.yml") + .binread + + assert_match(/^ minimum-xcode-16-runtime:$/, workflow) + assert_match(/^ minimum-xcode-16-ui:$/, workflow) + refute_match(/^ minimum-xcode-16:$/, workflow) + ui_job_header = + workflow.split(/^ minimum-xcode-16-ui:\n/, 2).fetch(1).lines.first(5).join + runtime_job = + workflow + .split(/^ minimum-xcode-16-runtime:\n/, 2) + .fetch(1) + .split(/^ minimum-xcode-16-ui:\n/, 2) + .first + assert_includes ui_job_header, "timeout-minutes: 60" + assert_includes workflow, "fail-fast: false" + assert_equal( + 2, + workflow.scan("uses: ./.github/actions/setup-minimum-xcode-16").length + ) + %w[ + external-consumer + quick-start-iphone + quick-start-ipad + host-iphone + host-ipad + ].each do |suite| + assert_includes workflow, "suite: #{suite}" + end + assert_includes workflow, "./Scripts/run-runtime-smoke.sh" + assert_includes workflow, "./Scripts/run-external-consumer-ui-smoke.sh" + assert_includes workflow, "./Scripts/run-quick-start-ui-smoke.sh" + assert_includes workflow, "./Scripts/run-host-app-ui-smoke.sh" + assert_includes( + workflow, + "pocketroot-ui-failure-${{ matrix.suite }}-${{ github.run_id }}" + ) + + assert_includes setup_action, "/Applications/Xcode_16.0.app" + assert_includes setup_action, 'ROOTFS_BYTE_COUNT: "6581376"' + assert_includes( + setup_action, + 'ROOTFS_SHA256: "be0f3c133f78f28b023288459b33dc28fa253a6ef29f7123bc5f3892edf90ad4"' + ) + assert_includes setup_action, 'XCODEGEN_VERSION: "2.46.0"' + assert_includes setup_action, "install-simulator-runtime:" + assert_includes( + setup_action, + "if: ${{ inputs.install-simulator-runtime == 'true' }}" + ) + assert_includes setup_action, "xcodebuild -downloadPlatform iOS" + assert_includes setup_action, 'grep -F "iOS 18.0"' + assert_includes runtime_job, 'install-simulator-runtime: "false"' + assert_operator( + runtime_job.index("Validate real RootFS install with Xcode 16.0"), + :<, + runtime_job.index("Install iOS 18.0 Simulator runtime") + ) + assert_operator( + runtime_job.index("Install iOS 18.0 Simulator runtime"), + :<, + runtime_job.index("Final-link runtime with Xcode 16.0") + ) end def test_standalone_host_retains_runtime_across_scene_recreation @@ -1135,7 +1204,30 @@ def test_standalone_host_has_real_pty_and_files_ui_smoke ui_test, '"FullDocumentManagerViewControllerNavigationBar"' ) - assert_includes ui_test, "pickerLanding.waitForExistence(timeout: 30)" + assert_includes ui_test, "let pickerDeadline = Date().addingTimeInterval(60)" + assert_includes( + ui_test, + 'XCTFail("document picker to expose a usable Host or local destination")' + ) + assert_includes ui_test, "while Date() < pickerDeadline" + assert_includes ui_test, "if currentHostDocuments.exists {" + assert_includes ui_test, "if hostFixture.exists {" + assert_includes ui_test, "continue" + assert_includes ui_test, "var didTapBrowse = false" + assert_includes ui_test, 'XCTFail("document picker Browse to become hittable")' + assert_includes ui_test, "hostFixture.waitForExistence(timeout: 30)" + assert_includes ui_test, 'XCTFail("Host Documents fixture to become visible")' + assert_equal 3, ui_test.scan("guard openHostDocuments(in: app) else").length + assert_equal 2, ui_test.scan("guard importHostFixture(in: app) else").length + assert_includes ui_test, "private func importHostFixture(" + assert_includes ui_test, "for _ in 0..<2" + assert_includes ui_test, "if imported.waitForExistence(timeout: 15)" + assert_includes( + ui_test, + 'XCTFail("Host Documents fixture selection to import the guest file")' + ) + refute_includes ui_test, "fixture.tap()" + refute_includes ui_test, "exported.tap()" assert_includes ui_test, "for _ in 0..<2" assert_includes ui_test, "hostDestination.waitForExistence(timeout: 10)" assert_includes ui_test, "openedHostDestination" @@ -1147,11 +1239,29 @@ def test_standalone_host_has_real_pty_and_files_ui_smoke 'NSPredicate(format: "label BEGINSWITH %@", "PocketRoot Host")' ) assert_operator( - ui_test.index("pickerLanding.waitForExistence(timeout: 30)"), + ui_test.index("let pickerDeadline = Date().addingTimeInterval(60)"), :<, - ui_test.index("localLocation.waitForExistence(timeout: 30)") + ui_test.index("hostFixture.waitForExistence(timeout: 30)") + ) + assert_operator( + ui_test.index("if currentHostDocuments.exists {"), + :<, + ui_test.index("if sidebarLocalLocation.exists {") ) assert_includes ui_test, "testPTYLifecycleAndShutdown" + query_terminal_size = ui_test[ + /private func queryTerminalSize\(.*?\n \}\n\n private enum WidthChangeDirection/m + ] + refute_nil query_terminal_size + assert_includes( + query_terminal_size, + %q{let command = "printf '\(marker)'; stty size\n"} + ) + assert_includes query_terminal_size, "if !waitWithoutAssertion(" + assert_includes query_terminal_size, "timeout: 15" + assert_includes query_terminal_size, "terminal.tap()" + assert_equal 2, query_terminal_size.scan("terminal.typeText(command)").length + assert_includes query_terminal_size, "guard wait(" assert_includes ui_test, "dismissKeyboardOnboardingIfPresent(in: app)" assert_includes ui_test, "let interruptFrames = waitForInteractionFrames(" assert_includes ui_test, "interruptFrames.elementFrame" @@ -1182,8 +1292,10 @@ def test_standalone_host_has_real_pty_and_files_ui_smoke assert_includes generic_runner, "POCKETROOT_UI_SKIP_TESTING" assert_includes generic_runner, "POCKETROOT_UI_FAILURE_ARTIFACTS_DIR" assert_includes generic_runner, "POCKETROOT_UI_INFRASTRUCTURE_RETRY_LIMIT" - assert_includes generic_runner, "is_retryable_simulator_launch_failure" + assert_includes generic_runner, "retryable_simulator_failure_kind" assert_includes generic_runner, "is unknown to FrontBoard" + assert_includes generic_runner, "no available devices matched the request" + assert_includes generic_runner, "replacement_device" assert_includes generic_runner, '"$CREATED_DEVICE" == "true"' assert_includes generic_runner, "xcodebuild-test-attempt-1.log" assert_includes generic_runner, "-attempt-1.xcresult" diff --git a/Tests/Scripts/RuntimeSmokeScriptTests.sh b/Tests/Scripts/RuntimeSmokeScriptTests.sh index 96578bc..d7a2589 100755 --- a/Tests/Scripts/RuntimeSmokeScriptTests.sh +++ b/Tests/Scripts/RuntimeSmokeScriptTests.sh @@ -84,8 +84,9 @@ if ! grep -Fq -- 'POCKETROOT_KEEP_UI_RESULT' "$GENERIC_UI_RUNNER" \ || ! grep -Fq -- 'POCKETROOT_UI_SKIP_TESTING' "$GENERIC_UI_RUNNER" \ || ! grep -Fq -- 'POCKETROOT_UI_FAILURE_ARTIFACTS_DIR' "$GENERIC_UI_RUNNER" \ || ! grep -Fq -- 'POCKETROOT_UI_INFRASTRUCTURE_RETRY_LIMIT' "$GENERIC_UI_RUNNER" \ - || ! grep -Fq -- 'is_retryable_simulator_launch_failure' "$GENERIC_UI_RUNNER" \ + || ! grep -Fq -- 'retryable_simulator_failure_kind' "$GENERIC_UI_RUNNER" \ || ! grep -Fq -- "is unknown to FrontBoard" "$GENERIC_UI_RUNNER" \ + || ! grep -Fq -- 'no available devices matched the request' "$GENERIC_UI_RUNNER" \ || ! grep -Fq -- 'retrying once' "$GENERIC_UI_RUNNER" \ || ! grep -Fq -- 'xcodebuild-test-attempt-1.log' "$GENERIC_UI_RUNNER" \ || ! grep -Fq -- '-attempt-1.xcresult' "$GENERIC_UI_RUNNER" \ @@ -185,6 +186,11 @@ if [[ "${POCKETROOT_MOCK_XCODEBUILD_MODE:-retry}" == "retry" && "$calls" -eq 1 ] echo 'Application "com.example.UITests.xctrunner" is unknown to FrontBoard.' exit 65 fi +if [[ "${POCKETROOT_MOCK_XCODEBUILD_MODE:-retry}" == "missing-destination" && "$calls" -eq 1 ]]; then + echo "Unable to find a destination matching the provided destination specifier:" + echo "The requested device could not be found because no available devices matched the request." + exit 70 +fi if [[ "${POCKETROOT_MOCK_XCODEBUILD_MODE:-retry}" == "assertion" ]]; then echo "Testing failed: expected value did not match" exit 65 @@ -214,6 +220,32 @@ if [[ "$(cat "$GENERIC_RUNNER_XCODEBUILD_CALLS")" != "2" ]] \ exit 1 fi +printf '0\n' > "$GENERIC_RUNNER_XCODEBUILD_CALLS" +: > "$GENERIC_RUNNER_XCRUN_CALLS" +: > "$GENERIC_RUNNER_BOOTSTATUS_CALLS" +: > "$GENERIC_RUNNER_BOOT_CALLS" +MISSING_DESTINATION_OUTPUT="$GENERIC_RUNNER_TEST_ROOT/missing-destination-output.txt" +PATH="$GENERIC_RUNNER_MOCK_BIN:$PATH" \ +POCKETROOT_UI_APP_DIR="$GENERIC_RUNNER_APP" \ +POCKETROOT_UI_PROJECT_NAME="MockApp" \ +POCKETROOT_UI_SCHEME="MockApp" \ +POCKETROOT_UI_TEST_BUNDLE="MockAppUITests" \ +POCKETROOT_CLONED_SOURCE_PACKAGES_DIR="$GENERIC_RUNNER_TEST_ROOT/packages" \ +POCKETROOT_MOCK_XCODEBUILD_CALLS="$GENERIC_RUNNER_XCODEBUILD_CALLS" \ +POCKETROOT_MOCK_XCRUN_CALLS="$GENERIC_RUNNER_XCRUN_CALLS" \ +POCKETROOT_MOCK_BOOTSTATUS_CALLS="$GENERIC_RUNNER_BOOTSTATUS_CALLS" \ +POCKETROOT_MOCK_BOOT_CALLS="$GENERIC_RUNNER_BOOT_CALLS" \ +POCKETROOT_MOCK_XCODEBUILD_MODE="missing-destination" \ + "$GENERIC_UI_RUNNER" "$GENERIC_RUNNER_ARCHIVE" \ + > "$MISSING_DESTINATION_OUTPUT" 2>&1 + +if [[ "$(cat "$GENERIC_RUNNER_XCODEBUILD_CALLS")" != "2" ]] \ + || [[ "$(grep -Fc -- 'simctl create' "$GENERIC_RUNNER_XCRUN_CALLS")" != "2" ]] \ + || ! grep -Fq -- 'destination disappeared' "$MISSING_DESTINATION_OUTPUT"; then + echo "Shared example UI runner did not recreate a disappeared temporary Simulator." >&2 + exit 1 +fi + printf '0\n' > "$GENERIC_RUNNER_XCODEBUILD_CALLS" : > "$GENERIC_RUNNER_XCRUN_CALLS" : > "$GENERIC_RUNNER_BOOT_CALLS"