diff --git a/.agents/skills/ios-simulator-test-recording/SKILL.md b/.agents/skills/ios-simulator-test-recording/SKILL.md new file mode 100644 index 00000000000..328d07befda --- /dev/null +++ b/.agents/skills/ios-simulator-test-recording/SKILL.md @@ -0,0 +1,73 @@ +--- +name: ios-simulator-test-recording +description: >- + Skill to run xcodebuild tests on iOS Simulator while recording a video walkthrough, dynamically selecting the + highest available OS and device. Most useful for running XCUITests. +--- + +# iOS Simulator Test Recording Skill + +This skill provides a generalized bash script to test run and record iOS Simulator workflows and UI tests. It handles +everything natively through AWK TTY streams to ensure the camera triggers exactly when the app launches — eliminating +the simulator's booting and install sequence. + +## The Script: `run_test_and_record.sh` + +The core logic of this skill resides in `./.agents/skills/ios-simulator-test-recording/scripts/run_test_and_record.sh`. +You can call this script whenever you need to execute a test block accompanied by screen recording. + +### Usage + +```bash +./.agents/skills/ios-simulator-test-recording/scripts/run_test_and_record.sh [OPTIONS] -- COMMAND +``` + +#### Options + +- `--video PATH` : Change the video output file. Default: `simulator_test_walkthrough.mp4` +- `--show-ui` : Explicitly brings the Simulator.app window to the foreground so the user sees the execution happening. + By default, it runs the simulator headless or in the background. +- `--udid UDID` : Specifies the ID of the simulator to attach to. If left omitted, it queries + `xcrun simctl list devices available -j` and auto-bootstraps the highest tier iPhone class. + +#### Substituting the Device ID + +If your test script requires injecting the target UDID dynamically, you can use the `{UDID}` placeholder inside your +command execution block. The script will intercept and substitute it before launching. + +### Example Implementations + +**1. Basic Automated Fallback (Latest Simulator + Default Video)** +```bash + ./.agents/skills/ios-simulator-test-recording/scripts/run_test_and_record.sh \ + -- xcodebuild test -project SampleApp.xcodeproj -scheme SampleApp \ + -destination "platform=iOS Simulator,id={UDID}" -quiet +``` +*Note the usage of `{UDID}` to let the script automatically populate the chosen device ID.* + +**2. Custom Video Path & Show UI** +```bash + ./.agents/skills/ios-simulator-test-recording/scripts/run_test_and_record.sh \ + --show-ui \ + --video "~/Code/firebase-ios-sdk/auth_test.mp4" \ + -- xcodebuild test -workspace App.xcworkspace -scheme AppUITests \ + -destination "platform=iOS Simulator,id={UDID}" -only-testing:AppUITests/testLogin -quiet +``` + +**3. With a specific Custom UDID provided** +```bash + ./.agents/skills/ios-simulator-test-recording/scripts/run_test_and_record.sh \ + --udid "62425200-F824-4E55-ACB4-08D031165A82" \ + --video ./fast_test.mp4 \ + -- xcodebuild test -project Proj.xcodeproj ... -destination "platform=iOS Simulator,id={UDID}" +``` + +### Important Maintenance Notes +If future framework changes cause the script to fail, take these notes into consideration: +- **Avoid** using `tail -F` on XCTest logs inside CI arrays (it natively locks trailing descriptors indefinitely). +- **Avoid** using "magic numbers" and `sleep` commands to time the simulator launch. The script uses a robust + TTY-based stream hook that triggers exactly when the test runner starts, ensuring perfect synchronization without + timing dependencies. +- **Avoid** pipe `xcodebuild` into `tee` logs natively; block buffering will cache the entire script payload until + test execution completion, causing the hook to deploy at the literal end of the tape, wiping your data. Always use + the internal TTY `script` wrapper! diff --git a/.agents/skills/ios-simulator-test-recording/scripts/run_test_and_record.sh b/.agents/skills/ios-simulator-test-recording/scripts/run_test_and_record.sh new file mode 100755 index 00000000000..159a58816be --- /dev/null +++ b/.agents/skills/ios-simulator-test-recording/scripts/run_test_and_record.sh @@ -0,0 +1,190 @@ +#!/bin/bash + +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -e + +SHOW_UI=false +VIDEO_PATH="simulator_test_walkthrough.mp4" +UDID="" + +while [[ "$#" -gt 0 ]]; do + case $1 in + --show-ui) SHOW_UI=true; shift ;; + --video) VIDEO_PATH="$2"; shift 2 ;; + --udid) UDID="$2"; shift 2 ;; + --) shift; break ;; + -*) echo "Unknown parameter passed: $1"; exit 1 ;; + *) break ;; + esac +done + +if [[ -z "$UDID" ]]; then + echo "0. Auto-selecting latest iOS Simulator..." + UDID=$(python3 -c " +import json, subprocess, re +try: + data = json.loads(subprocess.check_output(['xcrun', 'simctl', 'list', 'devices', 'available', '-j'])) + runtimes = sorted([r for r in data['devices'].keys() if 'iOS' in r], reverse=True) + if not runtimes: exit(1) + + def iphone_rank(d): + name = d['name'] + if 'SE' in name: return (-1, 0, name) + match = re.search(r'iPhone\s+(\d+)', name) + num = int(match.group(1)) if match else 0 + tier = 0 + if 'Pro Max' in name: tier = 1 + elif 'Pro' in name: tier = 3 + elif 'Plus' in name: tier = 0 + else: tier = 2 # Standard + return (num, tier, name) + + iphones = [d for d in data['devices'][runtimes[0]] if 'iPhone' in d['name']] + if not iphones: exit(1) + iphones.sort(key=iphone_rank, reverse=True) + print(iphones[0]['udid']) +except Exception: + exit(1) +") + if [[ -z "$UDID" ]]; then + echo "Error: Could not auto-detect the latest iOS Simulator. Please provide a --udid manually." + exit 1 + fi + echo "Selected UDID: $UDID" +fi + +if [[ -z "$1" ]]; then + echo "Error: No test command provided." + echo "" + echo "Usage: ./run_test_and_record.sh [--show-ui] [--video PATH] [--udid UDID] -- xcodebuild test ..." + echo "Note: Use the \{UDID\} placeholder in your test command to substitute the selected device UDID." + exit 1 +fi + +echo "1. Preparing test command..." +# Substitute {UDID} with actual UDID in the arguments +TEST_CMD=() +for arg in "$@"; do + TEST_CMD+=("${arg//\{UDID\}/$UDID}") +done + +BUILD_CMD=() +IS_XCODEBUILD_TEST=false + +# Try building _before_ running, that way we don't end up with a blank simulator recording for a test that wouldn't build. +if [[ "${TEST_CMD[0]}" == "xcodebuild" ]]; then + for i in "${!TEST_CMD[@]}"; do + if [[ "${TEST_CMD[$i]}" == "test" || "${TEST_CMD[$i]}" == "test-without-building" ]]; then + IS_XCODEBUILD_TEST=true + # Create a dedicated build command + BUILD_CMD=("${TEST_CMD[@]}") + if [[ "${TEST_CMD[$i]}" == "test" ]]; then + BUILD_CMD[$i]="build-for-testing" + TEST_CMD[$i]="test-without-building" + fi + + # Prevent Xcode from spawning clones ("Clone 1 of iPhone...") which ruins video capture targeting the base UDID + TEST_CMD+=("-disable-concurrent-destination-testing" "-parallel-testing-enabled" "NO") + BUILD_CMD+=("-disable-concurrent-destination-testing" "-parallel-testing-enabled" "NO") + break + fi + done +fi + +if [ "$IS_XCODEBUILD_TEST" = true ]; then + echo "1a. Auto-detected 'xcodebuild test'. Running 'build-for-testing' before booting simulator..." + echo "\$ ${BUILD_CMD[@]}" + set +e + "${BUILD_CMD[@]}" + BUILD_RESULT=$? + set -e + + if [ $BUILD_RESULT -ne 0 ]; then + echo "Error: Compilation failed (Exit $BUILD_RESULT). Aborting video recording and simulator boot." + exit $BUILD_RESULT + fi +fi + +echo "2. Booting simulator $UDID..." +xcrun simctl boot "$UDID" || true + +if [ "$SHOW_UI" = true ]; then + echo "Showing Simulator UI..." + open -a Simulator +fi + +# Move the old video file if it still exists. +if [[ -f "$VIDEO_PATH" ]]; then + TIMESTAMP=$(date +"%Y%m%d_%H%M%S") + FILENAME=$(basename "$VIDEO_PATH") + EXTENSION="${FILENAME##*.}" + FILENAME_NO_EXT="${FILENAME%.*}" + DIRNAME=$(dirname "$VIDEO_PATH") + BACKUP_PATH="$DIRNAME/${FILENAME_NO_EXT}_${TIMESTAMP}.${EXTENSION}" + echo "Found existing video. Renaming old file to $BACKUP_PATH..." + mv "$VIDEO_PATH" "$BACKUP_PATH" +fi + +echo "3. Starting xcodebuild testing payload..." + +rm -f "/tmp/simctl_record_pid_$$.txt" + +# Run xcodebuild natively against a TTY terminal via 'script' to prevent block-buffering, then stream through AWK +# AWK immediately echos each line, and fires the video recorder exactly when the signal crosses the pipe. +set +e +script -q /dev/null "${TEST_CMD[@]}" | awk '{ + print $0; + if ($0 ~ /Testing started/ && !triggered) { + system("echo -e \"\\n[+] Test suite execution detected! Engaging camera lens...\" >&2"); + system("xcrun simctl io '"$UDID"' recordVideo '"$VIDEO_PATH"' > /dev/null 2>&1 & echo $! > \"/tmp/simctl_record_pid_'$$'.txt\""); + triggered=1; + } +}' +TEST_RESULT=${PIPESTATUS[0]} +set -e + +if [[ -f "/tmp/simctl_record_pid_$$.txt" ]]; then + RECORD_PID=$(cat "/tmp/simctl_record_pid_$$.txt") + echo "4. Test finished (Exit $TEST_RESULT). Sending SIGINT to recording PID $RECORD_PID..." + # Capture the final UI state or test outcomes for a few seconds after tests finish before stopping + sleep 3 + kill -INT "$RECORD_PID" 2>/dev/null || true + echo "Waiting for video file to finalize..." + # Wait for it to be done - this is likely long enough. May need to tweak for later. + sleep 5 + rm -f "/tmp/simctl_record_pid_$$.txt" +else + # In case the test failed extremely fast before AWK could trigger + echo "Warning: Test completed too fast; video lens never engaged." +fi + +echo "5. Verifying video file size..." +if [[ -f "$VIDEO_PATH" ]]; then + FILE_SIZE=$(stat -f%z "$VIDEO_PATH" 2>/dev/null || stat -c%s "$VIDEO_PATH" 2>/dev/null || echo "0") + echo "Video file size: ${FILE_SIZE} bytes" + + if [[ "$FILE_SIZE" -gt 0 ]]; then + echo "Success: Video generated properly at $VIDEO_PATH" + else + echo "Error: Video file is empty." + exit 1 + fi +else + echo "Error: Video file is missing." + exit 1 +fi + +exit $TEST_RESULT diff --git a/agents.md b/agents.md index 46df803073a..f9e2f01fec9 100644 --- a/agents.md +++ b/agents.md @@ -1,9 +1,8 @@ -# Jules.md - Context for AI Assisted Development +# agents.md - Context for AI Assisted Development -This document provides essential context and guidelines for Jules, an AI software engineering -agent, to effectively understand, navigate, and make changes within the `firebase-ios-sdk` -repository. It highlights key aspects of the development environment, coding practices, and -architectural patterns. +This document provides essential context and guidelines for AI agents to effectively understand, +navigate, and make changes within the `firebase-ios-sdk` repository. It highlights key aspects of the +development environment, coding practices, and architectural patterns. ## Setup Commands @@ -12,13 +11,13 @@ following setup is beneficial. ### Prerequisites -1. **Xcode**: The development environment relies on Xcode 16.2 or above. +1. **Xcode**: The development environment relies on Xcode 26.2 or above. 2. **Command-Line Tools**: - * `clang-format`: Used for C, C++, and Objective-C code formatting. Version 20 is + * `clang-format`: Used for C, C++, and Objective-C code formatting. Version 22 is specifically mentioned. * `mint`: Used to install and run Swift command-line tools, including `swiftformat` for Swift code styling. - * Note: The build environment uses `clang-format` (version 20) and `mint` (for + * Note: The build environment uses `clang-format` (version 22) and `mint` (for `swiftformat`) for code styling. Ensure any generated or modified code adheres to these styling requirements. diff --git a/scripts/style.sh b/scripts/style.sh index 868f0e5286d..1c57e20c733 100755 --- a/scripts/style.sh +++ b/scripts/style.sh @@ -173,8 +173,8 @@ s%^./%% \%\.pb\.% d \%\.nanopb\.% d -# Format C-ish sources only -\%\.(h|m|mm|cc|swift)$% p +# Format C-ish sources and shell scripts only +\%\.(h|m|mm|cc|swift|sh)$% p ' ) @@ -185,6 +185,16 @@ for f in $files; do # 1/1 files would have been formatted. (with --dryrun) # 1/1 files formatted. (without --dryrun) mint run swiftformat "${swift_options[@]}" "$f" 2>&1 | grep '^1/1 files' > /dev/null + elif [[ "${f: -3}" == '.sh' ]]; then + if [[ "$test_only" == true ]]; then + grep -E '[[:space:]]+$' "$f" > /dev/null + else + if [[ "$(uname -s)" == "Darwin" ]]; then + sed -i '' -E 's/[[:space:]]+$//' "$f" + else + sed -i -E 's/[[:space:]]+$//' "$f" + fi + fi else "$clang_format_bin" "${clang_options[@]}" "$f" | grep " /dev/null fi