diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..5ace460 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/README.md b/.github/workflows/README.md index abd21e7..65165cb 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -2,9 +2,9 @@ This directory contains comprehensive CI/CD workflows using **orchestrated testing** powered by process-compose for the Devbox mobile plugins and example projects. -## What's New: Orchestrated Testing ๐Ÿš€ +## Orchestrated Testing -All workflows now use process-compose orchestration with: +All workflows use process-compose orchestration with: - โœ… **Automatic status checks** - Boot verification, app deployment, process health - โœ… **Concurrent execution** - Independent tests run in parallel - โœ… **Configurable timeouts** - No infinite hangs (`BOOT_TIMEOUT`, `TEST_TIMEOUT`) @@ -77,17 +77,17 @@ This workflow provides fast feedback with **improved reliability** through autom - **Min**: API 21 (Android 5.0 Lollipop) on ubuntu-24.04 - **Max**: API 36 (Android 15) on ubuntu-24.04 - **Hardware Acceleration**: KVM enabled for performance -- **NEW**: Orchestrated with automatic boot verification +- Orchestrated with automatic boot verification #### iOS - **Min**: iOS 15.4 on macos-14 (first Apple Silicon macOS supporting iOS 15.4) - **Max**: iOS 26.2 on macos-15 (latest macOS version) -- **NEW**: Orchestrated with automatic boot verification +- Orchestrated with automatic boot verification #### React Native - Tests both Android and iOS builds on min/max versions -- **NEW**: Unified job with platform matrix (was split before) -- **NEW**: Orchestrated with full status checking +- Unified job with platform matrix +- Orchestrated with full status checking **Jobs**: @@ -210,7 +210,7 @@ When a test fails: 1. **Check the job logs** in the Actions tab 2. **Download artifacts** (uploaded automatically on failure): - - **NEW**: Process-compose logs per process (setup, build, boot, deploy, verify) + - Process-compose logs per process (setup, build, boot, deploy, verify) - Android: `/tmp/android-e2e-logs/`, build outputs - iOS: `/tmp/ios-e2e-logs/`, CoreSimulator logs - React Native: `/tmp/rn-e2e-logs/`, both platforms diff --git a/.github/workflows/e2e-full.yml b/.github/workflows/e2e-full.yml index 335713b..b0fbe42 100644 --- a/.github/workflows/e2e-full.yml +++ b/.github/workflows/e2e-full.yml @@ -61,12 +61,11 @@ jobs: - name: Run Android E2E test working-directory: examples/android env: - EMU_HEADLESS: 1 BOOT_TIMEOUT: 240 TEST_TIMEOUT: 600 ANDROID_DEFAULT_DEVICE: ${{ matrix.device }} TEST_TUI: false - run: devbox run --pure test:e2e + run: devbox run --pure -e EMU_HEADLESS=1 test:e2e - name: Upload reports and logs if: always() @@ -91,7 +90,7 @@ jobs: - device: min os: macos-14 - device: max - os: macos-15 + os: macos-26 steps: - uses: actions/checkout@v4 @@ -122,12 +121,11 @@ jobs: - name: Run iOS E2E test working-directory: examples/ios env: - SIM_HEADLESS: 1 BOOT_TIMEOUT: 180 TEST_TIMEOUT: 600 IOS_DEFAULT_DEVICE: ${{ matrix.device }} TEST_TUI: false - run: devbox run --pure test:e2e + run: devbox run --pure -e SIM_HEADLESS=1 test:e2e - name: Upload reports and logs if: always() @@ -162,7 +160,7 @@ jobs: os: macos-14 - platform: ios device: max - os: macos-15 + os: macos-26 # Web test (fast, no device needed) - platform: web device: none @@ -223,23 +221,13 @@ jobs: - name: Run React Native E2E test working-directory: examples/react-native - env: - EMU_HEADLESS: ${{ matrix.platform == 'android' && '1' || '0' }} - SIM_HEADLESS: ${{ matrix.platform == 'ios' && '1' || '0' }} - BOOT_TIMEOUT: 300 - TEST_TIMEOUT: 900 - ANDROID_DEFAULT_DEVICE: ${{ matrix.device }} - IOS_DEFAULT_DEVICE: ${{ matrix.device }} - TEST_TUI: false run: | if [ "${{ matrix.platform }}" = "android" ]; then - # Use wrapper script for platform-specific optimization - bash tests/run-android-tests.sh + devbox run --pure -e IOS_SKIP_SETUP=1 -e EMU_HEADLESS=1 test:e2e:android elif [ "${{ matrix.platform }}" = "ios" ]; then - # Use wrapper script for platform-specific optimization - bash tests/run-ios-tests.sh + devbox run --pure -e ANDROID_SKIP_SETUP=1 -e SIM_HEADLESS=1 test:e2e:ios elif [ "${{ matrix.platform }}" = "web" ]; then - devbox run test:e2e:web + devbox run --pure -e ANDROID_SKIP_SETUP=1 -e IOS_SKIP_SETUP=1 test:e2e:web fi - name: Upload reports and logs diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index d64bd1a..31bd6a0 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -36,16 +36,12 @@ jobs: reports/ retention-days: 7 - # Android example E2E tests (min/max devices) + # Android example E2E test (max device only on PRs) android-e2e: - name: Android E2E - ${{ matrix.device }} + name: Android E2E - max runs-on: ubuntu-24.04 timeout-minutes: 30 needs: fast-tests - strategy: - fail-fast: false - matrix: - device: [min, max] steps: - uses: actions/checkout@v4 @@ -73,37 +69,28 @@ jobs: - name: Run Android E2E test working-directory: examples/android env: - EMU_HEADLESS: 1 BOOT_TIMEOUT: 180 TEST_TIMEOUT: 300 - ANDROID_DEFAULT_DEVICE: ${{ matrix.device }} + ANDROID_DEFAULT_DEVICE: max TEST_TUI: false - run: devbox run --pure test:e2e + run: devbox run --pure -e EMU_HEADLESS=1 test:e2e - name: Upload reports and logs if: always() uses: actions/upload-artifact@v4 with: - name: android-${{ matrix.device }}-reports + name: android-max-reports path: | examples/android/reports/ examples/android/app/build/outputs/ retention-days: 7 - # iOS example E2E tests (min/max devices) + # iOS example E2E test (max device only on PRs) ios-e2e: - name: iOS E2E - ${{ matrix.device }} - runs-on: ${{ matrix.os }} + name: iOS E2E - max + runs-on: macos-26 timeout-minutes: 25 needs: fast-tests - strategy: - fail-fast: false - matrix: - include: - - device: min - os: macos-14 - - device: max - os: macos-15 steps: - uses: actions/checkout@v4 @@ -134,24 +121,23 @@ jobs: - name: Run iOS E2E test working-directory: examples/ios env: - SIM_HEADLESS: 1 BOOT_TIMEOUT: 120 TEST_TIMEOUT: 300 - IOS_DEFAULT_DEVICE: ${{ matrix.device }} + IOS_DEFAULT_DEVICE: max TEST_TUI: false - run: devbox run --pure test:e2e + run: devbox run --pure -e SIM_HEADLESS=1 test:e2e - name: Upload reports and logs if: always() uses: actions/upload-artifact@v4 with: - name: ios-${{ matrix.device }}-reports + name: ios-max-reports path: | examples/ios/reports/ ~/Library/Logs/CoreSimulator/ retention-days: 7 - # React Native E2E tests (Android min/max, iOS min/max, Web) + # React Native E2E tests (max device only on PRs, plus web) react-native-e2e: name: React Native E2E - ${{ matrix.platform }}-${{ matrix.device }} runs-on: ${{ matrix.os }} @@ -161,21 +147,12 @@ jobs: fail-fast: false matrix: include: - # Android tests - - platform: android - device: min - os: ubuntu-24.04 - platform: android device: max os: ubuntu-24.04 - # iOS tests - - platform: ios - device: min - os: macos-14 - platform: ios device: max - os: macos-15 - # Web test (fast, no device needed) + os: macos-26 - platform: web device: none os: ubuntu-24.04 @@ -235,23 +212,13 @@ jobs: - name: Run React Native E2E test working-directory: examples/react-native - env: - EMU_HEADLESS: ${{ matrix.platform == 'android' && '1' || '0' }} - SIM_HEADLESS: ${{ matrix.platform == 'ios' && '1' || '0' }} - BOOT_TIMEOUT: 240 - TEST_TIMEOUT: 600 - ANDROID_DEFAULT_DEVICE: ${{ matrix.device }} - IOS_DEFAULT_DEVICE: ${{ matrix.device }} - TEST_TUI: false run: | if [ "${{ matrix.platform }}" = "android" ]; then - # Use wrapper script for platform-specific optimization - bash tests/run-android-tests.sh + devbox run --pure -e IOS_SKIP_SETUP=1 -e EMU_HEADLESS=1 test:e2e:android elif [ "${{ matrix.platform }}" = "ios" ]; then - # Use wrapper script for platform-specific optimization - bash tests/run-ios-tests.sh + devbox run --pure -e ANDROID_SKIP_SETUP=1 -e SIM_HEADLESS=1 test:e2e:ios elif [ "${{ matrix.platform }}" = "web" ]; then - devbox run test:e2e:web + devbox run --pure -e ANDROID_SKIP_SETUP=1 -e IOS_SKIP_SETUP=1 test:e2e:web fi - name: Upload reports and logs @@ -275,7 +242,7 @@ jobs: steps: - name: Check job results run: | - echo "๐Ÿ“Š PR Check Results:" + echo "PR Check Results:" echo " Fast Tests: ${{ needs.fast-tests.result }}" echo " Android E2E: ${{ needs.android-e2e.result }}" echo " iOS E2E: ${{ needs.ios-e2e.result }}" @@ -289,4 +256,4 @@ jobs: echo "::error::One or more PR checks failed" exit 1 fi - echo "::notice::โœ… All PR checks passed!" + echo "::notice::All PR checks passed!" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d129a64..9290da9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,6 +3,10 @@ on: push: branches: [main] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: check-release: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 2a4e7a9..3746265 100644 --- a/.gitignore +++ b/.gitignore @@ -23,9 +23,12 @@ *.cache .claude/ -# Test results (only last run kept locally) +# Test results and temp dirs (only last run kept locally) /reports/ +/reports/tmp/ /test-results/ +tests/test-results/ examples/*/reports/ examples/*/test-results/ plugins/*/tests/reports/ +plugins/*/tests/test-results/ diff --git a/CLAUDE.md b/CLAUDE.md index 224f7b6..885ad8a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -169,15 +169,17 @@ log_file="/tmp/test.log" # WRONG - may be cleaned up by system Three main plugins are located in `plugins/`: 1. **android** - Android SDK + emulator management via Nix flake - - SDK flake: `devbox.d/android/flake.nix` - - Device definitions: `devbox.d/android/devices/*.json` + - SDK flake: `devbox.d//flake.nix` + - Device definitions: `devbox.d//devices/*.json` - Scripts: `.devbox/virtenv/android/scripts/` - Configuration: Environment variables in `plugin.json` + - Note: `` is `android` for local includes, but for GitHub includes it uses the full path (e.g., `segment-integrations.devbox-plugins.android`) 2. **ios** - iOS toolchain + simulator management for macOS - - Device definitions: `devbox.d/ios/devices/*.json` + - Device definitions: `devbox.d//devices/*.json` - Scripts: `.devbox/virtenv/ios/scripts/` - Configuration: Environment variables in `plugin.json` + - Note: `` is `ios` for local includes, but for GitHub includes it uses the full path (e.g., `segment-integrations.devbox-plugins.ios`) 3. **react-native** - Composition layer over Android + iOS plugins - Inherits both Android and iOS device management @@ -185,7 +187,7 @@ Three main plugins are located in `plugins/`: ### Key Concepts -**Device Definitions**: JSON files defining emulator/simulator configurations +**Device Definitions**: JSON files defining emulator/simulator configurations stored in `devbox.d//devices/`. The actual directory name for `` depends on how the plugin is included: for local includes (e.g., `plugin:../plugins/android`) it matches the plugin name (e.g., `android`), but for GitHub includes it uses the full dotted path (e.g., `segment-integrations.devbox-plugins.android`). - Android: `{name, api, device, tag, preferred_abi}` - iOS: `{name, runtime}` - Default devices: `min.json` and `max.json` @@ -360,34 +362,34 @@ devbox run --pure ios.sh config show ```bash cd examples/android -# Build the app -devbox run --pure build-android +# Build the app (user-defined in example devbox.json) +devbox run --pure build:android -# Start emulator -devbox run --pure start-emu [device] # Defaults to ANDROID_DEFAULT_DEVICE +# Start emulator (plugin-provided) +devbox run --pure start:emu [device] # Defaults to ANDROID_DEFAULT_DEVICE -# Build, install, and launch app on emulator -devbox run --pure start-app [device] +# Build, install, and launch app on emulator (user-defined in example devbox.json) +devbox run --pure start:app [device] -# Stop emulator -devbox run --pure stop-emu +# Stop emulator (plugin-provided) +devbox run --pure stop:emu ``` #### iOS ```bash cd examples/ios -# Build the app -devbox run --pure build-ios +# Build the app (user-defined in example devbox.json) +devbox run --pure build:ios -# Start simulator -devbox run --pure start-sim [device] # Defaults to IOS_DEFAULT_DEVICE +# Start simulator (plugin-provided) +devbox run --pure start:sim [device] # Defaults to IOS_DEFAULT_DEVICE -# Build, install, and launch app on simulator -devbox run --pure start-ios [device] +# Build, install, and launch app on simulator (plugin-provided via ios.sh run) +devbox run --pure start:app [device] -# Stop simulator -devbox run --pure stop-sim +# Stop simulator (plugin-provided) +devbox run --pure stop:sim ``` #### React Native @@ -398,17 +400,17 @@ cd examples/react-native npm install # Android workflow -devbox run --pure start-emu [device] -devbox run --pure start-app [device] -devbox run --pure stop-emu +devbox run --pure start:emu [device] # plugin-provided +devbox run --pure start:app [device] # user-defined +devbox run --pure stop:emu # plugin-provided # iOS workflow -devbox run --pure start-sim [device] -devbox run --pure start-ios [device] -devbox run --pure stop-sim +devbox run --pure start:sim [device] # plugin-provided +devbox run --pure start:app [device] # user-defined (calls ios.sh run) +devbox run --pure stop:sim # plugin-provided -# Build for all platforms -devbox run build # Runs build-android, build-ios, build-web +# Build for all platforms (user-defined) +devbox run build # Runs build:android, build:ios, build:web ``` ### Testing @@ -467,9 +469,13 @@ act -j ios-plugin-tests โ”‚ โ”œโ”€โ”€ e2e-react-native.sh โ”‚ โ”œโ”€โ”€ e2e-sequential.sh โ”‚ โ””โ”€โ”€ e2e-all.sh +โ”œโ”€โ”€ wiki/ # Documentation +โ”‚ โ”œโ”€โ”€ guides/ # User guides and cheatsheets +โ”‚ โ”œโ”€โ”€ reference/ # CLI, config, and env var references +โ”‚ โ””โ”€โ”€ project/ # Architecture and strategy docs โ”œโ”€โ”€ .github/workflows/ -โ”‚ โ”œโ”€โ”€ pr-checks.yml # Fast PR validation (~15-30 min) -โ”‚ โ””โ”€โ”€ e2e-full.yml # Full E2E tests (~45-60 min per platform) +โ”‚ โ”œโ”€โ”€ pr-checks.yml # Fast PR validation +โ”‚ โ””โ”€โ”€ e2e-full.yml # Full E2E tests โ””โ”€โ”€ devbox.json # Root devbox config ``` @@ -513,7 +519,7 @@ See `wiki/project/ARCHITECTURE.md` for complete documentation. ### Device Management Workflow -1. Device definitions are JSON files in `devbox.d/{platform}/devices/` +1. Device definitions are JSON files in `devbox.d//devices/` (where `` depends on the include method; see Plugin System above) 2. Modify devices using CLI commands (not manual editing) 3. After changes, regenerate lock file: `{platform}.sh devices eval` 4. Lock files should be committed to optimize CI @@ -588,7 +594,12 @@ Document public APIs exhaustively in REFERENCE.md files, not in code comments. **Reduce edge cases and unexpected behavior.** Design for the common path. When edge cases arise, validate assumptions early and fail fast rather than adding complex branching logic. -**Scripts fail on error.** All shell scripts use `set -euo pipefail` (or `set -eu` for POSIX sh). Functions return 0 on success, non-zero on failure. Avoid `|| true` except in validation functions where warnings shouldn't block execution. +**Scripts fail on error.** Shell scripts use different strictness levels depending on context: +- Sourced scripts (lib.sh, core.sh, setup.sh): `set -e` (no `-u` due to Node.js package compatibility issues with unset variables) +- User-facing CLIs (android.sh, ios.sh, devices.sh): `set -eu` +- Test scripts: `set -euo pipefail` + +Functions return 0 on success, non-zero on failure. Avoid `|| true` except in validation functions where warnings shouldn't block execution. **Validation warns but doesn't block.** User-facing validation commands (like lock file checksum mismatches) should warn with actionable fix commands but never prevent the user from continuing. The validation philosophy is "inform, don't obstruct." @@ -702,12 +713,17 @@ plugins/{platform}/ ``` examples/{platform}/ โ”œโ”€โ”€ devbox.d/ -โ”‚ โ””โ”€โ”€ {platform}/ +โ”‚ โ””โ”€โ”€ / # Directory name depends on include method โ”‚ โ””โ”€โ”€ devices/ # User device definitions โ”‚ โ”œโ”€โ”€ *.json โ”‚ โ””โ”€โ”€ devices.lock -โ”œโ”€โ”€ devbox.json # Includes plugin +โ”œโ”€โ”€ devbox.json # Includes plugin via path: (local development) โ””โ”€โ”€ README.md # Usage guide +# Note: Examples use local path includes (path:../../plugins/{platform}/plugin.json) +# so PR checks test against the current plugin source. User-facing docs show the +# GitHub URL format (github:segment-integrations/devbox-plugins?dir=plugins/{platform}). +# is "{platform}" for local/path includes, but for GitHub includes it +# uses the full dotted path (e.g., "segment-integrations.devbox-plugins.android"). ``` **Test Directory Layout:** @@ -725,12 +741,12 @@ plugins/tests/ **File naming:** ``` -process-compose-{suite}.yaml +{suite}.yaml Examples: -- process-compose-lint.yaml -- process-compose-unit-tests.yaml -- process-compose-e2e.yaml +- lint.yaml +- unit-tests.yaml +- e2e.yaml ``` **Process naming:** @@ -780,17 +796,15 @@ Examples: ## CI/CD ### Fast PR Checks (`pr-checks.yml`) -- Runs automatically on every PR -- Plugin validation and quick smoke tests -- ~15-30 minutes total -- Tests default devices only +- Runs automatically on every PR and push to main +- Fast tests (lint + unit + integration) plus E2E tests with max devices only +- Tests: Android max, iOS max, React Native (android-max, ios-max, web) ### Full E2E Tests (`e2e-full.yml`) -- Manual trigger or weekly schedule -- Tests min/max platform versions: +- Weekly schedule (Monday 00:00 UTC) or manual trigger +- Tests both min and max platform versions: - Android: API 21 (min) to API 36 (max) - iOS: iOS 15.4 (min) to iOS 26.2 (max) -- ~45-60 minutes per platform - Matrix execution for parallel testing ### Running CI Locally @@ -811,7 +825,7 @@ act -W .github/workflows/pr-checks.yml ## Configuration -Configuration for both Android and iOS plugins is now managed via environment variables defined in `plugin.json`. These env vars are converted to JSON at runtime for internal use. +Configuration for both Android and iOS plugins is managed via environment variables defined in `plugin.json`. These env vars are converted to JSON at runtime for internal use. ### Android Plugin Environment Variables - `ANDROID_DEFAULT_DEVICE` - Default emulator @@ -823,22 +837,19 @@ Configuration for both Android and iOS plugins is now managed via environment va ### iOS Plugin Environment Variables - `IOS_DEFAULT_DEVICE` - Default simulator - `IOS_DEVICES` - Devices to evaluate (comma-separated, empty = all) -- `IOS_APP_PROJECT` - Xcode project path -- `IOS_APP_SCHEME` - Xcode build scheme -- `IOS_APP_ARTIFACT` - App bundle path/glob -- `IOS_DOWNLOAD_RUNTIME` - Auto-download runtimes (0/1) +- `IOS_APP_ARTIFACT` - Path or glob for .app bundle (empty = auto-detect via xcodebuild + search) +- `IOS_DOWNLOAD_RUNTIME` - Auto-download runtimes (0/1, default: 1) ## Important Implementation Notes ### Android SDK via Nix Flake -- The Android SDK is composed via Nix flake at `devbox.d/android/flake.nix` +- The Android SDK is composed via Nix flake at `devbox.d//flake.nix` (directory name depends on include method) - Flake outputs: `android-sdk`, `android-sdk-full`, `android-sdk-preview` - Nix handles flake evaluation caching internally (fast after first evaluation) - Lock file limits which API versions are evaluated (optimization for CI) ### iOS Xcode Discovery -- Multiple strategies: `IOS_DEVELOPER_DIR` env var โ†’ `xcode-select -p` โ†’ `/Applications/Xcode*.app` -- Selects latest Xcode by version number +- Multiple strategies: `IOS_DEVELOPER_DIR` env var โ†’ `/Applications/Xcode*.app` (latest by version) โ†’ `xcode-select -p` โ†’ `/Applications/Xcode.app` fallback - Path cached in `.xcode_dev_dir.cache` (1-hour TTL) ### Validation Philosophy @@ -848,7 +859,9 @@ Configuration for both Android and iOS plugins is now managed via environment va - Examples: lock file checksum mismatches, missing SDK paths ### Script Safety -- All scripts use `set -euo pipefail` (or `set -eu` for POSIX) +- Sourced scripts: `set -e` (no `-u` due to Node.js package compatibility) +- User-facing CLIs: `set -eu` +- Test scripts: `set -euo pipefail` - Functions return 0 on success, non-zero on failure - Validation functions use `|| true` to avoid blocking @@ -859,4 +872,6 @@ For complete command and configuration references, see: - `plugins/ios/REFERENCE.md` - `plugins/react-native/REFERENCE.md` - `plugins/CONVENTIONS.md` +- `wiki/project/ARCHITECTURE.md` +- `wiki/project/ENVIRONMENT-SETUP-STRATEGY.md` - `.github/workflows/README.md` diff --git a/README.md b/README.md index 749ddea..d2f2b79 100644 --- a/README.md +++ b/README.md @@ -5,14 +5,14 @@ Reproducible, project-local development environments for Android, iOS, and React ## Quick Start ```bash +# Install devbox (if you haven't already) +curl -fsSL https://get.jetify.com/devbox | bash + # Initialize devbox in your project devbox init - -# Add the Android plugin include to your devbox.json -# (devbox add only works for packages, not plugins โ€” edit devbox.json manually) ``` -Add the plugin to your `devbox.json`: +Replace the contents of your `devbox.json` with the Android plugin: ```json { @@ -27,18 +27,20 @@ Add the plugin to your `devbox.json`: } ``` +> **Note:** Plugins are added via `include` in `devbox.json`, not with `devbox add`. The plugin provides the Android SDK, emulator, and device management tools. You add your own build tooling (JDK, Gradle) as packages. + ```bash -# Enter development environment +# Enter development environment (downloads SDK on first run) devbox shell -# Start emulator and run app +# List available devices +devbox run android.sh devices list + +# Start the default emulator devbox run start:emu -devbox run start:app ``` -The app's package name is auto-detected from the APK at install time. - -**New to devbox-plugins?** Check out the [Quick Start Guide](wiki/guides/quick-start.md) for detailed setup instructions. +**New to devbox-plugins?** Check out the [Quick Start Guide](wiki/guides/quick-start.md) for a complete walkthrough including how to set up build and deploy scripts. ## Features @@ -94,7 +96,7 @@ Composition layer over Android and iOS with Metro bundler management. ### For Users -- **[Quick Start](wiki/guides/quick-start.md)** - Get started in 5 minutes +- **[Quick Start](wiki/guides/quick-start.md)** - Set up your first project - **[Device Management](wiki/guides/device-management.md)** - Managing emulators and simulators - **[Testing Guide](wiki/guides/testing.md)** - Testing strategies and best practices - **[Troubleshooting](wiki/guides/troubleshooting.md)** - Common issues and solutions @@ -111,34 +113,37 @@ Composition layer over Android and iOS with Metro bundler management. ## Examples -The repository includes example projects demonstrating plugin usage: +The repository includes example projects demonstrating full workflows including build scripts, deploy commands, and E2E test suites: + +- **[examples/android](examples/android/)** - Minimal Android app with Gradle build +- **[examples/ios](examples/ios/)** - Swift package with xcodebuild +- **[examples/react-native](examples/react-native/)** - React Native app with Android, iOS, and Web targets -- **[examples/android](examples/android/)** - Minimal Android app -- **[examples/ios](examples/ios/)** - Swift package example -- **[examples/react-native](examples/react-native/)** - React Native app with Android, iOS, and Web +These examples show how to wire up your own build and deploy scripts on top of the plugin-provided device and emulator management. -Each example includes device definitions, test suites, and build scripts. +## Plugin-Provided Commands -## Common Commands +The plugins provide device management, emulator/simulator control, and diagnostics. Build and deploy commands are project-specific โ€” you define them in your own `devbox.json` (see the [Quick Start Guide](wiki/guides/quick-start.md) or the [examples](examples/) for patterns). ```bash -# Android -devbox run start:emu # Start Android emulator -devbox run start:app # Build, install, and launch app -devbox run stop:emu # Stop emulator +# Android emulator +devbox run start:emu [device] # Start Android emulator +devbox run stop:emu # Stop emulator +devbox run reset:emu # Reset emulator state -# iOS -devbox run start:sim # Start iOS simulator -devbox run start:ios # Build and launch app -devbox run stop:sim # Stop simulator +# iOS simulator +devbox run start:sim [device] # Start iOS simulator +devbox run stop:sim # Stop simulator # Device management devbox run android.sh devices list +devbox run android.sh devices create mydevice --api 30 --device pixel devbox run ios.sh devices list +devbox run ios.sh devices create mydevice --runtime 18.0 -# Testing -devbox run test:fast # Quick tests (~2-5 min) -devbox run test:e2e # Full E2E tests (~15-30 min) +# Diagnostics +devbox run doctor # Check environment health +devbox run verify:setup # Quick verification ``` ## Requirements @@ -147,10 +152,6 @@ devbox run test:e2e # Full E2E tests (~15-30 min) - **macOS** - Required for iOS plugin (Xcode required) - **Linux** - Supported for Android and React Native (Android only) -## License - -MIT - ## Support - **Questions?** Check [Troubleshooting](wiki/guides/troubleshooting.md) diff --git a/devbox.json b/devbox.json index 84a64dc..385d8ce 100644 --- a/devbox.json +++ b/devbox.json @@ -33,7 +33,7 @@ ], "test:unit": [ "echo 'Running orchestrated unit tests...'", - "process-compose -f tests/process-compose-unit-tests.yaml --tui=${TEST_TUI:-false}" + "process-compose -f tests/unit-tests.yaml --tui=${TEST_TUI:-false}" ], "test:e2e:android": [ "echo 'Running Android example E2E test...'", @@ -49,7 +49,7 @@ ], "test:e2e": [ "echo 'Running E2E tests (orchestrated: android+ios parallel, then rn)...'", - "process-compose -f tests/process-compose-e2e.yaml --no-server --tui=${TEST_TUI:-false}" + "process-compose -f tests/e2e.yaml --no-server --tui=${TEST_TUI:-false}" ], "test": [ "echo '========================================'", @@ -107,6 +107,9 @@ "test:plugin:android:apk-detection": [ "(cd examples/android && devbox run bash ../../plugins/tests/android/test-apk-detection.sh)" ], + "test:plugin:android:apk-resolution": [ + "bash plugins/tests/android/test-apk-resolution.sh" + ], "test:plugin:android:emulator-detection": [ "(cd examples/android && devbox run bash ../../plugins/tests/android/test-emulator-detection.sh)" ], @@ -116,7 +119,8 @@ "test:plugin:android": [ "devbox run test:plugin:android:lib", "devbox run test:plugin:android:devices", - "devbox run test:plugin:android:apk-detection" + "devbox run test:plugin:android:apk-detection", + "devbox run test:plugin:android:apk-resolution" ], "test:plugin:android:all": [ "echo 'Running all Android plugin tests (including emulator tests)...'", @@ -131,9 +135,13 @@ "test:plugin:ios:devices": [ "sh plugins/tests/ios/test-devices.sh" ], + "test:plugin:ios:app-resolution": [ + "bash plugins/tests/ios/test-app-resolution.sh" + ], "test:plugin:ios": [ "devbox run test:plugin:ios:lib", - "devbox run test:plugin:ios:devices" + "devbox run test:plugin:ios:devices", + "devbox run test:plugin:ios:app-resolution" ], "test:plugin:devbox-mcp": [ "echo 'Running devbox-mcp plugin tests...'", @@ -187,10 +195,9 @@ "bash scripts/bump.sh \"${@}\"" ], "test:fast": [ - "echo 'Running fast tests (lint + unit + integration)...'", + "echo 'Running fast tests (lint + unit + integration in parallel)...'", "devbox run lint", - "devbox run test:plugin:unit", - "devbox run test:integration", + "devbox run test:unit", "bash tests/test-summary.sh" ] } diff --git a/examples/android/devbox.d/segment-integrations.devbox-plugins.android/devices/devices.lock b/examples/android/devbox.d/android/devices/devices.lock similarity index 84% rename from examples/android/devbox.d/segment-integrations.devbox-plugins.android/devices/devices.lock rename to examples/android/devbox.d/android/devices/devices.lock index ff19d71..f2c920f 100644 --- a/examples/android/devbox.d/segment-integrations.devbox-plugins.android/devices/devices.lock +++ b/examples/android/devbox.d/android/devices/devices.lock @@ -13,6 +13,5 @@ "tag": "google_apis" } ], - "checksum": "8df4d3393b61fbbb08e45cf8762f95c521316938e514527916e4fce88a849d57", - "generated_at": "2026-02-18T20:02:50Z" + "checksum": "8df4d3393b61fbbb08e45cf8762f95c521316938e514527916e4fce88a849d57" } diff --git a/examples/android/devbox.d/segment-integrations.devbox-plugins.android/devices/max.json b/examples/android/devbox.d/android/devices/max.json similarity index 100% rename from examples/android/devbox.d/segment-integrations.devbox-plugins.android/devices/max.json rename to examples/android/devbox.d/android/devices/max.json diff --git a/examples/android/devbox.d/segment-integrations.devbox-plugins.android/devices/min.json b/examples/android/devbox.d/android/devices/min.json similarity index 100% rename from examples/android/devbox.d/segment-integrations.devbox-plugins.android/devices/min.json rename to examples/android/devbox.d/android/devices/min.json diff --git a/examples/android/devbox.json b/examples/android/devbox.json index ab20fd2..3ee3a9b 100644 --- a/examples/android/devbox.json +++ b/examples/android/devbox.json @@ -1,5 +1,5 @@ { - "include": ["github:segment-integrations/devbox-plugins/main?dir=plugins/android"], + "include": ["path:../../plugins/android/plugin.json"], "packages": { "jdk17": "latest", "gradle": "latest" @@ -11,14 +11,10 @@ "shell": { "scripts": { "build": [ - "echo 'Building Android app...'", - "gradle assembleDebug --info" + "gradle assembleDebug" ], - "start:emu": [ - "android.sh emulator start ${1:-${ANDROID_DEFAULT_DEVICE:-max}}" - ], - "stop:emu": [ - "android.sh emulator stop" + "build:release": [ + "gradle assembleRelease" ], "start:app": [ "android.sh run ${1:-${ANDROID_DEFAULT_DEVICE:-max}}" @@ -28,9 +24,6 @@ ], "test:e2e": [ "process-compose -f tests/test-suite.yaml --no-server --tui=${TEST_TUI:-false}" - ], - "test:e2e:debug": [ - "ANDROID_DEBUG_SETUP=1 process-compose -f tests/test-suite.yaml --no-server --tui=false --log-level=debug" ] } } diff --git a/examples/android/tests/test-emulator-only.yaml b/examples/android/tests/test-emulator-only.yaml index 182cdd8..72500d4 100644 --- a/examples/android/tests/test-emulator-only.yaml +++ b/examples/android/tests/test-emulator-only.yaml @@ -8,6 +8,7 @@ processes: environment: - "ANDROID_EMULATOR_FOREGROUND=1" command: | + set -e echo "[EMULATOR-ONLY] Starting emulator..." . ${ANDROID_RUNTIME_DIR}/scripts/init/setup.sh @@ -25,7 +26,7 @@ processes: serial=$(cat ${ANDROID_RUNTIME_DIR}/emulator-serial.txt 2>/dev/null || echo "") [ -n "$serial" ] && adb -s $serial shell getprop sys.boot_completed 2>/dev/null | grep -q "1" initial_delay_seconds: 10 - period_seconds: 10 - timeout_seconds: 180 + period_seconds: 5 + timeout_seconds: 10 success_threshold: 1 - failure_threshold: 10 + failure_threshold: 24 diff --git a/examples/android/tests/test-suite.yaml b/examples/android/tests/test-suite.yaml index 30974d7..a61b0fe 100644 --- a/examples/android/tests/test-suite.yaml +++ b/examples/android/tests/test-suite.yaml @@ -1,141 +1,88 @@ version: "0.5" environment: - - "TEST_TIMEOUT=300" - - "BOOT_TIMEOUT=90" + - "SUITE_NAME=android-e2e" - "TEST_TUI=${TEST_TUI:-false}" - "ANDROID_DEBUG_SETUP=${ANDROID_DEBUG_SETUP:-}" log_location: "reports/android-e2e-logs" log_level: info +is_strict: true processes: - # Phase 1: Build - runs first (no dependency) + # Phase 1: Build app (runs concurrently with emulator startup) build-app: command: | - echo '[BUILD-APP] Starting build process (PID=$$)...' - # Source Android environment to set ANDROID_SDK_ROOT - . ${ANDROID_RUNTIME_DIR}/scripts/init/setup.sh - echo '๐Ÿ“ฆ Building Android app...' - gradle assembleDebug --info - echo 'โœ“ Build complete' + set -e + _step="build-app" + _step_dir="reports/steps" + mkdir -p "$_step_dir" + trap 'printf "fail\nExited with error (exit code $?)\n" > "$_step_dir/$_step.status"' ERR + + gradle assembleDebug + + echo "pass" > "$_step_dir/$_step.status" availability: restart: "no" # Phase 2a: Sync AVDs - ensure all AVDs match device definitions sync-avds: - command: "echo '[SYNC-AVDS] Starting (PID=$$)...' && echo '[SYNC-AVDS] About to source setup.sh...' && . ${ANDROID_RUNTIME_DIR}/scripts/init/setup.sh && echo '[SYNC-AVDS] setup.sh sourced' && echo '๐Ÿ”„ Syncing AVDs with device definitions...' && android.sh devices sync && echo 'โœ“ Sync complete'" + command: "android.sh devices sync" availability: restart: "no" - # Phase 2b: Start emulator (depends on sync completing) - # Reuses existing emulator if already running - # Max boot time: 5 minutes, 1 retry allowed, total timeout: 10 minutes + # Phase 2b: Start emulator (runs concurrently with build) android-emulator: depends_on: sync-avds: condition: process_completed_successfully environment: - - "ANDROID_EMULATOR_FOREGROUND=1" # Run emulator in foreground for process-compose monitoring + - "ANDROID_EMULATOR_FOREGROUND=1" command: | - echo "[ANDROID-EMULATOR] Starting emulator in foreground mode..." - # Source Android environment - . ${ANDROID_RUNTIME_DIR}/scripts/init/setup.sh - - # Determine which device to start + set -e + echo "[ANDROID-EMULATOR] Starting emulator..." device="${ANDROID_DEFAULT_DEVICE:-max}" - # In pure mode, always start fresh - if [ "${IN_NIX_SHELL:-}" = "pure" ]; then - echo "๐Ÿš€ Starting fresh Android emulator (pure mode): $device" - export ANDROID_EMULATOR_PURE=1 - - # Write expected serial for readiness probe (emulator always starts on 5554 first) - echo "emulator-5554" > ${ANDROID_RUNTIME_DIR}/emulator-serial.txt - - # Start emulator in foreground - this blocks and runs the emulator - # Process-compose will monitor it and use readiness probe to check when ready - android.sh emulator start "$device" + if [ "${DEVBOX_PURE_SHELL:-}" = "1" ]; then + echo "Starting fresh Android emulator (pure mode): $device" + android.sh emulator start --pure "$device" else - echo "๐Ÿš€ Verifying Android emulator is running: $device" - - # Write expected serial for readiness probe - serial="emulator-5554" - echo "$serial" > ${ANDROID_RUNTIME_DIR}/emulator-serial.txt - - # Check if emulator is already running - if adb devices | grep -q "$serial"; then - echo "โœ“ Emulator already running: $serial" - else - echo "Starting emulator in background: $device" - # Start emulator in background (no foreground mode for reuse) - unset ANDROID_EMULATOR_FOREGROUND - android.sh emulator start "$device" & - echo "โœ“ Emulator starting (PID: $!)" - fi - - # Wait for emulator to be fully booted - echo "Waiting for emulator to boot..." - max_attempts=60 - attempt=0 - while [ $attempt -lt $max_attempts ]; do - if adb -s $serial shell getprop sys.boot_completed 2>/dev/null | grep -q "1"; then - echo "โœ“ Emulator ready for testing" - break - fi - attempt=$((attempt + 1)) - sleep 2 - done - - if [ $attempt -ge $max_attempts ]; then - echo "ERROR: Emulator did not boot within timeout" - exit 1 - fi - - # Let process complete - emulator stays running in background - echo "โœ“ Emulator will remain running after tests complete" - exit 0 + echo "Starting Android emulator: $device" + unset ANDROID_EMULATOR_FOREGROUND + android.sh emulator start "$device" fi + # Keep process alive for readiness probe (process-compose services pattern) + tail -f /dev/null availability: restart: "no" readiness_probe: exec: - command: | - serial=$(cat ${ANDROID_RUNTIME_DIR}/emulator-serial.txt 2>/dev/null || echo "") - [ -n "$serial" ] && adb -s $serial shell getprop sys.boot_completed 2>/dev/null | grep -q "1" + command: "android.sh emulator ready" initial_delay_seconds: 10 - period_seconds: 10 - timeout_seconds: 180 + period_seconds: 5 + timeout_seconds: 10 success_threshold: 1 - failure_threshold: 12 + failure_threshold: 60 - # Phase 3: Run app - depends on both build and emulator being ready - run-app: + # Phase 3: Deploy app - depends on both build and emulator being ready + deploy-app: command: | - # Source Android environment - . ${ANDROID_RUNTIME_DIR}/scripts/init/setup.sh + set -e + _step="deploy-app" + _step_dir="reports/steps" + mkdir -p "$_step_dir" + trap 'printf "fail\nExited with error (exit code $?)\n" > "$_step_dir/$_step.status"' ERR - serial=$(cat ${ANDROID_RUNTIME_DIR}/emulator-serial.txt 2>/dev/null || echo emulator-5554) - export ANDROID_EMULATOR_SERIAL="$serial" - - echo "๐Ÿ“ฒ Running app on emulator: $serial" - # Use already-built APK from build-app phase - android.sh run ${ANDROID_APP_APK:-app/build/outputs/apk/debug/app-debug.apk} + android.sh deploy echo "" - # Resolve app ID: env var > auto-detected from APK > fail - app_id="${ANDROID_APP_ID:-$(cat ${ANDROID_RUNTIME_DIR}/app-id.txt 2>/dev/null || true)}" - if [ -z "$app_id" ]; then - echo "ERROR: Cannot determine app ID. Set ANDROID_APP_ID or run deploy first." >&2 - exit 1 - fi - - echo "Waiting for app to start ($app_id)..." + echo "Waiting for app to start..." max_attempts=10 attempt=0 while [ $attempt -lt $max_attempts ]; do - if adb -s $serial shell pidof "$app_id" >/dev/null 2>&1; then - echo "โœ“ App process running (PID: $(adb -s $serial shell pidof "$app_id"))" + if android.sh app status; then + echo "App is running" + echo "pass" > "$_step_dir/$_step.status" exit 0 fi attempt=$((attempt + 1)) @@ -143,55 +90,48 @@ processes: sleep 2 done - echo "ERROR: App did not start within timeout" >&2 - echo "Installed packages:" >&2 - adb -s $serial shell pm list packages | grep -i devbox || true - echo "Running processes:" >&2 - adb -s $serial shell ps | grep -i devbox || true + printf 'fail\nApp did not start within timeout\n' > "$_step_dir/$_step.status" exit 1 depends_on: build-app: condition: process_completed_successfully android-emulator: - condition: process_completed_successfully + condition: process_healthy availability: restart: "no" # Cleanup - stop everything in pure mode, keep running in dev mode cleanup-app: command: | - # Source Android environment - . ${ANDROID_RUNTIME_DIR}/scripts/init/setup.sh - - serial=$(cat ${ANDROID_RUNTIME_DIR}/emulator-serial.txt 2>/dev/null || echo emulator-5554) - - # In pure mode (CI), stop app and emulator for reproducibility - if [ "${IN_NIX_SHELL:-}" = "pure" ]; then - echo "๐Ÿงน Cleaning up (pure mode): stopping app and emulator..." - app_id="${ANDROID_APP_ID:-$(cat ${ANDROID_RUNTIME_DIR}/app-id.txt 2>/dev/null || true)}" - [ -n "$app_id" ] && adb -s $serial shell am force-stop "$app_id" 2>/dev/null || true + if [ "${DEVBOX_PURE_SHELL:-}" = "1" ]; then + echo "Cleaning up (pure mode)..." + android.sh app stop || true android.sh emulator stop || true - echo "โœ“ App stopped, emulator stopped" + echo "App stopped, emulator stopped" else - # In dev mode, keep app and emulator running for interaction - echo "โœ“ Test complete - app and emulator still running" + echo "Test complete - app and emulator still running" fi depends_on: - run-app: - condition: process_completed_successfully + deploy-app: + condition: process_completed availability: restart: "no" - # Summary - displays results and optionally stays open + # Summary - reports per-step pass/fail summary: command: | - bash tests/test-summary.sh 'Android E2E Test Suite' 'reports/android-e2e-logs' - exit 0 + set -e + export REPO_ROOT="$(cd ../.. && pwd)" + . "$REPO_ROOT/plugins/tests/test-framework.sh" + log_test "Android E2E Test Suite" + e2e_report_steps || true + test_summary "android-e2e" depends_on: cleanup-app: condition: process_completed availability: restart: "no" + exit_on_end: true shutdown: signal: 15 timeout_seconds: 1 diff --git a/examples/android/tests/test-summary.sh b/examples/android/tests/test-summary.sh deleted file mode 100755 index cdf07a9..0000000 --- a/examples/android/tests/test-summary.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash -# Shared test summary script for process-compose test suites -# Usage: test-summary.sh "Test Suite Name" "path/to/logs" - -set -euo pipefail - -SUITE_NAME="${1:-Test Suite}" -LOG_PATH="${2:-reports/logs}" - -echo "" -echo "====================================" -echo "${SUITE_NAME} Summary" -echo "====================================" -echo "" -echo "Test Logs:" -echo " ${LOG_PATH}" -echo "" -echo "All tests completed!" -echo "====================================" - -# If TUI is enabled, sleep to keep results visible -if [ "${TEST_TUI:-false}" = "true" ] || [ "${TEST_TUI:-false}" = "1" ]; then - echo "" - echo "TUI mode: Waiting 30 seconds before exit (Ctrl+C to exit now)..." - sleep 30 - echo "Exiting..." -fi - -exit 0 diff --git a/examples/ios/devbox.d/segment-integrations.devbox-plugins.ios/devices/devices.lock b/examples/ios/devbox.d/ios/devices/devices.lock similarity index 78% rename from examples/ios/devbox.d/segment-integrations.devbox-plugins.ios/devices/devices.lock rename to examples/ios/devbox.d/ios/devices/devices.lock index 0f5a917..ed4e6c1 100644 --- a/examples/ios/devbox.d/segment-integrations.devbox-plugins.ios/devices/devices.lock +++ b/examples/ios/devbox.d/ios/devices/devices.lock @@ -9,6 +9,5 @@ "runtime": "15.4" } ], - "checksum": "4d5276f203d7ad62860bfc067f76194df53be449d4aa8a3b2d069855ec1f3232", - "generated_at": "2026-02-18T20:03:01Z" + "checksum": "4d5276f203d7ad62860bfc067f76194df53be449d4aa8a3b2d069855ec1f3232" } diff --git a/examples/ios/devbox.d/segment-integrations.devbox-plugins.ios/devices/max.json b/examples/ios/devbox.d/ios/devices/max.json similarity index 100% rename from examples/ios/devbox.d/segment-integrations.devbox-plugins.ios/devices/max.json rename to examples/ios/devbox.d/ios/devices/max.json diff --git a/examples/ios/devbox.d/segment-integrations.devbox-plugins.ios/devices/min.json b/examples/ios/devbox.d/ios/devices/min.json similarity index 100% rename from examples/ios/devbox.d/segment-integrations.devbox-plugins.ios/devices/min.json rename to examples/ios/devbox.d/ios/devices/min.json diff --git a/examples/ios/devbox.json b/examples/ios/devbox.json index 601dd3a..c6b1902 100644 --- a/examples/ios/devbox.json +++ b/examples/ios/devbox.json @@ -1,37 +1,21 @@ { - "include": ["github:segment-integrations/devbox-plugins/main?dir=plugins/ios"], + "include": ["path:../../plugins/ios/plugin.json"], "packages": { "process-compose": "latest" }, - "env": { - "IOS_APP_PROJECT": "ios.xcodeproj", - "IOS_APP_SCHEME": "ios", - "IOS_APP_BUNDLE_ID": "com.example.ios", - "IOS_APP_ARTIFACT": ".devbox/virtenv/ios/DerivedData/Build/Products/Debug-iphonesimulator/ios.app" - }, "shell": { "scripts": { "build": [ - "echo 'Building iOS app...'", - "env -u LD -u LDFLAGS -u NIX_LDFLAGS -u NIX_CFLAGS_COMPILE -u NIX_CFLAGS_LINK xcodebuild -project ${IOS_APP_PROJECT} -scheme ${IOS_APP_SCHEME} -configuration Debug -destination 'generic/platform=iOS Simulator' -derivedDataPath .devbox/virtenv/ios/DerivedData build" - ], - "start:sim": [ - "ios.sh simulator start ${1:-${IOS_DEFAULT_DEVICE:-max}}" + "ios.sh xcodebuild -project ios.xcodeproj -scheme ios -configuration Debug -destination 'generic/platform=iOS Simulator' build" ], - "stop:sim": [ - "ios.sh simulator stop" + "build:release": [ + "ios.sh xcodebuild -project ios.xcodeproj -scheme ios -configuration Release build" ], "start:app": [ - "ios.sh simulator start ${1:-${IOS_DEFAULT_DEVICE:-max}}", - "xcrun simctl install booted ${IOS_APP_ARTIFACT}", - "xcrun simctl launch booted ${IOS_APP_BUNDLE_ID}" - ], - "deploy": [ - "devbox run build", - "devbox run start:app" + "ios.sh run ${1:-}" ], "test": [ - "env -u LD -u LDFLAGS -u NIX_LDFLAGS -u NIX_CFLAGS_COMPILE -u NIX_CFLAGS_LINK xcodebuild -project ${IOS_APP_PROJECT} -scheme ${IOS_APP_SCHEME} -configuration Debug -destination 'generic/platform=iOS Simulator' -derivedDataPath .devbox/virtenv/ios/DerivedData test" + "ios.sh xcodebuild -project ios.xcodeproj -scheme ios -destination 'platform=iOS Simulator,name=iPhone 17' test" ], "test:e2e": [ "process-compose -f tests/test-suite.yaml --no-server --tui=${TEST_TUI:-false}" diff --git a/examples/ios/tests/README.md b/examples/ios/tests/README.md index e7fb845..4fb35c0 100644 --- a/examples/ios/tests/README.md +++ b/examples/ios/tests/README.md @@ -50,11 +50,9 @@ devbox run test:e2e ```bash devbox run --pure test:e2e -# or in CI: -IN_NIX_SHELL=pure devbox run test:e2e ``` -The test suite automatically detects `IN_NIX_SHELL=pure` and adjusts cleanup behavior. +The test suite automatically detects `DEVBOX_PURE_SHELL=1` (set by `--pure`) and adjusts cleanup behavior. ## Copy to Your Project @@ -217,7 +215,7 @@ deploy-app (phase 4) โ”€โ”€โ”€โ”€โ”€โ”˜ depends on simulator ready โ†“ verify-app-running (phase 5) โ†“ -cleanup (phase 6) - conditional based on IN_NIX_SHELL +cleanup (phase 6) - conditional based on DEVBOX_PURE_SHELL โ†“ summary (phase 7) ``` diff --git a/examples/ios/tests/test-simulator-only.yaml b/examples/ios/tests/test-simulator-only.yaml index 1ef7749..976759b 100644 --- a/examples/ios/tests/test-simulator-only.yaml +++ b/examples/ios/tests/test-simulator-only.yaml @@ -6,6 +6,7 @@ log_level: info processes: ios-simulator: command: | + set -e echo "[SIMULATOR-ONLY] Starting simulator..." # Ensure reports/logs directory exists @@ -47,9 +48,9 @@ processes: [ "$state" = "Booted" ] initial_delay_seconds: 5 period_seconds: 5 - timeout_seconds: 120 + timeout_seconds: 10 success_threshold: 1 - failure_threshold: 20 + failure_threshold: 24 cleanup: depends_on: diff --git a/examples/ios/tests/test-suite.yaml b/examples/ios/tests/test-suite.yaml index 62bda28..2c81a0e 100644 --- a/examples/ios/tests/test-suite.yaml +++ b/examples/ios/tests/test-suite.yaml @@ -2,65 +2,50 @@ version: "0.5" environment: - "IOS_DEVICE=${IOS_DEFAULT_DEVICE:-max}" - - "TEST_TIMEOUT=300" - - "BOOT_TIMEOUT=120" + - "SUITE_NAME=ios-e2e" log_location: "reports/ios-e2e-logs" log_level: info +is_strict: true processes: - # Phase 1: Build app - runs first + # Phase 1: Build app (runs concurrently with simulator startup) build-app: - command: "devbox run --pure build" + command: | + set -e + _step="build-app" + _step_dir="reports/steps" + mkdir -p "$_step_dir" + trap 'printf "fail\nExited with error (exit code $?)\n" > "$_step_dir/$_step.status"' ERR + + DERIVED_DATA="${DEVBOX_PROJECT_ROOT:-.}/.devbox/virtenv/ios/DerivedData" + mkdir -p "$DERIVED_DATA" + xcodebuild -project ios.xcodeproj -scheme ios \ + -configuration Debug \ + -destination "generic/platform=iOS Simulator" \ + -derivedDataPath "$DERIVED_DATA" \ + build + + echo "pass" > "$_step_dir/$_step.status" availability: restart: "no" # Phase 2: Sync simulators - ensure all simulators match device definitions sync-simulators: - command: "devbox run ios.sh devices sync" + command: "ios.sh devices sync" availability: restart: "no" - # Phase 3: Start simulator - depends on sync completing + # Phase 3: Start simulator (runs concurrently with build) ios-simulator: command: | - # Ensure reports/logs directory exists - mkdir -p reports/logs - - # In pure mode (IN_NIX_SHELL=pure), start fresh simulator with clean state - # Otherwise, reuse existing simulator if available - if [ "${IN_NIX_SHELL:-}" = "pure" ]; then - echo "๐Ÿš€ Starting fresh iOS simulator with clean state (${IOS_DEVICE})..." - output=$(ios.sh simulator start --pure ${IOS_DEVICE} 2>&1) - echo "$output" - - # Extract and save test simulator UDID for cleanup - test_udid=$(echo "$output" | grep -oE '[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}' | tail -1) - if [ -n "$test_udid" ]; then - echo "$test_udid" > reports/logs/ios-test-sim-udid.txt - echo "โœ“ Test simulator UDID saved: $test_udid" - fi + set -e + if [ "${DEVBOX_PURE_SHELL:-}" = "1" ]; then + ios.sh simulator start --pure ${IOS_DEVICE} else - echo "๐Ÿš€ Starting iOS simulator (${IOS_DEVICE})..." ios.sh simulator start ${IOS_DEVICE} - - # Wait for simulator to be fully booted - echo "Waiting for simulator to boot..." - DEVICE_UDID=$(xcrun simctl list devices | grep "Booted" | grep -oE '[0-9A-F-]{36}' | head -1 || echo "") - if [ -z "$DEVICE_UDID" ]; then - echo "ERROR: No booted simulator found" - exit 1 - fi - xcrun simctl bootstatus "$DEVICE_UDID" >/dev/null 2>&1 || true - - echo "โœ“ Simulator ready for testing" - echo "โœ“ Simulator will remain running after tests complete" - - # In reuse mode, let process complete - simulator stays running in background - exit 0 fi - - # Pure mode: Keep process alive so cleanup can shut down test simulator + # Keep process alive for readiness probe (process-compose services pattern) tail -f /dev/null depends_on: sync-simulators: @@ -69,50 +54,32 @@ processes: restart: "no" readiness_probe: exec: - command: | - # Find booted simulator (either test simulator or regular) - if [ "${IN_NIX_SHELL:-}" = "pure" ]; then - # In pure mode, look for test simulator - DEVICE_UDID=$(xcrun simctl list devices | grep "Test" | grep "Booted" | grep -oE '[0-9A-F-]{36}' | head -1 || echo "") - else - # In normal mode, look for regular simulator by device selection - DEVICE_UDID=$(xcrun simctl list devices | grep "Booted" | grep -oE '[0-9A-F-]{36}' | head -1 || echo "") - fi - [ -n "$DEVICE_UDID" ] && xcrun simctl bootstatus "$DEVICE_UDID" >/dev/null 2>&1 - initial_delay_seconds: 10 + command: "ios.sh simulator ready" + initial_delay_seconds: 5 period_seconds: 5 - timeout_seconds: 120 + timeout_seconds: 10 success_threshold: 1 - failure_threshold: 3 + failure_threshold: 36 # Phase 4: Deploy app - depends on both build and simulator deploy-app: command: | - # Find the booted simulator (test or regular) - if [ "${IN_NIX_SHELL:-}" = "pure" ]; then - DEVICE_UDID=$(xcrun simctl list devices | grep "Test" | grep "Booted" | grep -oE '[0-9A-F-]{36}' | head -1) - else - DEVICE_UDID=$(xcrun simctl list devices | grep "Booted" | grep -oE '[0-9A-F-]{36}' | head -1) - fi + set -e + _step="deploy-app" + _step_dir="reports/steps" + mkdir -p "$_step_dir" + trap 'printf "fail\nExited with error (exit code $?)\n" > "$_step_dir/$_step.status"' ERR - if [ -z "$DEVICE_UDID" ]; then - echo "ERROR: No booted simulator found" - exit 1 - fi - - echo "๐Ÿ“ฒ Installing app on simulator: $DEVICE_UDID" - xcrun simctl install "$DEVICE_UDID" "${IOS_APP_ARTIFACT}" - - echo "๐Ÿš€ Launching app: ${IOS_APP_BUNDLE_ID}" - xcrun simctl launch "$DEVICE_UDID" "${IOS_APP_BUNDLE_ID}" + ios.sh deploy echo "" echo "Waiting for app to start..." max_attempts=10 attempt=0 while [ $attempt -lt $max_attempts ]; do - if xcrun simctl spawn "$DEVICE_UDID" launchctl list | grep -q "${IOS_APP_BUNDLE_ID}"; then - echo "โœ“ App is running" + if ios.sh app status; then + echo "App is running" + echo "pass" > "$_step_dir/$_step.status" exit 0 fi attempt=$((attempt + 1)) @@ -120,34 +87,29 @@ processes: sleep 2 done - echo "ERROR: App did not start within timeout" >&2 - echo "Checking app status:" >&2 - xcrun simctl get_app_container "$DEVICE_UDID" "${IOS_APP_BUNDLE_ID}" 2>&1 || true + printf 'fail\nApp did not start within timeout\n' > "$_step_dir/$_step.status" exit 1 depends_on: build-app: condition: process_completed_successfully ios-simulator: - condition: process_completed_successfully + condition: process_healthy availability: restart: "no" # Phase 5: Verify app is running (liveness check) verify-app-running: command: | - # Find the booted simulator - if [ "${IN_NIX_SHELL:-}" = "pure" ]; then - DEVICE_UDID=$(xcrun simctl list devices | grep "Test" | grep "Booted" | grep -oE '[0-9A-F-]{36}' | head -1) - else - DEVICE_UDID=$(xcrun simctl list devices | grep "Booted" | grep -oE '[0-9A-F-]{36}' | head -1) - fi + _step="verify-app" + _step_dir="reports/steps" + mkdir -p "$_step_dir" - echo "Verifying app is running on simulator: $DEVICE_UDID" - if xcrun simctl spawn "$DEVICE_UDID" launchctl list | grep -q "${IOS_APP_BUNDLE_ID}"; then - echo "โœ“ App is running: ${IOS_APP_BUNDLE_ID}" + if ios.sh app status; then + echo "App is running" + echo "pass" > "$_step_dir/$_step.status" exit 0 else - echo "ERROR: App not found in running processes" + printf 'fail\nApp not found in running processes\n' > "$_step_dir/$_step.status" exit 1 fi depends_on: @@ -159,36 +121,13 @@ processes: # Cleanup - stop everything in pure mode, keep running in dev mode cleanup: command: | - # In pure mode (CI), stop app and delete test simulator for reproducibility - if [ "${IN_NIX_SHELL:-}" = "pure" ]; then - echo "๐Ÿงน Cleaning up (pure mode): stopping app and deleting test simulator..." - - # Get test simulator UDID - test_sim_udid=$(cat reports/logs/ios-test-sim-udid.txt 2>/dev/null || echo "") - - if [ -z "$test_sim_udid" ]; then - # Fallback: find test simulator by name - test_sim_udid=$(xcrun simctl list devices | grep "Test" | grep -oE '[0-9A-F-]{36}' | head -1 || true) - fi - - if [ -n "$test_sim_udid" ]; then - # Stop the app first - xcrun simctl terminate "$test_sim_udid" "${IOS_APP_BUNDLE_ID}" 2>/dev/null || true - - # Shutdown and delete the simulator - xcrun simctl shutdown "$test_sim_udid" >/dev/null 2>&1 || true - xcrun simctl delete "$test_sim_udid" >/dev/null 2>&1 || true - - # Clean up temp file - rm -f reports/logs/ios-test-sim-udid.txt - - echo "โœ“ App stopped, test simulator deleted" - else - echo "โœ“ No test simulator to clean up" - fi + if [ "${DEVBOX_PURE_SHELL:-}" = "1" ]; then + echo "Cleaning up (pure mode)..." + ios.sh app stop || true + ios.sh simulator stop + echo "App stopped, test simulator deleted" else - # In dev mode, keep app and simulator running for interaction - echo "โœ“ Test complete - app and simulator still running" + echo "Test complete - app and simulator still running" fi depends_on: verify-app-running: @@ -196,16 +135,21 @@ processes: availability: restart: "no" - # Summary - displays results and optionally stays open + # Summary - reports per-step pass/fail summary: command: | - bash tests/test-summary.sh 'iOS E2E Test Suite' 'reports/ios-e2e-logs' - exit 0 + set -e + export REPO_ROOT="$(cd ../.. && pwd)" + . "$REPO_ROOT/plugins/tests/test-framework.sh" + log_test "iOS E2E Test Suite" + e2e_report_steps || true + test_summary "ios-e2e" depends_on: cleanup: condition: process_completed availability: restart: "no" + exit_on_end: true shutdown: signal: 15 timeout_seconds: 1 diff --git a/examples/ios/tests/test-summary.sh b/examples/ios/tests/test-summary.sh deleted file mode 100755 index cdf07a9..0000000 --- a/examples/ios/tests/test-summary.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash -# Shared test summary script for process-compose test suites -# Usage: test-summary.sh "Test Suite Name" "path/to/logs" - -set -euo pipefail - -SUITE_NAME="${1:-Test Suite}" -LOG_PATH="${2:-reports/logs}" - -echo "" -echo "====================================" -echo "${SUITE_NAME} Summary" -echo "====================================" -echo "" -echo "Test Logs:" -echo " ${LOG_PATH}" -echo "" -echo "All tests completed!" -echo "====================================" - -# If TUI is enabled, sleep to keep results visible -if [ "${TEST_TUI:-false}" = "true" ] || [ "${TEST_TUI:-false}" = "1" ]; then - echo "" - echo "TUI mode: Waiting 30 seconds before exit (Ctrl+C to exit now)..." - sleep 30 - echo "Exiting..." -fi - -exit 0 diff --git a/examples/react-native/devbox.d/segment-integrations.devbox-plugins.android/devices/devices.lock b/examples/react-native/devbox.d/android/devices/devices.lock similarity index 84% rename from examples/react-native/devbox.d/segment-integrations.devbox-plugins.android/devices/devices.lock rename to examples/react-native/devbox.d/android/devices/devices.lock index b34baa7..f2c920f 100644 --- a/examples/react-native/devbox.d/segment-integrations.devbox-plugins.android/devices/devices.lock +++ b/examples/react-native/devbox.d/android/devices/devices.lock @@ -13,6 +13,5 @@ "tag": "google_apis" } ], - "checksum": "8df4d3393b61fbbb08e45cf8762f95c521316938e514527916e4fce88a849d57", - "generated_at": "2026-02-18T20:03:16Z" + "checksum": "8df4d3393b61fbbb08e45cf8762f95c521316938e514527916e4fce88a849d57" } diff --git a/examples/react-native/devbox.d/segment-integrations.devbox-plugins.android/devices/max.json b/examples/react-native/devbox.d/android/devices/max.json similarity index 100% rename from examples/react-native/devbox.d/segment-integrations.devbox-plugins.android/devices/max.json rename to examples/react-native/devbox.d/android/devices/max.json diff --git a/examples/react-native/devbox.d/segment-integrations.devbox-plugins.android/devices/min.json b/examples/react-native/devbox.d/android/devices/min.json similarity index 100% rename from examples/react-native/devbox.d/segment-integrations.devbox-plugins.android/devices/min.json rename to examples/react-native/devbox.d/android/devices/min.json diff --git a/examples/react-native/devbox.d/segment-integrations.devbox-plugins.ios/devices/devices.lock b/examples/react-native/devbox.d/ios/devices/devices.lock similarity index 78% rename from examples/react-native/devbox.d/segment-integrations.devbox-plugins.ios/devices/devices.lock rename to examples/react-native/devbox.d/ios/devices/devices.lock index 0654b04..ed4e6c1 100644 --- a/examples/react-native/devbox.d/segment-integrations.devbox-plugins.ios/devices/devices.lock +++ b/examples/react-native/devbox.d/ios/devices/devices.lock @@ -9,6 +9,5 @@ "runtime": "15.4" } ], - "checksum": "4d5276f203d7ad62860bfc067f76194df53be449d4aa8a3b2d069855ec1f3232", - "generated_at": "2026-02-18T20:03:18Z" + "checksum": "4d5276f203d7ad62860bfc067f76194df53be449d4aa8a3b2d069855ec1f3232" } diff --git a/examples/react-native/devbox.d/segment-integrations.devbox-plugins.ios/devices/max.json b/examples/react-native/devbox.d/ios/devices/max.json similarity index 100% rename from examples/react-native/devbox.d/segment-integrations.devbox-plugins.ios/devices/max.json rename to examples/react-native/devbox.d/ios/devices/max.json diff --git a/examples/react-native/devbox.d/segment-integrations.devbox-plugins.ios/devices/min.json b/examples/react-native/devbox.d/ios/devices/min.json similarity index 100% rename from examples/react-native/devbox.d/segment-integrations.devbox-plugins.ios/devices/min.json rename to examples/react-native/devbox.d/ios/devices/min.json diff --git a/examples/react-native/devbox.json b/examples/react-native/devbox.json index 3cffe6c..7edc0ac 100644 --- a/examples/react-native/devbox.json +++ b/examples/react-native/devbox.json @@ -1,5 +1,5 @@ { - "include": ["github:segment-integrations/devbox-plugins/main?dir=plugins/react-native"], + "include": ["path:../../plugins/react-native/plugin.json"], "packages": [ "nodejs@20", "watchman@latest", @@ -7,14 +7,11 @@ "gradle@latest" ], "env": { - "IOS_APP_PROJECT": "ReactNativeExample.xcodeproj", "IOS_APP_SCHEME": "ReactNativeExample", "IOS_APP_BUNDLE_ID": "org.reactjs.native.example.ReactNativeExample", - "IOS_APP_ARTIFACT": ".devbox/virtenv/ios/DerivedData/Build/Products/Debug-iphonesimulator/ReactNativeExample.app", "ANDROID_APP_ID": "com.reactnativeexample", "ANDROID_APP_APK": "android/app/build/outputs/apk/debug/app-debug.apk", "ANDROID_MAX_API": "35", - "DEVBOX_COREPACK_ENABLED": "", "ANDROID_SDK_REQUIRED": "0" }, "shell": { @@ -27,12 +24,21 @@ ], "build:android": [ "devbox run install", - "cd android && gradle assembleDebug" + "cd android && ./gradlew assembleDebug" + ], + "build:android:release": [ + "devbox run install", + "cd android && ./gradlew assembleRelease" ], "build:ios": [ "devbox run install", "cd ios && pod install --repo-update", - "cd ios && env -u LD -u LDFLAGS -u NIX_LDFLAGS -u NIX_CFLAGS_COMPILE -u NIX_CFLAGS_LINK xcodebuild -workspace ReactNativeExample.xcworkspace -scheme ${IOS_APP_SCHEME} -configuration Debug -destination 'generic/platform=iOS Simulator' -derivedDataPath ${DEVBOX_PROJECT_ROOT}/.devbox/virtenv/ios/DerivedData -quiet build" + "ios.sh xcodebuild -workspace ReactNativeExample.xcworkspace -scheme ReactNativeExample -configuration Debug -destination 'generic/platform=iOS Simulator' build" + ], + "build:ios:release": [ + "devbox run install", + "cd ios && pod install --repo-update", + "ios.sh xcodebuild -workspace ReactNativeExample.xcworkspace -scheme ReactNativeExample -configuration Release build" ], "build:web": [ "devbox run install", @@ -43,12 +49,26 @@ "devbox run build:ios", "devbox run build:web" ], + "build:debug": [ + "devbox run build:android", + "devbox run build:ios" + ], + "build:release": [ + "devbox run build:android:release", + "devbox run build:ios:release" + ], "start:android": [ "process-compose -f tests/dev-android.yaml --tui=${DEVBOX_TUI:-false}" ], + "start:android:release": [ + "ANDROID_BUILD_CONFIG=Release process-compose -f tests/dev-android.yaml --tui=${DEVBOX_TUI:-false}" + ], "start:ios": [ "process-compose -f tests/dev-ios.yaml --tui=${DEVBOX_TUI:-false}" ], + "start:ios:release": [ + "IOS_BUILD_CONFIG=Release process-compose -f tests/dev-ios.yaml --tui=${DEVBOX_TUI:-false}" + ], "start:web": [ "process-compose -f tests/dev-web.yaml --tui=${DEVBOX_TUI:-false}" ], @@ -73,20 +93,17 @@ "test": [ "npm test" ], - "test:e2e": [ - "bash tests/run-android-tests.sh && bash tests/run-ios-tests.sh" - ], "test:e2e:android": [ - "bash tests/run-android-tests.sh" + "process-compose -f tests/test-suite-android-e2e.yaml --no-server --tui=${TEST_TUI:-false}" ], "test:e2e:ios": [ - "bash tests/run-ios-tests.sh" - ], - "test:e2e:all": [ - "bash tests/run-android-tests.sh && bash tests/run-ios-tests.sh" + "process-compose -f tests/test-suite-ios-e2e.yaml --no-server --tui=${TEST_TUI:-false}" ], "test:e2e:web": [ - "bash -c \"ANDROID_SKIP_SETUP=1 IOS_SKIP_SETUP=1 devbox run --pure bash -c 'process-compose -f tests/test-suite-web-e2e.yaml --no-server --tui=${TEST_TUI:-false}'\"" + "process-compose -f tests/test-suite-web-e2e.yaml --no-server --tui=${TEST_TUI:-false}" + ], + "test:e2e:all": [ + "process-compose -f tests/test-suite-all-e2e.yaml --no-server --tui=${TEST_TUI:-false}" ] } } diff --git a/examples/react-native/tests/README.md b/examples/react-native/tests/README.md index 7f387c0..6260379 100644 --- a/examples/react-native/tests/README.md +++ b/examples/react-native/tests/README.md @@ -101,7 +101,7 @@ devbox run test:e2e:android # Clean, isolated test run ## Metro Bundler Management -The React Native plugin provides robust Metro bundler management with isolated state: +The React Native plugin provides Metro bundler management with isolated state: ```bash # Automatic Metro management (via process-compose) @@ -172,30 +172,26 @@ This ensures each test gets its own Metro instance on a unique port with isolate ## Platform-Specific Optimization -The wrapper scripts optimize startup time by skipping the unused platform: +Skip unused platform setup with `-e` flags to speed up test startup: ```bash -# iOS tests only (fast - skips Android SDK) -./tests/run-ios-tests.sh +# iOS tests only (fast - skips Android SDK evaluation) +devbox run --pure -e ANDROID_SKIP_SETUP=1 test:e2e:ios # Android tests only (fast - skips iOS setup) -./tests/run-android-tests.sh +devbox run --pure -e IOS_SKIP_SETUP=1 test:e2e:android + +# Web tests (skip both mobile platforms) +devbox run --pure -e ANDROID_SKIP_SETUP=1 -e IOS_SKIP_SETUP=1 test:e2e:web ``` **Environment variables:** - `ANDROID_SKIP_SETUP=1` - Skip Android SDK Nix flake evaluation - `IOS_SKIP_SETUP=1` - Skip iOS environment setup +- `EMU_HEADLESS=1` - Run Android emulator without display (CI) +- `SIM_HEADLESS=1` - Run iOS simulator without display (CI) -**Important:** When using `--pure` mode, environment variables must be passed with the `-e` flag: -```bash -# Correct way to skip Android SDK in pure mode -devbox run --pure -e ANDROID_SKIP_SETUP=1 test:e2e:ios - -# Incorrect - env var gets reset to default -ANDROID_SKIP_SETUP=1 devbox run --pure test:e2e:ios -``` - -The wrapper scripts (`run-ios-tests.sh` and `run-android-tests.sh`) use the correct `-e` flag syntax automatically. This is particularly useful in CI/CD pipelines where you split platform tests into separate jobs. +**Important:** When using `--pure` mode, environment variables must be passed with the `-e` flag. Variables set in the parent shell are stripped by `--pure`. ## Build Configuration diff --git a/examples/react-native/tests/dev-android.yaml b/examples/react-native/tests/dev-android.yaml index 7947db0..ddbf227 100644 --- a/examples/react-native/tests/dev-android.yaml +++ b/examples/react-native/tests/dev-android.yaml @@ -7,41 +7,28 @@ version: "0.5" # - TUI enabled for interactive development environment: - - "ANDROID_SERIAL=emulator-5554" - - "BOOT_TIMEOUT=180" + - "SUITE_NAME=rn-android-dev" log_location: "reports/react-native-android-dev-logs" log_level: info is_strict: false # Allow graceful exit processes: - # Phase 0: Allocate Metro port - allocate-metro-port: - command: | - . ${REACT_NATIVE_VIRTENV}/scripts/lib/lib.sh - metro_port=$(rn_allocate_metro_port "android") - echo "๐Ÿ“ก Allocated Metro port: $metro_port" - env_file=$(rn_save_metro_env "android" "$metro_port") - echo "โœ“ Metro environment saved to: $env_file" - availability: - restart: "no" - # Phase 1: Build Node dependencies build-node: - depends_on: - allocate-metro-port: - condition: process_completed_successfully - command: "devbox run --pure build:node" + command: "npm install" availability: restart: "no" # Phase 2: Build Android app (Debug) build-android: command: | - . ${ANDROID_RUNTIME_DIR}/scripts/init/setup.sh - echo '๐Ÿ“ฆ Building Android app (Debug)...' - cd android && gradle assembleDebug - echo 'โœ“ Build complete' + set -e + BUILD_CONFIG="${ANDROID_BUILD_CONFIG:-Debug}" + echo "Building Android app (${BUILD_CONFIG})..." + TASK="assemble${BUILD_CONFIG}" + cd android && ./gradlew "$TASK" + echo 'Build complete' depends_on: build-node: condition: process_completed_successfully @@ -50,11 +37,7 @@ processes: # Phase 3: Sync AVDs sync-avds: - command: | - . ${ANDROID_RUNTIME_DIR}/scripts/init/setup.sh - echo '๐Ÿ”„ Syncing AVDs with device definitions...' - android.sh devices sync - echo 'โœ“ Sync complete' + command: "android.sh devices sync" availability: restart: "no" @@ -64,58 +47,27 @@ processes: sync-avds: condition: process_completed_successfully command: | - . ${ANDROID_RUNTIME_DIR}/scripts/init/setup.sh + set -e device="${ANDROID_DEFAULT_DEVICE:-max}" - serial="emulator-5554" - echo "$serial" > ${ANDROID_RUNTIME_DIR}/emulator-serial.txt - - if adb devices | grep -q "$serial"; then - echo "โœ“ Emulator already running: $serial" - else - echo "๐Ÿš€ Starting emulator: $device" - android.sh emulator start "$device" & - echo "โœ“ Emulator starting (PID: $!)" - - echo "Waiting for emulator to boot..." - max_attempts=60 - attempt=0 - while [ $attempt -lt $max_attempts ]; do - if adb -s $serial shell getprop sys.boot_completed 2>/dev/null | grep -q "1"; then - echo "โœ“ Emulator ready for development" - break - fi - attempt=$((attempt + 1)) - sleep 2 - done - - if [ $attempt -ge $max_attempts ]; then - echo "ERROR: Emulator did not boot within timeout" - exit 1 - fi - fi - - echo "โœ“ Emulator will remain running for hot reload" - exit 0 + echo "Starting emulator: $device" + android.sh emulator start "$device" + # Keep process alive for readiness probe (process-compose services pattern) + tail -f /dev/null availability: restart: "no" readiness_probe: exec: - command: | - . ${ANDROID_RUNTIME_DIR}/scripts/init/setup.sh - serial=$(cat ${ANDROID_RUNTIME_DIR}/emulator-serial.txt 2>/dev/null || echo "") - [ -n "$serial" ] && adb -s $serial shell getprop sys.boot_completed 2>/dev/null | grep -q "1" + command: "android.sh emulator ready" initial_delay_seconds: 10 - period_seconds: 10 - timeout_seconds: 180 + period_seconds: 5 + timeout_seconds: 10 success_threshold: 1 - failure_threshold: 12 + failure_threshold: 24 # Phase 5: Start Metro bundler with hot reload metro-bundler: command: "metro.sh start android" depends_on: - allocate-metro-port: - condition: process_completed_successfully build-node: condition: process_completed_successfully availability: @@ -130,24 +82,29 @@ processes: command: "metro.sh health android android" initial_delay_seconds: 5 period_seconds: 5 - timeout_seconds: 60 + timeout_seconds: 5 success_threshold: 1 + failure_threshold: 12 # Phase 6: Deploy app (Debug build for fast iteration) deploy-android: command: | + set -e . ${REACT_NATIVE_VIRTENV}/metro/env-android.sh - echo "๐Ÿ“ฒ Deploying React Native app (Debug, Metro port: $METRO_PORT)..." - npx react-native run-android --no-packager + echo "Deploying React Native app (Debug, Metro port: $METRO_PORT)..." + + # Deploy using android.sh (APK already built by build-android) + android.sh deploy + echo "" - echo "โœ“ App deployed!" + echo "App deployed!" echo " Metro: http://localhost:$METRO_PORT" echo " Hot reload enabled - edit code and save to see changes" depends_on: build-android: condition: process_completed_successfully android-emulator: - condition: process_completed_successfully + condition: process_healthy metro-bundler: condition: process_healthy availability: diff --git a/examples/react-native/tests/dev-ios.yaml b/examples/react-native/tests/dev-ios.yaml index 5928bf1..970e245 100644 --- a/examples/react-native/tests/dev-ios.yaml +++ b/examples/react-native/tests/dev-ios.yaml @@ -8,7 +8,7 @@ version: "0.5" environment: - "IOS_DEVICE=${IOS_DEFAULT_DEVICE:-max}" - - "BOOT_TIMEOUT=120" + - "SUITE_NAME=rn-ios-dev" - "ANDROID_SKIP_SETUP=1" - "LANG=en_US.UTF-8" - "LC_ALL=en_US.UTF-8" @@ -19,33 +19,20 @@ log_level: info is_strict: false # Allow graceful exit processes: - # Phase 0: Allocate Metro port - allocate-metro-port: - command: | - . ${REACT_NATIVE_VIRTENV}/scripts/lib/lib.sh - metro_port=$(rn_allocate_metro_port "ios") - echo "๐Ÿ“ก Allocated Metro port: $metro_port" - env_file=$(rn_save_metro_env "ios" "$metro_port") - echo "โœ“ Metro environment saved to: $env_file" - availability: - restart: "no" - # Phase 1: Build Node dependencies build-node: - depends_on: - allocate-metro-port: - condition: process_completed_successfully - command: "devbox run --pure build:node" + command: "npm install" availability: restart: "no" # Phase 2: Install CocoaPods dependencies install-pods: command: | + set -e cd ios echo "Installing CocoaPods dependencies..." pod install --repo-update - echo 'โœ“ Pod install complete' + echo 'Pod install complete' depends_on: build-node: condition: process_completed_successfully @@ -54,27 +41,19 @@ processes: # Phase 3: Sync simulators sync-simulators: - command: "devbox run ios.sh devices sync" + command: "ios.sh devices sync" availability: restart: "no" # Phase 4: Start simulator (reuses if already running) ios-simulator: command: | - echo "๐Ÿš€ Starting iOS simulator (${IOS_DEVICE})..." + set -e + echo "Starting iOS simulator (${IOS_DEVICE})..." ios.sh simulator start ${IOS_DEVICE} - - echo "Waiting for simulator to boot..." - DEVICE_UDID=$(xcrun simctl list devices | grep "Booted" | grep -oE '[0-9A-F-]{36}' | head -1 || echo "") - if [ -z "$DEVICE_UDID" ]; then - echo "ERROR: No booted simulator found" - exit 1 - fi - xcrun simctl bootstatus "$DEVICE_UDID" >/dev/null 2>&1 || true - - echo "โœ“ Simulator ready for development" - echo "โœ“ Simulator will remain running for hot reload" - exit 0 + echo "Simulator ready for development" + # Keep process alive for readiness probe (process-compose services pattern) + tail -f /dev/null depends_on: sync-simulators: condition: process_completed_successfully @@ -82,21 +61,17 @@ processes: restart: "no" readiness_probe: exec: - command: | - DEVICE_UDID=$(xcrun simctl list devices | grep "Booted" | grep -oE '[0-9A-F-]{36}' | head -1 || echo "") - [ -n "$DEVICE_UDID" ] && xcrun simctl bootstatus "$DEVICE_UDID" >/dev/null 2>&1 - initial_delay_seconds: 10 + command: "ios.sh simulator ready" + initial_delay_seconds: 5 period_seconds: 5 - timeout_seconds: 120 + timeout_seconds: 10 success_threshold: 1 - failure_threshold: 3 + failure_threshold: 12 # Phase 5: Start Metro bundler with hot reload metro-bundler: command: "metro.sh start ios" depends_on: - allocate-metro-port: - condition: process_completed_successfully build-node: condition: process_completed_successfully availability: @@ -111,88 +86,39 @@ processes: command: "metro.sh health ios ios" initial_delay_seconds: 5 period_seconds: 5 - timeout_seconds: 60 + timeout_seconds: 5 success_threshold: 1 + failure_threshold: 12 # Phase 6: Build and deploy app (Debug for fast iteration) deploy-ios: command: | set -e - # Use Debug build for fast compilation - BUILD_CONFIG="Debug" - - PROJECT_ROOT="${DEVBOX_PROJECT_ROOT:-$(pwd)}" - echo "๐Ÿ“ฒ Building React Native app (Debug)" - echo "Build configuration: $BUILD_CONFIG" + BUILD_CONFIG="${IOS_BUILD_CONFIG:-Debug}" + echo "Building React Native app (${BUILD_CONFIG})" . ${REACT_NATIVE_VIRTENV}/metro/env-ios.sh echo "Metro port: $METRO_PORT" - # Get simulator UDID - max_attempts=15 - attempt=0 - DEVICE_UDID="" - while [ $attempt -lt $max_attempts ] && [ -z "$DEVICE_UDID" ]; do - DEVICE_UDID=$(xcrun simctl list devices | grep "Booted" | grep -oE '[0-9A-F-]{36}' | head -1 || echo "") - if [ -z "$DEVICE_UDID" ]; then - attempt=$((attempt + 1)) - echo "Waiting for simulator... (attempt $attempt/$max_attempts)" - sleep 2 - fi - done - - if [ -z "$DEVICE_UDID" ]; then - echo "ERROR: No booted simulator found" - exit 1 - fi - - echo "Building for simulator: $DEVICE_UDID" - - DERIVED_DATA_PATH="$PROJECT_ROOT/.devbox/virtenv/ios/DerivedData" - mkdir -p "$DERIVED_DATA_PATH" - - # Build with xcodebuild (Debug for fast iteration) - ( - cd ios - env -u LD -u LDFLAGS -u NIX_LDFLAGS -u NIX_CFLAGS_COMPILE -u NIX_CFLAGS_LINK \ - NODE_BINARY="$NODE_BINARY" \ - RCT_METRO_PORT="$METRO_PORT" \ - xcodebuild \ - -workspace ReactNativeExample.xcworkspace \ - -scheme ${IOS_APP_SCHEME} \ - -configuration "$BUILD_CONFIG" \ - -destination "id=$DEVICE_UDID" \ - -derivedDataPath "$DERIVED_DATA_PATH" \ - -quiet \ - build - ) + # Build with xcodebuild (Nix vars stripped by init hook) + NODE_BINARY="$NODE_BINARY" RCT_METRO_PORT="$METRO_PORT" \ + ios.sh xcodebuild -workspace ios/ReactNativeExample.xcworkspace \ + -scheme ReactNativeExample -configuration "$BUILD_CONFIG" \ + -destination 'generic/platform=iOS Simulator' build - # Install the built app - echo "Installing app to simulator..." - APP_PATH="${DEVBOX_PROJECT_ROOT}/.devbox/virtenv/ios/DerivedData/Build/Products/Debug-iphonesimulator/ReactNativeExample.app" - - if [ ! -d "$APP_PATH" ]; then - echo "ERROR: App bundle not found at $APP_PATH" - exit 1 - fi - - xcrun simctl install "$DEVICE_UDID" "$APP_PATH" - - # Launch app with Metro port - echo "Launching app with Metro on port $METRO_PORT..." - xcrun simctl launch "$DEVICE_UDID" "${IOS_APP_BUNDLE_ID}" \ - RCT_METRO_PORT="$METRO_PORT" + # Deploy to simulator + ios.sh deploy echo "" - echo "โœ“ App deployed and running!" + echo "App deployed and running!" echo " Metro: http://localhost:$METRO_PORT" echo " Hot reload enabled - edit code and save to see changes" depends_on: install-pods: condition: process_completed_successfully ios-simulator: - condition: process_completed_successfully + condition: process_healthy metro-bundler: condition: process_healthy availability: diff --git a/examples/react-native/tests/dev-web.yaml b/examples/react-native/tests/dev-web.yaml index 3e33d49..7cdebf8 100644 --- a/examples/react-native/tests/dev-web.yaml +++ b/examples/react-native/tests/dev-web.yaml @@ -14,6 +14,7 @@ processes: # Phase 0: Allocate Metro port allocate-metro-port: command: | + set -e . ${REACT_NATIVE_VIRTENV}/scripts/lib/lib.sh metro_port=$(rn_allocate_metro_port "web") echo "๐Ÿ“ก Allocated Metro port: $metro_port" @@ -27,7 +28,7 @@ processes: depends_on: allocate-metro-port: condition: process_completed_successfully - command: "devbox run --pure build:node" + command: "npm install" availability: restart: "no" @@ -63,8 +64,9 @@ processes: command: "metro.sh health web ios" initial_delay_seconds: 5 period_seconds: 5 - timeout_seconds: 60 + timeout_seconds: 5 success_threshold: 1 + failure_threshold: 12 # Phase 4: Open browser open-browser: diff --git a/examples/react-native/tests/run-android-tests.sh b/examples/react-native/tests/run-android-tests.sh deleted file mode 100755 index d26bcf6..0000000 --- a/examples/react-native/tests/run-android-tests.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash -# External wrapper script to run Android tests with iOS setup skipped -# Run this from the react-native example directory: ./tests/run-android-tests.sh - -set -e - -cd "$(dirname "$0")/.." - -# Skip iOS setup for Android-only testing -# Use -e flag to pass environment variable through --pure mode -exec devbox run --pure -e IOS_SKIP_SETUP=1 -e TEST_TUI="${TEST_TUI:-false}" bash -c 'process-compose -f tests/test-suite-android-e2e.yaml --no-server --tui="${TEST_TUI:-false}"' diff --git a/examples/react-native/tests/run-ios-tests.sh b/examples/react-native/tests/run-ios-tests.sh deleted file mode 100755 index 8c91b0a..0000000 --- a/examples/react-native/tests/run-ios-tests.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash -# External wrapper script to run iOS tests with Android SDK evaluation skipped -# Run this from the react-native example directory: ./tests/run-android-tests.sh - -set -e - -cd "$(dirname "$0")/.." - -# Skip Android SDK downloads/evaluation for iOS-only testing -# Use -e flag to pass environment variable through --pure mode -exec devbox run --pure -e ANDROID_SKIP_SETUP=1 -e TEST_TUI="${TEST_TUI:-false}" bash -c 'process-compose -f tests/test-suite-ios-e2e.yaml --no-server --tui="${TEST_TUI:-false}"' diff --git a/examples/react-native/tests/test-suite-all-e2e.yaml b/examples/react-native/tests/test-suite-all-e2e.yaml index 76a8c35..6dec39f 100644 --- a/examples/react-native/tests/test-suite-all-e2e.yaml +++ b/examples/react-native/tests/test-suite-all-e2e.yaml @@ -1,62 +1,90 @@ version: "0.5" environment: - - "ANDROID_SERIAL=emulator-5554" + - "SUITE_NAME=rn-all-e2e" - "IOS_DEVICE=${IOS_DEFAULT_DEVICE:-max}" - - "TEST_TIMEOUT=300" - - "BOOT_TIMEOUT=180" - "TEST_TUI=${TEST_TUI:-false}" log_location: "reports/react-native-all-e2e-logs" log_level: info -is_strict: false +is_strict: true processes: - # Phase 0: Allocate Metro port (runs first, no dependencies) + # Phase 0: Allocate Metro port (no running process needed for builds) allocate-metro-port: command: | + set -e # Source React Native lib . ${REACT_NATIVE_VIRTENV}/scripts/lib/lib.sh - # Allocate port for all test suite + # Allocate port for all-platforms test suite metro_port=$(rn_allocate_metro_port "all") - echo "๐Ÿ“ก Allocated Metro port: $metro_port" + echo "Allocated Metro port: $metro_port" # Save environment file for other processes env_file=$(rn_save_metro_env "all" "$metro_port") - echo "โœ“ Metro environment saved to: $env_file" + echo "Metro environment saved to: $env_file" availability: restart: "no" # Phase 1: Build Node dependencies (shared by both platforms) build-node: - depends_on: - allocate-metro-port: - condition: process_completed_successfully - command: "devbox run --pure build:node" + command: | + set -e + _step="build-node" + _step_dir="reports/steps" + mkdir -p "$_step_dir" + trap 'printf "fail\nExited with error (exit code $?)\n" > "$_step_dir/$_step.status"' ERR + + npm install + + echo "pass" > "$_step_dir/$_step.status" availability: restart: "no" - # Phase 2a: Android build + # Phase 2a: Android build (depends on port allocation, NOT metro running) build-android: command: | - . ${ANDROID_RUNTIME_DIR}/scripts/init/setup.sh - echo '๐Ÿ“ฆ Building Android app...' - cd android && gradle assembleDebug - echo 'โœ“ Build complete' + set -e + _step="build-android" + _step_dir="$PWD/reports/steps" + mkdir -p "$_step_dir" + trap 'printf "fail\nExited with error (exit code $?)\n" > "$_step_dir/$_step.status"' ERR + + # Source Metro environment so build knows correct port + . ${REACT_NATIVE_VIRTENV}/metro/env-all.sh + echo "Metro port for build: $METRO_PORT" + + BUILD_CONFIG="${ANDROID_BUILD_CONFIG:-Debug}" + echo "Building Android app (${BUILD_CONFIG})..." + cd android + ./gradlew assemble${BUILD_CONFIG} -PreactNativeDevServerPort="$METRO_PORT" + echo 'Build complete' + + echo "pass" > "$_step_dir/$_step.status" depends_on: build-node: condition: process_completed_successfully + allocate-metro-port: + condition: process_completed_successfully availability: restart: "no" # Phase 2b: Install CocoaPods (parallel with Android build) install-pods: command: | + set -e + _step="install-pods" + _step_dir="$PWD/reports/steps" + mkdir -p "$_step_dir" + trap 'printf "fail\nExited with error (exit code $?)\n" > "$_step_dir/$_step.status"' ERR + cd ios echo "Installing CocoaPods dependencies..." pod install --repo-update - echo 'โœ“ Pod install complete' + echo 'Pod install complete' + + echo "pass" > "$_step_dir/$_step.status" depends_on: build-node: condition: process_completed_successfully @@ -65,119 +93,107 @@ processes: # Phase 3a: Sync Android AVDs sync-avds: - command: | - . ${ANDROID_RUNTIME_DIR}/scripts/init/setup.sh - echo '๐Ÿ”„ Syncing AVDs with device definitions...' - android.sh devices sync - echo 'โœ“ Sync complete' + command: "android.sh devices sync" availability: restart: "no" # Phase 3b: Sync iOS simulators (parallel with Android) sync-simulators: - command: "devbox run ios.sh devices sync" + command: "ios.sh devices sync" availability: restart: "no" - # Phase 4a: Start Android emulator - android-emulator: + # Phase 4a: Launch Android emulator if not running + launch-emulator: depends_on: sync-avds: condition: process_completed_successfully environment: - "ANDROID_EMULATOR_FOREGROUND=1" command: | - echo "[ANDROID-EMULATOR] Starting emulator..." - . ${ANDROID_RUNTIME_DIR}/scripts/init/setup.sh - + set -e device="${ANDROID_DEFAULT_DEVICE:-max}" - if [ "${IN_NIX_SHELL:-}" = "pure" ]; then - echo "๐Ÿš€ Starting fresh Android emulator (pure mode): $device" - export ANDROID_EMULATOR_PURE=1 - echo "emulator-5554" > ${ANDROID_RUNTIME_DIR}/emulator-serial.txt - android.sh emulator start "$device" - else - echo "๐Ÿš€ Verifying Android emulator is running: $device" - serial="emulator-5554" - echo "$serial" > ${ANDROID_RUNTIME_DIR}/emulator-serial.txt - - if adb devices | grep -q "$serial"; then - echo "โœ“ Emulator already running: $serial" - else - echo "Starting emulator in background: $device" - unset ANDROID_EMULATOR_FOREGROUND - android.sh emulator start "$device" & - echo "โœ“ Emulator starting (PID: $!)" - fi - - echo "Waiting for emulator to boot..." - max_attempts=60 - attempt=0 - while [ $attempt -lt $max_attempts ]; do - if adb -s $serial shell getprop sys.boot_completed 2>/dev/null | grep -q "1"; then - echo "โœ“ Emulator ready for testing" - break - fi - attempt=$((attempt + 1)) - sleep 2 - done - - if [ $attempt -ge $max_attempts ]; then - echo "ERROR: Emulator did not boot within timeout" - exit 1 - fi - - echo "โœ“ Emulator will remain running after tests complete" + # Check if emulator already running + if android.sh emulator ready 2>/dev/null; then + echo "Emulator already running, skipping launch" exit 0 fi + + echo "[ANDROID-EMULATOR] Starting emulator..." + if [ "${DEVBOX_PURE_SHELL:-}" = "1" ]; then + # Pure mode: foreground + echo "Starting fresh Android emulator (pure mode): $device" + android.sh emulator start --pure "$device" + tail -f /dev/null + else + # Dev mode: start, wait for ready, then detach + echo "Starting Android emulator: $device" + unset ANDROID_EMULATOR_FOREGROUND + android.sh emulator start --wait-ready "$device" + fi availability: restart: "no" readiness_probe: exec: - command: | - . ${ANDROID_RUNTIME_DIR}/scripts/init/setup.sh - serial=$(cat ${ANDROID_RUNTIME_DIR}/emulator-serial.txt 2>/dev/null || echo "") - [ -n "$serial" ] && adb -s $serial shell getprop sys.boot_completed 2>/dev/null | grep -q "1" + command: "android.sh emulator ready" initial_delay_seconds: 10 - period_seconds: 10 - timeout_seconds: 180 + period_seconds: 5 + timeout_seconds: 10 success_threshold: 1 - failure_threshold: 12 + failure_threshold: 60 - # Phase 4b: Start iOS simulator (parallel with Android) - ios-simulator: + # Phase 4b: Verify Android emulator ready + verify-emulator-ready: command: | - mkdir -p .devbox/virtenv/ios/runtime - - if [ "${IN_NIX_SHELL:-}" = "pure" ]; then - echo "๐Ÿš€ Starting fresh iOS simulator with clean state (${IOS_DEVICE})..." - output=$(ios.sh simulator start --pure ${IOS_DEVICE} 2>&1) - echo "$output" - - test_udid=$(echo "$output" | grep -oE '[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}' | tail -1) - if [ -n "$test_udid" ]; then - echo "$test_udid" > .devbox/virtenv/ios/runtime/simulator-udid.txt - echo "โœ“ Test simulator UDID saved: $test_udid" - fi - else - echo "๐Ÿš€ Starting iOS simulator (${IOS_DEVICE})..." - ios.sh simulator start ${IOS_DEVICE} - - echo "Waiting for simulator to boot..." - DEVICE_UDID=$(xcrun simctl list devices | grep "Booted" | grep -oE '[0-9A-F-]{36}' | head -1 || echo "") - if [ -z "$DEVICE_UDID" ]; then - echo "ERROR: No booted simulator found" + _step="emulator-boot" + _step_dir="reports/steps" + mkdir -p "$_step_dir" + + # Wait a moment for launch to start + sleep 5 + + echo "Verifying emulator is ready..." + max_wait=300 + elapsed=0 + while ! android.sh emulator ready 2>/dev/null; do + sleep 3 + elapsed=$((elapsed + 3)) + echo " Waiting for emulator... ${elapsed}s/${max_wait}s" + if [ $elapsed -ge $max_wait ]; then + printf 'fail\nTimed out after %ds waiting for emulator to boot\n' "$max_wait" > "$_step_dir/$_step.status" exit 1 fi - xcrun simctl bootstatus "$DEVICE_UDID" >/dev/null 2>&1 || true + done + echo "Emulator ready" + + echo "pass" > "$_step_dir/$_step.status" + depends_on: + sync-avds: + condition: process_completed_successfully + availability: + restart: "no" + + # Phase 4c: Launch iOS simulator if not running + launch-simulator: + command: | + set -e - echo "โœ“ Simulator ready for testing" - echo "โœ“ Simulator will remain running after tests complete" + # Check if simulator already running + if ios.sh simulator ready 2>/dev/null; then + echo "Simulator already running, skipping launch" exit 0 fi - tail -f /dev/null + echo "Starting iOS simulator..." + if [ "${DEVBOX_PURE_SHELL:-}" = "1" ]; then + # Pure mode: foreground + ios.sh simulator start --pure ${IOS_DEVICE} + tail -f /dev/null + else + # Dev mode: start, wait for ready, then detach + ios.sh simulator start --wait-ready ${IOS_DEVICE} + fi depends_on: sync-simulators: condition: process_completed_successfully @@ -185,75 +201,172 @@ processes: restart: "no" readiness_probe: exec: - command: | - if [ "${IN_NIX_SHELL:-}" = "pure" ]; then - DEVICE_UDID=$(xcrun simctl list devices | grep "Test" | grep "Booted" | grep -oE '[0-9A-F-]{36}' | head -1 || echo "") - else - DEVICE_UDID=$(xcrun simctl list devices | grep "Booted" | grep -oE '[0-9A-F-]{36}' | head -1 || echo "") - fi - [ -n "$DEVICE_UDID" ] && xcrun simctl bootstatus "$DEVICE_UDID" >/dev/null 2>&1 - initial_delay_seconds: 10 + command: "ios.sh simulator ready" + initial_delay_seconds: 5 period_seconds: 5 - timeout_seconds: 120 + timeout_seconds: 10 success_threshold: 1 - failure_threshold: 3 + failure_threshold: 36 + + # Phase 4d: Verify iOS simulator ready + verify-simulator-ready: + command: | + _step="simulator-boot" + _step_dir="reports/steps" + mkdir -p "$_step_dir" + + # Wait a moment for launch to start + sleep 3 + + echo "Verifying simulator is ready..." + max_wait=180 + elapsed=0 + while ! ios.sh simulator ready 2>/dev/null; do + sleep 2 + elapsed=$((elapsed + 2)) + echo " Waiting for simulator... ${elapsed}s/${max_wait}s" + if [ $elapsed -ge $max_wait ]; then + printf 'fail\nTimed out after %ds waiting for simulator to boot\n' "$max_wait" > "$_step_dir/$_step.status" + exit 1 + fi + done + echo "Simulator ready" + + echo "pass" > "$_step_dir/$_step.status" + depends_on: + sync-simulators: + condition: process_completed_successfully + availability: + restart: "no" # Phase 5: Start Metro bundler (shared by both platforms) metro-bundler: command: "metro.sh start all" depends_on: - allocate-metro-port: - condition: process_completed_successfully build-node: condition: process_completed_successfully + allocate-metro-port: + condition: process_completed_successfully availability: restart: "no" - shutdown: - command: "metro.sh stop all || true" - signal: 15 - timeout_seconds: 5 readiness_probe: exec: command: "metro.sh health all ios" initial_delay_seconds: 5 period_seconds: 5 - timeout_seconds: 60 + timeout_seconds: 5 success_threshold: 1 + failure_threshold: 12 + + # Phase 5b: Build iOS app (depends on port allocation, NOT metro running) + build-ios: + command: | + set -e + _step="build-ios" + _step_dir="$PWD/reports/steps" + mkdir -p "$_step_dir" + trap 'printf "fail\nExited with error (exit code $?)\n" > "$_step_dir/$_step.status"' ERR + + BUILD_CONFIG="${IOS_BUILD_CONFIG:-Debug}" + echo "Building React Native app (${BUILD_CONFIG})" + + # Source Metro environment so React Native uses correct port + . ${REACT_NATIVE_VIRTENV}/metro/env-all.sh + echo "Metro port: $METRO_PORT" + + # Set up DerivedData path + DERIVED_DATA="${DEVBOX_PROJECT_ROOT:-.}/.devbox/virtenv/ios/DerivedData" + mkdir -p "$DERIVED_DATA" + + # Build with xcodebuild + cd ios + NODE_BINARY="$NODE_BINARY" RCT_METRO_PORT="$METRO_PORT" \ + xcodebuild -workspace ReactNativeExample.xcworkspace \ + -scheme ReactNativeExample \ + -configuration "$BUILD_CONFIG" \ + -destination "generic/platform=iOS Simulator" \ + -derivedDataPath "$DERIVED_DATA" \ + -quiet \ + build + + echo "Build complete" + + echo "pass" > "$_step_dir/$_step.status" + depends_on: + install-pods: + condition: process_completed_successfully + allocate-metro-port: + condition: process_completed_successfully + availability: + restart: "no" # Phase 6a: Deploy Android app deploy-android: command: | + set -e + _step="deploy-android" + _step_dir="reports/steps" + mkdir -p "$_step_dir" + trap 'printf "fail\nExited with error (exit code $?)\n" > "$_step_dir/$_step.status"' ERR + # Source Metro environment so React Native uses correct port . ${REACT_NATIVE_VIRTENV}/metro/env-all.sh + echo "Metro port: $METRO_PORT" + + # Setup ADB reverse port forwarding so app can reach Metro + # React Native apps connect to localhost:8081 by default, forward to actual Metro port + serial="" + state_dir="${ANDROID_RUNTIME_DIR:-.devbox/virtenv}/${SUITE_NAME:-default}" + if [ -f "$state_dir/emulator-serial.txt" ]; then + serial="$(cat "$state_dir/emulator-serial.txt")" + fi + if [ -z "$serial" ]; then + serial="emulator-${EMU_PORT:-5554}" + fi + + echo "Setting up Metro port forwarding: 8081 -> $METRO_PORT (serial: $serial)" + + # Remove any existing reverse on port 8081 + adb -s "$serial" reverse --remove tcp:8081 2>/dev/null || true + + # Set up new reverse + adb -s "$serial" reverse tcp:8081 tcp:$METRO_PORT - echo "๐Ÿ“ฒ Deploying React Native app to Android (Metro port: $METRO_PORT)..." - npx react-native run-android --no-packager + # Verify reverse is active + echo "Verifying port forwarding..." + adb -s "$serial" reverse --list + + # Deploy using android.sh (APK already built by build-android) + android.sh deploy + + echo "pass" > "$_step_dir/$_step.status" depends_on: build-android: condition: process_completed_successfully - android-emulator: + verify-emulator-ready: condition: process_completed_successfully metro-bundler: condition: process_healthy availability: restart: "no" - # Phase 6b: Build and deploy iOS app (React Native CLI handles both, like Android) + # Phase 6b: Deploy iOS app deploy-ios: command: | set -e + _step="deploy-ios" + _step_dir="reports/steps" + mkdir -p "$_step_dir" + trap 'printf "fail\nExited with error (exit code $?)\n" > "$_step_dir/$_step.status"' ERR - # Source Metro environment so React Native uses correct port - . ${REACT_NATIVE_VIRTENV}/metro/env-all.sh + ios.sh deploy + echo "App deployed and launched successfully" - echo "๐Ÿ“ฒ Deploying React Native app to iOS (Metro port: $METRO_PORT)..." - echo "Build configuration: Release" - # Use Release build for E2E tests - npx react-native run-ios --no-packager --configuration Release + echo "pass" > "$_step_dir/$_step.status" depends_on: - install-pods: + build-ios: condition: process_completed_successfully - ios-simulator: + verify-simulator-ready: condition: process_completed_successfully metro-bundler: condition: process_healthy @@ -263,30 +376,66 @@ processes: # Phase 7a: Verify Android app verify-android-running: command: | - . ${ANDROID_RUNTIME_DIR}/scripts/init/setup.sh - serial=$(cat ${ANDROID_RUNTIME_DIR}/emulator-serial.txt 2>/dev/null || echo emulator-5554) - - # Resolve app ID: env var > auto-detected from APK > fail - app_id="${ANDROID_APP_ID:-$(cat ${ANDROID_RUNTIME_DIR}/app-id.txt 2>/dev/null || true)}" - if [ -z "$app_id" ]; then - echo "ERROR: Cannot determine app ID. Set ANDROID_APP_ID or run deploy first." >&2 - exit 1 + _step="verify-android" + _step_dir="reports/steps" + mkdir -p "$_step_dir" + + echo "Waiting for Android app to start..." + + # Resolve emulator serial for log checking + serial="" + state_dir="${ANDROID_RUNTIME_DIR:-.devbox/virtenv}/${SUITE_NAME:-default}" + if [ -f "$state_dir/emulator-serial.txt" ]; then + serial="$(cat "$state_dir/emulator-serial.txt")" fi + if [ -z "$serial" ]; then + serial="emulator-${EMU_PORT:-5554}" + fi + echo " Serial: $serial" - echo "Waiting for app to start ($app_id)..." - max_attempts=10 + max_attempts=20 attempt=0 while [ $attempt -lt $max_attempts ]; do - if adb -s $serial shell pidof "$app_id" >/dev/null 2>&1; then - echo "โœ“ App process running (PID: $(adb -s $serial shell pidof "$app_id"))" - exit 0 + if ! android.sh app status; then + attempt=$((attempt + 1)) + echo " Attempt $attempt/$max_attempts: App not running yet..." + sleep 2 + continue fi - attempt=$((attempt + 1)) - echo " Attempt $attempt/$max_attempts..." - sleep 2 + + echo " App process is running, waiting 5 seconds for startup..." + sleep 5 + + echo " Checking logs for errors..." + + # Get recent logs from ALL tags (not just ReactNative), more lines + log_output=$(adb -s "$serial" logcat -d -t 500 2>/dev/null || true) + + # Check for Metro connection errors (these appear in red box) + if echo "$log_output" | grep -qi "unable.*load.*script\|could.*not.*connect.*metro\|connection.*refused.*8081\|packaged.*correctly.*for.*release"; then + echo "ERROR: App cannot connect to Metro bundler:" >&2 + echo "$log_output" | grep -iE "unable|load|script|connect|metro|refused|8081|packaged|release|bundle" | tail -30 >&2 + echo "" >&2 + echo "Full recent logs:" >&2 + echo "$log_output" | tail -50 >&2 + printf 'fail\nApp cannot connect to Metro bundler\n' > "$_step_dir/$_step.status" + exit 1 + fi + + # Check for other fatal errors + if echo "$log_output" | grep -qi "fatal.*error\|exception.*error"; then + echo "ERROR: App encountered fatal errors:" >&2 + echo "$log_output" | grep -iE "fatal|exception" | tail -20 >&2 + printf 'fail\nApp encountered fatal errors\n' > "$_step_dir/$_step.status" + exit 1 + fi + + echo "Android app is running and connected to Metro" + echo "pass" > "$_step_dir/$_step.status" + exit 0 done - echo "ERROR: App did not start within timeout" >&2 + printf 'fail\nAndroid app did not start within timeout\n' > "$_step_dir/$_step.status" exit 1 depends_on: deploy-android: @@ -297,26 +446,25 @@ processes: # Phase 7b: Verify iOS app (parallel with Android) verify-ios-running: command: | - if [ "${IN_NIX_SHELL:-}" = "pure" ]; then - DEVICE_UDID=$(xcrun simctl list devices | grep "Test" | grep "Booted" | grep -oE '[0-9A-F-]{36}' | head -1) - else - DEVICE_UDID=$(xcrun simctl list devices | grep "Booted" | grep -oE '[0-9A-F-]{36}' | head -1) - fi + _step="verify-ios" + _step_dir="reports/steps" + mkdir -p "$_step_dir" - echo "Waiting for app to start..." - max_attempts=10 + echo "Waiting for iOS app to start..." + max_attempts=15 attempt=0 while [ $attempt -lt $max_attempts ]; do - if xcrun simctl spawn "$DEVICE_UDID" launchctl list | grep -q "${IOS_APP_BUNDLE_ID}"; then - echo "โœ“ App is running: ${IOS_APP_BUNDLE_ID}" + if ios.sh app status; then + echo "iOS app is running" + echo "pass" > "$_step_dir/$_step.status" exit 0 fi attempt=$((attempt + 1)) - echo " Attempt $attempt/$max_attempts..." + echo " Attempt $attempt/$max_attempts: App not running yet..." sleep 2 done - echo "ERROR: App did not start within timeout" >&2 + printf 'fail\niOS app did not start within timeout\n' > "$_step_dir/$_step.status" exit 1 depends_on: deploy-ios: @@ -327,57 +475,45 @@ processes: # Cleanup - runs when both platforms complete cleanup: command: | - # Stop Metro bundler explicitly - echo "Stopping Metro bundler..." - metro.sh stop all || true - - # Also kill the metro.sh wrapper process - pkill -f "metro.sh start all" || true - - if [ "${IN_NIX_SHELL:-}" = "pure" ]; then - echo "๐Ÿงน Cleaning up (pure mode)..." + if [ "${DEVBOX_PURE_SHELL:-}" = "1" ]; then + echo "Cleaning up (pure mode)..." + metro.sh stop all || true # Cleanup Android - . ${ANDROID_RUNTIME_DIR}/scripts/init/setup.sh - serial=$(cat ${ANDROID_RUNTIME_DIR}/emulator-serial.txt 2>/dev/null || echo emulator-5554) - app_id="${ANDROID_APP_ID:-$(cat ${ANDROID_RUNTIME_DIR}/app-id.txt 2>/dev/null || true)}" - [ -n "$app_id" ] && adb -s $serial shell am force-stop "$app_id" 2>/dev/null || true + android.sh app stop || true android.sh emulator stop || true # Cleanup iOS - test_sim_udid=$(cat .devbox/virtenv/ios/runtime/simulator-udid.txt 2>/dev/null || echo "") - if [ -z "$test_sim_udid" ]; then - test_sim_udid=$(xcrun simctl list devices | grep "Test" | grep -oE '[0-9A-F-]{36}' | head -1 || true) - fi - if [ -n "$test_sim_udid" ]; then - xcrun simctl terminate "$test_sim_udid" "${IOS_APP_BUNDLE_ID}" 2>/dev/null || true - xcrun simctl shutdown "$test_sim_udid" >/dev/null 2>&1 || true - xcrun simctl delete "$test_sim_udid" >/dev/null 2>&1 || true - rm -f .devbox/virtenv/ios/runtime/simulator-udid.txt - fi + ios.sh app stop || true + ios.sh simulator stop || true - echo "โœ“ Both platforms cleaned up" + echo "Metro, apps, and emulator/simulator stopped" else - echo "โœ“ Test complete - apps and emulator/simulator still running" + echo "Test complete - Metro, apps, and emulator/simulator still running for iteration" fi depends_on: verify-android-running: condition: process_completed verify-ios-running: condition: process_completed - metro-bundler: - condition: process_healthy availability: restart: "no" - # Summary - displays results and optionally stays open + # Summary - reports per-step pass/fail summary: - command: "bash tests/test-summary.sh 'React Native All Platforms E2E Test Suite' 'reports/react-native-all-e2e-logs'" + command: | + set -e + export REPO_ROOT="$(cd ../.. && pwd)" + . "$REPO_ROOT/plugins/tests/test-framework.sh" + log_test "React Native All Platforms E2E Test Suite" + e2e_report_steps || true + test_summary "rn-all-e2e" depends_on: cleanup: condition: process_completed availability: restart: "no" + exit_on_end: true shutdown: signal: 15 timeout_seconds: 1 diff --git a/examples/react-native/tests/test-suite-android-e2e.yaml b/examples/react-native/tests/test-suite-android-e2e.yaml index 904390c..ac02762 100644 --- a/examples/react-native/tests/test-suite-android-e2e.yaml +++ b/examples/react-native/tests/test-suite-android-e2e.yaml @@ -1,199 +1,285 @@ version: "0.5" environment: + - "SUITE_NAME=rn-android-e2e" - "TEST_TUI=${TEST_TUI:-false}" - - "ANDROID_SERIAL=emulator-5554" - - "TEST_TIMEOUT=300" - - "BOOT_TIMEOUT=180" log_location: "reports/react-native-android-e2e-logs" log_level: info -is_strict: false +is_strict: true processes: - # Phase 0: Allocate Metro port (runs first, no dependencies) + # Phase 0: Allocate Metro port (no running process needed for builds) allocate-metro-port: command: | + set -e # Source React Native lib . ${REACT_NATIVE_VIRTENV}/scripts/lib/lib.sh # Allocate port for android test suite metro_port=$(rn_allocate_metro_port "android") - echo "๐Ÿ“ก Allocated Metro port: $metro_port" + echo "Allocated Metro port: $metro_port" # Save environment file for other processes env_file=$(rn_save_metro_env "android" "$metro_port") - echo "โœ“ Metro environment saved to: $env_file" + echo "Metro environment saved to: $env_file" availability: restart: "no" # Phase 1: Build Node dependencies build-node: - depends_on: - allocate-metro-port: - condition: process_completed_successfully - command: "devbox run --pure build:node" + command: | + set -e + _step="build-node" + _step_dir="reports/steps" + mkdir -p "$_step_dir" + trap 'printf "fail\nExited with error (exit code $?)\n" > "$_step_dir/$_step.status"' ERR + + npm install + + echo "pass" > "$_step_dir/$_step.status" availability: restart: "no" - # Phase 2: Build Android app + # Phase 2: Build Android app (depends on port allocation, NOT metro running) build-android: command: | - # Source Android environment - . ${ANDROID_RUNTIME_DIR}/scripts/init/setup.sh - echo '๐Ÿ“ฆ Building Android app...' - cd android && gradle assembleDebug - echo 'โœ“ Build complete' + set -e + _step="build-android" + _step_dir="$PWD/reports/steps" + mkdir -p "$_step_dir" + trap 'printf "fail\nExited with error (exit code $?)\n" > "$_step_dir/$_step.status"' ERR + + # Source Metro environment so build knows correct port + . ${REACT_NATIVE_VIRTENV}/metro/env-android.sh + echo "Metro port for build: $METRO_PORT" + + BUILD_CONFIG="${ANDROID_BUILD_CONFIG:-Debug}" + echo "Building Android app (${BUILD_CONFIG})..." + cd android + ./gradlew assemble${BUILD_CONFIG} -PreactNativeDevServerPort="$METRO_PORT" + echo 'Build complete' + + echo "pass" > "$_step_dir/$_step.status" depends_on: build-node: condition: process_completed_successfully + allocate-metro-port: + condition: process_completed_successfully availability: restart: "no" # Phase 3: Sync AVDs sync-avds: - command: | - . ${ANDROID_RUNTIME_DIR}/scripts/init/setup.sh - echo '๐Ÿ”„ Syncing AVDs with device definitions...' - android.sh devices sync - echo 'โœ“ Sync complete' + command: "android.sh devices sync" availability: restart: "no" - # Phase 4: Start emulator - android-emulator: + # Phase 4a: Launch emulator if not running + launch-emulator: depends_on: sync-avds: condition: process_completed_successfully environment: - "ANDROID_EMULATOR_FOREGROUND=1" command: | - echo "[ANDROID-EMULATOR] Starting emulator..." - # Source Android environment - . ${ANDROID_RUNTIME_DIR}/scripts/init/setup.sh - + set -e device="${ANDROID_DEFAULT_DEVICE:-max}" - if [ "${IN_NIX_SHELL:-}" = "pure" ]; then - echo "๐Ÿš€ Starting fresh Android emulator (pure mode): $device" - export ANDROID_EMULATOR_PURE=1 - echo "emulator-5554" > ${ANDROID_RUNTIME_DIR}/emulator-serial.txt - android.sh emulator start "$device" - else - echo "๐Ÿš€ Verifying Android emulator is running: $device" - serial="emulator-5554" - echo "$serial" > ${ANDROID_RUNTIME_DIR}/emulator-serial.txt - - if adb devices | grep -q "$serial"; then - echo "โœ“ Emulator already running: $serial" - else - echo "Starting emulator in background: $device" - unset ANDROID_EMULATOR_FOREGROUND - android.sh emulator start "$device" & - echo "โœ“ Emulator starting (PID: $!)" - fi - - echo "Waiting for emulator to boot..." - max_attempts=60 - attempt=0 - while [ $attempt -lt $max_attempts ]; do - if adb -s $serial shell getprop sys.boot_completed 2>/dev/null | grep -q "1"; then - echo "โœ“ Emulator ready for testing" - break - fi - attempt=$((attempt + 1)) - sleep 2 - done - - if [ $attempt -ge $max_attempts ]; then - echo "ERROR: Emulator did not boot within timeout" - exit 1 - fi - - echo "โœ“ Emulator will remain running after tests complete" + # Check if emulator already running + if android.sh emulator ready 2>/dev/null; then + echo "Emulator already running, skipping launch" exit 0 fi + + echo "[ANDROID-EMULATOR] Starting emulator..." + if [ "${DEVBOX_PURE_SHELL:-}" = "1" ]; then + # Pure mode: foreground, process-compose manages lifecycle + echo "Starting fresh Android emulator (pure mode): $device" + android.sh emulator start --pure "$device" + tail -f /dev/null # Keep alive + else + # Dev mode: start emulator, wait for ready, then detach + echo "Starting Android emulator: $device" + unset ANDROID_EMULATOR_FOREGROUND + android.sh emulator start --wait-ready "$device" + fi availability: restart: "no" readiness_probe: exec: - command: | - . ${ANDROID_RUNTIME_DIR}/scripts/init/setup.sh - serial=$(cat ${ANDROID_RUNTIME_DIR}/emulator-serial.txt 2>/dev/null || echo "") - [ -n "$serial" ] && adb -s $serial shell getprop sys.boot_completed 2>/dev/null | grep -q "1" + command: "android.sh emulator ready" initial_delay_seconds: 10 - period_seconds: 10 - timeout_seconds: 180 + period_seconds: 5 + timeout_seconds: 10 success_threshold: 1 - failure_threshold: 12 + failure_threshold: 60 + + # Phase 4b: Verify emulator ready + verify-emulator-ready: + command: | + _step="emulator-boot" + _step_dir="reports/steps" + mkdir -p "$_step_dir" + + # Wait a moment for launch to start + sleep 5 + + echo "Verifying emulator is ready..." + max_wait=300 + elapsed=0 + while ! android.sh emulator ready 2>/dev/null; do + sleep 3 + elapsed=$((elapsed + 3)) + echo " Waiting for emulator... ${elapsed}s/${max_wait}s" + if [ $elapsed -ge $max_wait ]; then + printf 'fail\nTimed out after %ds waiting for emulator to boot\n' "$max_wait" > "$_step_dir/$_step.status" + exit 1 + fi + done + echo "Emulator ready" + + echo "pass" > "$_step_dir/$_step.status" + depends_on: + sync-avds: + condition: process_completed_successfully + availability: + restart: "no" # Phase 5: Start Metro bundler metro-bundler: command: "metro.sh start android" depends_on: - allocate-metro-port: - condition: process_completed_successfully build-node: condition: process_completed_successfully + allocate-metro-port: + condition: process_completed_successfully availability: restart: "no" - shutdown: - command: "metro.sh stop android || true" - signal: 15 - timeout_seconds: 5 readiness_probe: exec: command: "metro.sh health android android" initial_delay_seconds: 5 period_seconds: 5 - timeout_seconds: 60 + timeout_seconds: 5 success_threshold: 1 + failure_threshold: 12 # Phase 6: Deploy app deploy-android: command: | + set -e + _step="deploy" + _step_dir="reports/steps" + mkdir -p "$_step_dir" + trap 'printf "fail\nExited with error (exit code $?)\n" > "$_step_dir/$_step.status"' ERR + # Source Metro environment so React Native uses correct port . ${REACT_NATIVE_VIRTENV}/metro/env-android.sh + echo "Metro port: $METRO_PORT" - echo "๐Ÿ“ฒ Deploying React Native app (Metro port: $METRO_PORT)..." - npx react-native run-android --no-packager + # Setup ADB reverse port forwarding so app can reach Metro + # React Native apps connect to localhost:8081 by default, forward to actual Metro port + serial="" + state_dir="${ANDROID_RUNTIME_DIR:-.devbox/virtenv}/${SUITE_NAME:-default}" + if [ -f "$state_dir/emulator-serial.txt" ]; then + serial="$(cat "$state_dir/emulator-serial.txt")" + fi + if [ -z "$serial" ]; then + serial="emulator-${EMU_PORT:-5554}" + fi + + echo "Setting up Metro port forwarding: 8081 -> $METRO_PORT (serial: $serial)" + + # Remove any existing reverse on port 8081 + adb -s "$serial" reverse --remove tcp:8081 2>/dev/null || true + + # Set up new reverse + adb -s "$serial" reverse tcp:8081 tcp:$METRO_PORT + + # Verify reverse is active + echo "Verifying port forwarding..." + adb -s "$serial" reverse --list + + # Deploy using android.sh (APK already built by build-android) + android.sh deploy + + echo "pass" > "$_step_dir/$_step.status" depends_on: build-android: condition: process_completed_successfully - android-emulator: + verify-emulator-ready: condition: process_completed_successfully metro-bundler: condition: process_healthy availability: restart: "no" - # Phase 7: Verify app is running + # Phase 7: Verify app is running and connected to Metro verify-app-running: command: | - . ${ANDROID_RUNTIME_DIR}/scripts/init/setup.sh - serial=$(cat ${ANDROID_RUNTIME_DIR}/emulator-serial.txt 2>/dev/null || echo emulator-5554) - - # Resolve app ID: env var > auto-detected from APK > fail - app_id="${ANDROID_APP_ID:-$(cat ${ANDROID_RUNTIME_DIR}/app-id.txt 2>/dev/null || true)}" - if [ -z "$app_id" ]; then - echo "ERROR: Cannot determine app ID. Set ANDROID_APP_ID or run deploy first." >&2 - exit 1 + _step="verify-app" + _step_dir="reports/steps" + mkdir -p "$_step_dir" + + echo "Waiting for app to start and connect to Metro..." + + # Resolve emulator serial for log checking + state_dir="${ANDROID_RUNTIME_DIR:-.devbox/virtenv}/${SUITE_NAME:-default}" + serial="" + if [ -f "$state_dir/emulator-serial.txt" ]; then + serial="$(cat "$state_dir/emulator-serial.txt")" fi + if [ -z "$serial" ]; then + serial="emulator-${EMU_PORT:-5554}" + fi + echo " Serial: $serial" - echo "Waiting for app to start ($app_id)..." - max_attempts=10 + max_attempts=20 attempt=0 while [ $attempt -lt $max_attempts ]; do - if adb -s $serial shell pidof "$app_id" >/dev/null 2>&1; then - echo "โœ“ App process running (PID: $(adb -s $serial shell pidof "$app_id"))" - exit 0 + # Check if app process is running using built-in command + if ! android.sh app status; then + attempt=$((attempt + 1)) + echo " Attempt $attempt/$max_attempts: App not running yet..." + sleep 2 + continue fi - attempt=$((attempt + 1)) - echo " Attempt $attempt/$max_attempts..." - sleep 2 + + echo " App process is running, waiting 5 seconds for startup..." + sleep 5 + + echo " Checking logs for errors..." + + # Get recent logs from ALL tags (not just ReactNative), more lines + log_output=$(adb -s "$serial" logcat -d -t 500 2>/dev/null || true) + + # Check for Metro connection errors (these appear in red box) + if echo "$log_output" | grep -qi "unable.*load.*script\|could.*not.*connect.*metro\|connection.*refused.*8081\|packaged.*correctly.*for.*release"; then + echo "ERROR: App cannot connect to Metro bundler:" >&2 + echo "$log_output" | grep -iE "unable|load|script|connect|metro|refused|8081|packaged|release|bundle" | tail -30 >&2 + echo "" >&2 + echo "Full recent logs:" >&2 + echo "$log_output" | tail -50 >&2 + printf 'fail\nApp cannot connect to Metro bundler\n' > "$_step_dir/$_step.status" + exit 1 + fi + + # Check for other fatal errors + if echo "$log_output" | grep -qi "fatal.*error\|exception.*error"; then + echo "ERROR: App encountered fatal errors:" >&2 + echo "$log_output" | grep -iE "fatal|exception" | tail -20 >&2 + printf 'fail\nApp encountered fatal errors\n' > "$_step_dir/$_step.status" + exit 1 + fi + + # Success: app is running and no errors + echo "App is running and connected to Metro" + echo "pass" > "$_step_dir/$_step.status" + exit 0 done - echo "ERROR: App did not start within timeout" >&2 + printf 'fail\nApp did not start within timeout\n' > "$_step_dir/$_step.status" exit 1 depends_on: deploy-android: @@ -204,42 +290,36 @@ processes: # Cleanup cleanup: command: | - . ${ANDROID_RUNTIME_DIR}/scripts/init/setup.sh - - serial=$(cat ${ANDROID_RUNTIME_DIR}/emulator-serial.txt 2>/dev/null || echo emulator-5554) - - # Stop Metro bundler explicitly - echo "Stopping Metro bundler..." - metro.sh stop android || true - - # Also kill the metro.sh wrapper process - pkill -f "metro.sh start android" || true - - if [ "${IN_NIX_SHELL:-}" = "pure" ]; then - echo "๐Ÿงน Cleaning up (pure mode)..." - app_id="${ANDROID_APP_ID:-$(cat ${ANDROID_RUNTIME_DIR}/app-id.txt 2>/dev/null || true)}" - [ -n "$app_id" ] && adb -s $serial shell am force-stop "$app_id" 2>/dev/null || true + if [ "${DEVBOX_PURE_SHELL:-}" = "1" ]; then + echo "Cleaning up (pure mode)..." + metro.sh stop android || true + android.sh app stop || true android.sh emulator stop || true - echo "โœ“ App stopped, emulator stopped" + echo "Metro, app, and emulator stopped" else - echo "โœ“ Test complete - app and emulator still running" + echo "Test complete - Metro, app, and emulator still running for iteration" fi depends_on: verify-app-running: condition: process_completed - metro-bundler: - condition: process_healthy availability: restart: "no" - # Summary - displays results and optionally stays open + # Summary - reports per-step pass/fail summary: - command: "bash tests/test-summary.sh 'React Native Android E2E Test Suite' 'reports/react-native-android-e2e-logs'" + command: | + set -e + export REPO_ROOT="$(cd ../.. && pwd)" + . "$REPO_ROOT/plugins/tests/test-framework.sh" + log_test "React Native Android E2E Test Suite" + e2e_report_steps || true + test_summary "rn-android-e2e" depends_on: cleanup: condition: process_completed availability: restart: "no" + exit_on_end: true shutdown: signal: 15 timeout_seconds: 1 diff --git a/examples/react-native/tests/test-suite-ios-e2e.yaml b/examples/react-native/tests/test-suite-ios-e2e.yaml index 08c43fc..006c289 100644 --- a/examples/react-native/tests/test-suite-ios-e2e.yaml +++ b/examples/react-native/tests/test-suite-ios-e2e.yaml @@ -2,51 +2,64 @@ version: "0.5" environment: - "IOS_DEVICE=${IOS_DEFAULT_DEVICE:-max}" - - "TEST_TIMEOUT=300" - - "BOOT_TIMEOUT=120" + - "SUITE_NAME=rn-ios-e2e" + - "IOS_BUILD_CONFIG=Debug" - "ANDROID_SKIP_SETUP=1" - "TEST_TUI=${TEST_TUI:-false}" - - "LANG=en_US.UTF-8" - - "LC_ALL=en_US.UTF-8" - "REACT_NATIVE_VIRTENV=${REACT_NATIVE_VIRTENV}" log_location: "reports/react-native-ios-e2e-logs" log_level: info -is_strict: false +is_strict: true processes: - # Phase 0: Allocate Metro port (runs first, no dependencies) + # Phase 0: Allocate Metro port (no running process needed for builds) allocate-metro-port: command: | + set -e # Source React Native lib . ${REACT_NATIVE_VIRTENV}/scripts/lib/lib.sh # Allocate port for ios test suite metro_port=$(rn_allocate_metro_port "ios") - echo "๐Ÿ“ก Allocated Metro port: $metro_port" + echo "Allocated Metro port: $metro_port" # Save environment file for other processes env_file=$(rn_save_metro_env "ios" "$metro_port") - echo "โœ“ Metro environment saved to: $env_file" + echo "Metro environment saved to: $env_file" availability: restart: "no" # Phase 1: Build Node dependencies build-node: - depends_on: - allocate-metro-port: - condition: process_completed_successfully - command: "devbox run --pure build:node" + command: | + set -e + _step="build-node" + _step_dir="reports/steps" + mkdir -p "$_step_dir" + trap 'printf "fail\nExited with error (exit code $?)\n" > "$_step_dir/$_step.status"' ERR + + npm install + + echo "pass" > "$_step_dir/$_step.status" availability: restart: "no" # Phase 2: Install CocoaPods dependencies install-pods: command: | + set -e + _step="install-pods" + _step_dir="$PWD/reports/steps" + mkdir -p "$_step_dir" + trap 'printf "fail\nExited with error (exit code $?)\n" > "$_step_dir/$_step.status"' ERR + cd ios echo "Installing CocoaPods dependencies..." pod install --repo-update - echo 'โœ“ Pod install complete' + echo 'Pod install complete' + + echo "pass" > "$_step_dir/$_step.status" depends_on: build-node: condition: process_completed_successfully @@ -55,43 +68,29 @@ processes: # Phase 3: Sync simulators sync-simulators: - command: "devbox run ios.sh devices sync" + command: "ios.sh devices sync" availability: restart: "no" - # Phase 4: Start simulator - ios-simulator: + # Phase 4a: Launch simulator if not running + launch-simulator: command: | - mkdir -p .devbox/virtenv/ios/runtime - - if [ "${DEVBOX_PURE_SHELL:-}" = "1" ]; then - echo "๐Ÿš€ Starting fresh iOS simulator with clean state (${IOS_DEVICE})..." - output=$(ios.sh simulator start --pure ${IOS_DEVICE} 2>&1) - echo "$output" - - test_udid=$(echo "$output" | grep -oE '[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}' | tail -1) - if [ -n "$test_udid" ]; then - echo "$test_udid" > .devbox/virtenv/ios/runtime/simulator-udid.txt - echo "โœ“ Test simulator UDID saved: $test_udid" - fi + set -e - echo "โœ“ Test simulator ready for testing" + # Check if simulator already running + if ios.sh simulator ready 2>/dev/null; then + echo "Simulator already running, skipping launch" exit 0 - else - echo "๐Ÿš€ Starting iOS simulator (${IOS_DEVICE})..." - ios.sh simulator start ${IOS_DEVICE} - - echo "Waiting for simulator to boot..." - DEVICE_UDID=$(xcrun simctl list devices | grep "Booted" | grep -oE '[0-9A-F-]{36}' | head -1 || echo "") - if [ -z "$DEVICE_UDID" ]; then - echo "ERROR: No booted simulator found" - exit 1 - fi - xcrun simctl bootstatus "$DEVICE_UDID" >/dev/null 2>&1 || true + fi - echo "โœ“ Simulator ready for testing" - echo "โœ“ Simulator will remain running after tests complete" - exit 0 + echo "Starting iOS simulator..." + if [ "${DEVBOX_PURE_SHELL:-}" = "1" ]; then + # Pure mode: foreground, process-compose manages lifecycle + ios.sh simulator start --pure ${IOS_DEVICE} + tail -f /dev/null # Keep alive + else + # Dev mode: start simulator, wait for ready, then detach + ios.sh simulator start --wait-ready ${IOS_DEVICE} fi depends_on: sync-simulators: @@ -100,173 +99,155 @@ processes: restart: "no" readiness_probe: exec: - command: | - if [ "${DEVBOX_PURE_SHELL:-}" = "1" ]; then - DEVICE_UDID=$(xcrun simctl list devices | grep "Test" | grep "Booted" | grep -oE '[0-9A-F-]{36}' | head -1 || echo "") - else - DEVICE_UDID=$(xcrun simctl list devices | grep "Booted" | grep -oE '[0-9A-F-]{36}' | head -1 || echo "") - fi - [ -n "$DEVICE_UDID" ] && xcrun simctl bootstatus "$DEVICE_UDID" >/dev/null 2>&1 - initial_delay_seconds: 10 + command: "ios.sh simulator ready" + initial_delay_seconds: 5 period_seconds: 5 - timeout_seconds: 120 + timeout_seconds: 10 success_threshold: 1 - failure_threshold: 3 + failure_threshold: 36 + + # Phase 4b: Verify simulator ready + verify-simulator-ready: + command: | + _step="simulator-boot" + _step_dir="reports/steps" + mkdir -p "$_step_dir" + + # Wait a moment for launch to start + sleep 3 + + echo "Verifying simulator is ready..." + max_wait=180 + elapsed=0 + while ! ios.sh simulator ready 2>/dev/null; do + sleep 2 + elapsed=$((elapsed + 2)) + echo " Waiting for simulator... ${elapsed}s/${max_wait}s" + if [ $elapsed -ge $max_wait ]; then + printf 'fail\nTimed out after %ds waiting for simulator to boot\n' "$max_wait" > "$_step_dir/$_step.status" + exit 1 + fi + done + echo "Simulator ready" + + echo "pass" > "$_step_dir/$_step.status" + depends_on: + sync-simulators: + condition: process_completed_successfully + availability: + restart: "no" # Phase 5: Start Metro bundler metro-bundler: command: "metro.sh start ios" depends_on: - allocate-metro-port: - condition: process_completed_successfully build-node: condition: process_completed_successfully + allocate-metro-port: + condition: process_completed_successfully availability: restart: "no" - shutdown: - command: "metro.sh stop ios || true" - signal: 15 - timeout_seconds: 5 readiness_probe: exec: command: "metro.sh health ios ios" initial_delay_seconds: 5 period_seconds: 5 - timeout_seconds: 60 + timeout_seconds: 5 success_threshold: 1 + failure_threshold: 12 - # Phase 6: Build and deploy app with xcodebuild - deploy-ios: + # Phase 6a: Build iOS app (depends on port allocation, NOT metro running) + build-ios: command: | set -e + _step="build-ios" + _step_dir="$PWD/reports/steps" + mkdir -p "$_step_dir" + trap 'printf "fail\nExited with error (exit code $?)\n" > "$_step_dir/$_step.status"' ERR - # Set build configuration (default to Release for E2E tests) - BUILD_CONFIG="Release" - test -n "${IOS_BUILD_CONFIG:-}" && BUILD_CONFIG="$IOS_BUILD_CONFIG" - - # Get absolute project root path - PROJECT_ROOT="${DEVBOX_PROJECT_ROOT:-}" - test -z "$PROJECT_ROOT" && PROJECT_ROOT="$(pwd)" - - echo "๐Ÿ“ฒ Building React Native app" - echo "Build configuration: $BUILD_CONFIG" - echo "Project root: $PROJECT_ROOT" - - # Validate critical variables early - test -z "$BUILD_CONFIG" && { echo "ERROR: BUILD_CONFIG is empty!" >&2; exit 1; } - test -z "$PROJECT_ROOT" && { echo "ERROR: PROJECT_ROOT is empty!" >&2; exit 1; } + BUILD_CONFIG="${IOS_BUILD_CONFIG:-Release}" + echo "Building React Native app (${BUILD_CONFIG})" # Source Metro environment so React Native uses correct port . ${REACT_NATIVE_VIRTENV}/metro/env-ios.sh echo "Metro port: $METRO_PORT" - # Get simulator UDID (retry for up to 30 seconds) - max_attempts=15 - attempt=0 - DEVICE_UDID="" - - while [ $attempt -lt $max_attempts ] && [ -z "$DEVICE_UDID" ]; do - if [ "${DEVBOX_PURE_SHELL:-}" = "1" ]; then - DEVICE_UDID=$(xcrun simctl list devices | grep "Test" | grep "Booted" | grep -oE '[0-9A-F-]{36}' | head -1 || echo "") - else - DEVICE_UDID=$(xcrun simctl list devices | grep "Booted" | grep -oE '[0-9A-F-]{36}' | head -1 || echo "") - fi - - if [ -z "$DEVICE_UDID" ]; then - attempt=$((attempt + 1)) - echo "Waiting for simulator to boot... (attempt $attempt/$max_attempts)" - sleep 2 - fi - done - - if [ -z "$DEVICE_UDID" ]; then - echo "ERROR: No booted simulator found after $max_attempts attempts" >&2 - echo "Available devices:" >&2 - xcrun simctl list devices >&2 - exit 1 - fi - - echo "Building for simulator: $DEVICE_UDID" + # Set up DerivedData path + DERIVED_DATA="${DEVBOX_PROJECT_ROOT:-.}/.devbox/virtenv/ios/DerivedData" + mkdir -p "$DERIVED_DATA" # Build with xcodebuild - # Remove Nix-specific flags that interfere with Apple toolchain - # NODE_BINARY is set by React Native plugin init hook - # Pass RCT_METRO_PORT so the app connects to the correct Metro instance - - DERIVED_DATA_PATH="$PROJECT_ROOT/.devbox/virtenv/ios/DerivedData" - echo "Derived data path: $DERIVED_DATA_PATH" - - # Ensure the derived data directory exists - mkdir -p "$DERIVED_DATA_PATH" - - # Build with xcodebuild in a subshell to preserve variables - ( - cd ios - env -u LD -u LDFLAGS -u NIX_LDFLAGS -u NIX_CFLAGS_COMPILE -u NIX_CFLAGS_LINK \ - NODE_BINARY="$NODE_BINARY" \ - RCT_METRO_PORT="$METRO_PORT" \ - xcodebuild \ - -workspace ReactNativeExample.xcworkspace \ - -scheme ${IOS_APP_SCHEME} \ + cd ios + NODE_BINARY="$NODE_BINARY" RCT_METRO_PORT="$METRO_PORT" \ + xcodebuild -workspace ReactNativeExample.xcworkspace \ + -scheme ReactNativeExample \ -configuration "$BUILD_CONFIG" \ - -destination "id=$DEVICE_UDID" \ - -derivedDataPath "$DERIVED_DATA_PATH" \ + -destination "generic/platform=iOS Simulator" \ + -derivedDataPath "$DERIVED_DATA" \ -quiet \ build - ) - # Install the built app - echo "Installing app to simulator..." + echo "Build complete" - # Use Release build for E2E tests - APP_PATH="${DEVBOX_PROJECT_ROOT}/.devbox/virtenv/ios/DerivedData/Build/Products/Release-iphonesimulator/ReactNativeExample.app" - - if [ ! -d "$APP_PATH" ]; then - echo "ERROR: App bundle not found at $APP_PATH" >&2 - echo "ERROR: Contents of Products directory:" >&2 - ls -la "${DEVBOX_PROJECT_ROOT}/.devbox/virtenv/ios/DerivedData/Build/Products/" >&2 || true - exit 1 - fi + echo "pass" > "$_step_dir/$_step.status" + depends_on: + install-pods: + condition: process_completed_successfully + allocate-metro-port: + condition: process_completed_successfully + availability: + restart: "no" - xcrun simctl install "$DEVICE_UDID" "$APP_PATH" + # Phase 6b: Deploy iOS app + deploy-ios: + command: | + set -e + _step="deploy" + _step_dir="reports/steps" + mkdir -p "$_step_dir" + trap 'printf "fail\nExited with error (exit code $?)\n" > "$_step_dir/$_step.status"' ERR - # Launch app with Metro port environment variable - echo "Launching app with Metro on port $METRO_PORT..." - xcrun simctl launch "$DEVICE_UDID" "${IOS_APP_BUNDLE_ID}" \ - RCT_METRO_PORT="$METRO_PORT" + ios.sh deploy + echo "App deployed and launched successfully" - echo "โœ“ App deployed and launched successfully" + echo "pass" > "$_step_dir/$_step.status" depends_on: - install-pods: + build-ios: condition: process_completed_successfully - ios-simulator: + verify-simulator-ready: condition: process_completed_successfully + metro-bundler: + condition: process_healthy availability: restart: "no" # Phase 7: Verify app is running verify-app-running: command: | - if [ "${DEVBOX_PURE_SHELL:-}" = "1" ]; then - DEVICE_UDID=$(xcrun simctl list devices | grep "Test" | grep "Booted" | grep -oE '[0-9A-F-]{36}' | head -1) - else - DEVICE_UDID=$(xcrun simctl list devices | grep "Booted" | grep -oE '[0-9A-F-]{36}' | head -1) - fi + _step="verify-app" + _step_dir="reports/steps" + mkdir -p "$_step_dir" echo "Waiting for app to start..." - max_attempts=10 + + max_attempts=15 attempt=0 while [ $attempt -lt $max_attempts ]; do - if xcrun simctl spawn "$DEVICE_UDID" launchctl list | grep -q "${IOS_APP_BUNDLE_ID}"; then - echo "โœ“ App is running: ${IOS_APP_BUNDLE_ID}" - exit 0 + # Check if app process is running using built-in command + if ! ios.sh app status; then + attempt=$((attempt + 1)) + echo " Attempt $attempt/$max_attempts: App not running yet..." + sleep 2 + continue fi - attempt=$((attempt + 1)) - echo " Attempt $attempt/$max_attempts..." - sleep 2 + + # Success: app is running + echo "App is running" + echo "pass" > "$_step_dir/$_step.status" + exit 0 done - echo "ERROR: App did not start within timeout" >&2 + printf 'fail\nApp did not start within timeout\n' > "$_step_dir/$_step.status" exit 1 depends_on: deploy-ios: @@ -277,50 +258,36 @@ processes: # Cleanup - runs after verify completes cleanup: command: | - # Stop Metro bundler explicitly - echo "Stopping Metro bundler..." - metro.sh stop ios || true - - # Also kill the metro.sh wrapper process - pkill -f "metro.sh start ios" || true - if [ "${DEVBOX_PURE_SHELL:-}" = "1" ]; then - echo "๐Ÿงน Cleaning up (pure mode)..." - - test_sim_udid=$(cat .devbox/virtenv/ios/runtime/simulator-udid.txt 2>/dev/null || echo "") - - if [ -z "$test_sim_udid" ]; then - test_sim_udid=$(xcrun simctl list devices | grep "Test" | grep -oE '[0-9A-F-]{36}' | head -1 || echo "") - fi - - if [ -n "$test_sim_udid" ]; then - xcrun simctl terminate "$test_sim_udid" "${IOS_APP_BUNDLE_ID}" 2>/dev/null || true - xcrun simctl shutdown "$test_sim_udid" 2>/dev/null || true - xcrun simctl delete "$test_sim_udid" 2>/dev/null || true - rm -f .devbox/virtenv/ios/runtime/simulator-udid.txt 2>/dev/null || true - echo "โœ“ App stopped, test simulator deleted" - else - echo "โœ“ No test simulator to clean up" - fi + echo "Cleaning up (pure mode)..." + metro.sh stop ios || true + ios.sh app stop || true + ios.sh simulator stop + echo "Metro, app, and test simulator stopped" else - echo "โœ“ Test complete - app and simulator still running" + echo "Test complete - Metro, app, and simulator still running for iteration" fi depends_on: verify-app-running: condition: process_completed - metro-bundler: - condition: process_healthy availability: restart: "no" - # Summary - displays results and optionally stays open + # Summary - reports per-step pass/fail summary: - command: "bash tests/test-summary.sh 'React Native iOS E2E Test Suite' 'reports/react-native-ios-e2e-logs'" + command: | + set -e + export REPO_ROOT="$(cd ../.. && pwd)" + . "$REPO_ROOT/plugins/tests/test-framework.sh" + log_test "React Native iOS E2E Test Suite" + e2e_report_steps || true + test_summary "rn-ios-e2e" depends_on: cleanup: condition: process_completed availability: restart: "no" + exit_on_end: true shutdown: signal: 15 timeout_seconds: 1 diff --git a/examples/react-native/tests/test-suite-web-e2e.yaml b/examples/react-native/tests/test-suite-web-e2e.yaml index 098cf15..616edb4 100644 --- a/examples/react-native/tests/test-suite-web-e2e.yaml +++ b/examples/react-native/tests/test-suite-web-e2e.yaml @@ -3,26 +3,26 @@ version: "0.5" environment: - "TEST_TUI=${TEST_TUI:-false}" - "WEB_PORT=8081" - - "TEST_TIMEOUT=60" log_location: "reports/react-native-web-e2e-logs" log_level: info -is_strict: false +is_strict: true processes: # Phase 0: Allocate Metro port allocate-metro-port: command: | + set -e # Source React Native lib . ${REACT_NATIVE_VIRTENV}/scripts/lib/lib.sh # Allocate port for web test suite metro_port=$(rn_allocate_metro_port "web") - echo "๐Ÿ“ก Allocated Metro port: $metro_port" + echo "Allocated Metro port: $metro_port" # Save environment file for other processes env_file=$(rn_save_metro_env "web" "$metro_port") - echo "โœ“ Metro environment saved to: $env_file" + echo "Metro environment saved to: $env_file" availability: restart: "no" @@ -31,16 +31,33 @@ processes: depends_on: allocate-metro-port: condition: process_completed_successfully - command: "devbox run --pure build:node" + command: | + set -e + _step="build-node" + _step_dir="reports/steps" + mkdir -p "$_step_dir" + trap 'printf "fail\nExited with error (exit code $?)\n" > "$_step_dir/$_step.status"' ERR + + npm install + + echo "pass" > "$_step_dir/$_step.status" availability: restart: "no" # Phase 2: Build web bundle directory build-web: command: | - echo '๐Ÿ“ฆ Creating web build directory...' + set -e + _step="build-web" + _step_dir="reports/steps" + mkdir -p "$_step_dir" + trap 'printf "fail\nExited with error (exit code $?)\n" > "$_step_dir/$_step.status"' ERR + + echo 'Creating web build directory...' mkdir -p web/build - echo 'โœ“ Web build directory ready' + echo 'Web build directory ready' + + echo "pass" > "$_step_dir/$_step.status" depends_on: build-node: condition: process_completed_successfully @@ -57,79 +74,94 @@ processes: condition: process_completed_successfully availability: restart: "no" - shutdown: - command: "metro.sh stop web || true" - signal: 15 - timeout_seconds: 5 readiness_probe: exec: command: "metro.sh health web ios" initial_delay_seconds: 5 period_seconds: 5 - timeout_seconds: 60 + timeout_seconds: 5 success_threshold: 1 + failure_threshold: 12 - # Phase 4: Open browser with web bundle - open-browser: + # Phase 4: Verify web app is being served + verify-web-serving: command: | + _step="verify-web" + _step_dir="reports/steps" + mkdir -p "$_step_dir" + # Source Metro environment to get correct port . ${REACT_NATIVE_VIRTENV}/metro/env-web.sh - echo "๐ŸŒ Opening browser to Metro web bundle..." - echo " URL: http://localhost:$METRO_PORT" - - # Open default browser - if command -v open >/dev/null 2>&1; then - # macOS - open "http://localhost:$METRO_PORT" - elif command -v xdg-open >/dev/null 2>&1; then - # Linux - xdg-open "http://localhost:$METRO_PORT" - elif command -v start >/dev/null 2>&1; then - # Windows (Git Bash/WSL) - start "http://localhost:$METRO_PORT" - else - echo "โš  Could not detect browser launcher" - echo " Please open: http://localhost:$METRO_PORT" - fi - - echo "โœ“ Browser launched" - echo "" - echo "Metro is running at: http://localhost:$METRO_PORT" - echo "Press Ctrl+C to stop Metro and exit" + echo "Verifying web app is being served on port $METRO_PORT..." + + # Use node to make HTTP request (curl may not be available in --pure) + node -e " + const http = require('http'); + const url = 'http://localhost:' + process.env.METRO_PORT; + http.get(url, (res) => { + let data = ''; + res.on('data', (chunk) => data += chunk); + res.on('end', () => { + if (res.statusCode === 200) { + console.log('Web app is being served (HTTP ' + res.statusCode + ')'); + console.log(' Response size: ' + data.length + ' bytes'); + require('fs').mkdirSync('reports/steps', { recursive: true }); + require('fs').writeFileSync('reports/steps/verify-web.status', 'pass\n'); + process.exit(0); + } else { + console.error('ERROR: Unexpected status code: ' + res.statusCode); + require('fs').mkdirSync('reports/steps', { recursive: true }); + require('fs').writeFileSync('reports/steps/verify-web.status', 'fail\nUnexpected HTTP status: ' + res.statusCode + '\n'); + process.exit(1); + } + }); + }).on('error', (err) => { + console.error('ERROR: Could not connect to Metro: ' + err.message); + require('fs').mkdirSync('reports/steps', { recursive: true }); + require('fs').writeFileSync('reports/steps/verify-web.status', 'fail\nCould not connect to Metro: ' + err.message + '\n'); + process.exit(1); + }); + " depends_on: metro-bundler: condition: process_healthy + build-web: + condition: process_completed_successfully availability: restart: "no" # Cleanup cleanup: command: | - # Stop Metro bundler explicitly - echo "Stopping Metro bundler..." - metro.sh stop web || true - - # Also kill the metro.sh wrapper process - pkill -f "metro.sh start web" || true - - echo "โœ“ Test complete" + if [ "${DEVBOX_PURE_SHELL:-}" = "1" ]; then + echo "Cleaning up (pure mode)..." + metro.sh stop web || true + echo "Metro stopped" + else + echo "Test complete - Metro still running for iteration" + fi depends_on: - open-browser: + verify-web-serving: condition: process_completed - metro-bundler: - condition: process_healthy availability: restart: "no" - # Summary - displays results and optionally stays open + # Summary - reports per-step pass/fail summary: - command: "bash tests/test-summary.sh 'React Native Web E2E Test Suite' 'reports/react-native-web-e2e-logs'" + command: | + set -e + export REPO_ROOT="$(cd ../.. && pwd)" + . "$REPO_ROOT/plugins/tests/test-framework.sh" + log_test "React Native Web E2E Test Suite" + e2e_report_steps || true + test_summary "rn-web-e2e" depends_on: cleanup: condition: process_completed availability: restart: "no" + exit_on_end: true shutdown: signal: 15 timeout_seconds: 1 diff --git a/examples/react-native/tests/test-summary.sh b/examples/react-native/tests/test-summary.sh deleted file mode 100755 index cdf07a9..0000000 --- a/examples/react-native/tests/test-summary.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash -# Shared test summary script for process-compose test suites -# Usage: test-summary.sh "Test Suite Name" "path/to/logs" - -set -euo pipefail - -SUITE_NAME="${1:-Test Suite}" -LOG_PATH="${2:-reports/logs}" - -echo "" -echo "====================================" -echo "${SUITE_NAME} Summary" -echo "====================================" -echo "" -echo "Test Logs:" -echo " ${LOG_PATH}" -echo "" -echo "All tests completed!" -echo "====================================" - -# If TUI is enabled, sleep to keep results visible -if [ "${TEST_TUI:-false}" = "true" ] || [ "${TEST_TUI:-false}" = "1" ]; then - echo "" - echo "TUI mode: Waiting 30 seconds before exit (Ctrl+C to exit now)..." - sleep 30 - echo "Exiting..." -fi - -exit 0 diff --git a/plugins/android/REFERENCE.md b/plugins/android/REFERENCE.md index 3657a2c..4a4ad67 100644 --- a/plugins/android/REFERENCE.md +++ b/plugins/android/REFERENCE.md @@ -28,6 +28,8 @@ Configure the plugin by setting environment variables in `plugin.json`. These ar - `ANDROID_DEFAULT_DEVICE` โ€” Default device name when none specified - `ANDROID_SYSTEM_IMAGE_TAG` โ€” System image tag (e.g., "google_apis", "google_apis_playstore") - `ANDROID_APP_APK` โ€” Path or glob pattern for APK (relative to project root) +- `ANDROID_BUILD_CONFIG` โ€” Build configuration: Debug or Release (default: "Debug") +- `ANDROID_BUILD_TASK` โ€” Gradle task override (empty = auto-derive from config, e.g., assembleDebug) - `ANDROID_BUILD_TOOLS_VERSION` โ€” Build tools version (e.g., "36.1.0") - `ANDROID_INCLUDE_NDK` โ€” Include Android NDK in SDK (true/false, default: false) - `ANDROID_NDK_VERSION` โ€” NDK version when enabled (e.g., "27.0.12077973") @@ -48,23 +50,64 @@ Configure the plugin by setting environment variables in `plugin.json`. These ar - `devbox run --pure android.sh emulator start [--pure] [device]` - `--pure`: Start fresh emulator with wiped data (clean Android OS state for deterministic tests) - Without `--pure`: Reuses existing emulator if running (faster for development, preserves data) + - Auto-detects pure mode when `DEVBOX_PURE_SHELL=1` (set by `devbox run --pure`) + - `REUSE_EMU=1`: Override pure mode to reuse existing emulator (e.g., `devbox run --pure -e REUSE_EMU=1`) - `devbox run --pure android.sh emulator stop` +- `devbox run --pure android.sh emulator ready` + - Silent readiness probe: exit 0 if emulator is booted, exit 1 if not + - Reads emulator serial from suite-namespaced state file + - Checks `adb -s $serial shell getprop sys.boot_completed` - `devbox run --pure android.sh emulator reset [device]` **Convenience aliases:** -- `devbox run --pure start-emu [device]` (equivalent to `android.sh emulator start` without `--pure`) -- `devbox run --pure stop-emu` (equivalent to `android.sh emulator stop`) +- `devbox run --pure start:emu [device]` (equivalent to `android.sh emulator start` without `--pure`) +- `devbox run --pure stop:emu` (equivalent to `android.sh emulator stop`) **Behavior:** - Without `--pure`: Checks if an emulator with the same AVD is already running and reuses it - With `--pure`: Always starts a new emulator instance with `-wipe-data` flag (fresh Android OS) +- Emulator serial is saved to `$ANDROID_RUNTIME_DIR/${SUITE_NAME:-default}/emulator-serial.txt` + +### Deploy + +```bash +android.sh deploy [apk_path] +``` +- Installs and launches an app on an already-running emulator (no build, no emulator start) +- If `apk_path` is provided, installs the specified APK +- If no arguments, auto-detects APK using the same resolution as `run` +- Reads emulator serial from suite-namespaced state file +- Saves app ID and activity to state files for use by `app status` and `app stop` + +### App Lifecycle + +```bash +android.sh app status +``` +- Checks if the deployed app is running on the emulator +- Exit 0 if running, exit 1 if not +- Reads app ID and emulator serial from suite-namespaced state files + +```bash +android.sh app stop +``` +- Stops the deployed app via `adb shell am force-stop` +- Reads app ID and emulator serial from suite-namespaced state files ### Run app - `devbox run start:app [apk_path] [device]` - Builds, installs, and launches the app on the emulator - If `apk_path` is provided, skips build step and installs provided APK - - If no arguments, builds project and installs APK matched by `ANDROID_APP_APK` + - If no arguments, builds project and auto-detects APK + +**APK resolution precedence (when no explicit path):** + +1. `ANDROID_APP_APK` env var โ€” glob resolved relative to project root +2. Recursive search of project root for `*.apk` files (excludes .gradle/, build/intermediates/, node_modules/, .devbox/) +3. Recursive search of `$PWD` if different from project root (same exclusions) + +**Build script detection:** Tries `build:android` first, then falls back to `build`. Define a build script in `devbox.json` using native tools (e.g., `gradle assembleDebug`). ### Device management @@ -93,7 +136,7 @@ Configure the plugin by setting environment variables in `plugin.json`. These ar ### Device selection - `ANDROID_DEFAULT_DEVICE` - Default device name when none specified (set in devbox.json) - `ANDROID_DEVICES` - Device names to evaluate in flake (comma-separated, empty = all; set in devbox.json) -- `ANDROID_DEVICE_NAME` - Override device selection at runtime (e.g., `ANDROID_DEVICE_NAME=min devbox run start-emu`) +- `ANDROID_DEVICE_NAME` - Override device selection at runtime (e.g., `ANDROID_DEVICE_NAME=min devbox run start:emu`) - `TARGET_DEVICE` - Alias for ANDROID_DEVICE_NAME (legacy, prefer ANDROID_DEVICE_NAME) ### Emulator configuration @@ -110,5 +153,12 @@ Configure the plugin by setting environment variables in `plugin.json`. These ar - Or set in test suite environment sections (process-compose spawns new shells) - Cannot be set within script definitions (too late, init hook already ran) +### Runtime state +- `ANDROID_RUNTIME_DIR` - Directory for runtime state files (default: `.devbox/virtenv/android`) +- `SUITE_NAME` - Test suite name for state isolation (default: "default") + - Each suite gets its own subdirectory under `$ANDROID_RUNTIME_DIR/$SUITE_NAME/` + - State files: `emulator-serial.txt`, `app-id.txt`, `app-activity.txt` + - Set in process-compose environment blocks for parallel test execution + ### App configuration -- `ANDROID_APP_APK` - Path or glob pattern for APK (relative to project root) +- `ANDROID_APP_APK` - Path or glob pattern for APK (relative to project root; empty = auto-detect) diff --git a/plugins/android/plugin.json b/plugins/android/plugin.json index 88423e9..23956dc 100644 --- a/plugins/android/plugin.json +++ b/plugins/android/plugin.json @@ -1,6 +1,6 @@ { "name": "android", - "version": "0.0.2", + "version": "0.0.3", "description": "Sets Android home/AVD paths inside the Devbox virtenv for reproducible, project-local Android tooling.", "env": { "ANDROID_USER_HOME": "{{ .Virtenv }}/android", @@ -22,6 +22,8 @@ "ANDROID_INCLUDE_CMAKE": "false", "ANDROID_CMAKE_VERSION": "3.22.1", "ANDROID_CMDLINE_TOOLS_VERSION": "19.0", + "ANDROID_BUILD_CONFIG": "Debug", + "ANDROID_BUILD_TASK": "", "ANDROID_DISABLE_SNAPSHOTS": "0", "ANDROID_SKIP_SETUP": "0", "REPORTS_DIR": "reports" diff --git a/plugins/android/virtenv/flake.nix b/plugins/android/virtenv/flake.nix index 9a25668..42244dc 100644 --- a/plugins/android/virtenv/flake.nix +++ b/plugins/android/virtenv/flake.nix @@ -51,8 +51,15 @@ then map (device: device.api) lockData.devices else [ 36 ]; # Default to latest stable API + # Include ANDROID_COMPILE_SDK in platform versions if set (for projects + # that compile against a different API than the emulator/device targets) + compileSdkApis = + if builtins.hasAttr "ANDROID_COMPILE_SDK" defaultsData + then [ (toString defaultsData.ANDROID_COMPILE_SDK) ] + else []; + androidSdkConfig = { - platformVersions = unique (map toString deviceApis); + platformVersions = unique ((map toString deviceApis) ++ compileSdkApis); buildToolsVersion = getVar "ANDROID_BUILD_TOOLS_VERSION"; cmdLineToolsVersion = getVar "ANDROID_CMDLINE_TOOLS_VERSION"; systemImageTypes = [ (getVar "ANDROID_SYSTEM_IMAGE_TAG") ]; diff --git a/plugins/android/virtenv/scripts/domain/avd-reset.sh b/plugins/android/virtenv/scripts/domain/avd-reset.sh index 6bd576b..423f3eb 100644 --- a/plugins/android/virtenv/scripts/domain/avd-reset.sh +++ b/plugins/android/virtenv/scripts/domain/avd-reset.sh @@ -2,7 +2,7 @@ # Android Plugin - AVD Reset and Cleanup Operations # Extracted from avd.sh to eliminate circular dependencies -set -eu +set -e if ! (return 0 2>/dev/null); then echo "ERROR: avd_reset.sh must be sourced, not executed directly" >&2 diff --git a/plugins/android/virtenv/scripts/domain/avd.sh b/plugins/android/virtenv/scripts/domain/avd.sh index 17e5230..e56d2b9 100644 --- a/plugins/android/virtenv/scripts/domain/avd.sh +++ b/plugins/android/virtenv/scripts/domain/avd.sh @@ -2,7 +2,7 @@ # Android Plugin - AVD Manager Operations # Extracted from avd.sh to eliminate circular dependencies -set -eu +set -e if ! (return 0 2>/dev/null); then echo "ERROR: avd_manager.sh must be sourced, not executed directly" >&2 diff --git a/plugins/android/virtenv/scripts/domain/deploy.sh b/plugins/android/virtenv/scripts/domain/deploy.sh index 47151d2..4be555d 100644 --- a/plugins/android/virtenv/scripts/domain/deploy.sh +++ b/plugins/android/virtenv/scripts/domain/deploy.sh @@ -2,7 +2,7 @@ # Android Plugin - Application Run # See SCRIPTS.md for detailed documentation -set -eu +set -e if ! (return 0 2>/dev/null); then echo "ERROR: deploy.sh must be sourced, not executed directly" >&2 @@ -35,53 +35,111 @@ android_run_build() { echo "Building Android project: $project_root" - # Try platform-specific build command first (for React Native), then fall back to generic build + # Try platform-specific build command first, then generic if (cd "$project_root" && devbox run --list 2>/dev/null | grep -q "build:android"); then (cd "$project_root" && devbox run --pure build:android) - else + elif (cd "$project_root" && devbox run --list 2>/dev/null | grep -q "build"); then (cd "$project_root" && devbox run --pure build) + else + android_log_error "deploy.sh" "No build:android or build script found in devbox.json." + android_log_error "deploy.sh" "Define a build script using native tools (e.g., gradle assembleDebug)." + return 1 fi } # Resolve APK path from glob pattern -android_resolve_apk_path() { - project_root="$1" - apk_pattern="$2" +# Args: project_root, apk_pattern +# Returns: first matching APK path +android_resolve_apk_glob() { + _arg_root="$1" + _arg_pattern="$2" - if [ -z "$apk_pattern" ]; then + if [ -z "$_arg_pattern" ]; then return 1 fi # Make pattern absolute if it's relative - if [ "${apk_pattern#/}" = "$apk_pattern" ]; then - apk_pattern="${project_root%/}/$apk_pattern" + if [ "${_arg_pattern#/}" = "$_arg_pattern" ]; then + _arg_pattern="${_arg_root%/}/$_arg_pattern" fi - # Find matching APK files - # Temporarily disable glob failure to check if any files match set +f - matched_apks="" - for apk_candidate in $apk_pattern; do - if [ -f "$apk_candidate" ]; then - matched_apks="${matched_apks}${matched_apks:+ -}$apk_candidate" + _matched="" + for _candidate in $_arg_pattern; do + if [ -f "$_candidate" ]; then + _matched="${_matched}${_matched:+ +}$_candidate" fi done set -f - if [ -z "$matched_apks" ]; then + if [ -z "$_matched" ]; then return 1 fi - # Count matches - match_count="$(printf '%s\n' "$matched_apks" | wc -l | tr -d ' ')" - if [ "$match_count" -gt 1 ]; then - echo "WARNING: Multiple APKs matched pattern: $apk_pattern" >&2 - echo " Using first match" >&2 + _count="$(printf '%s\n' "$_matched" | wc -l | tr -d ' ')" + if [ "$_count" -gt 1 ]; then + android_log_warn "deploy.sh" "Multiple APKs matched pattern: $_arg_pattern; using first match" fi - # Return first match - printf '%s\n' "$matched_apks" | head -n1 + printf '%s\n' "$_matched" | head -n1 +} + +# Find APK using auto-detect precedence chain +# Args: project_root +# Precedence: +# 1. ANDROID_APP_APK env var (glob resolved relative to project_root) +# 2. Recursive search of project_root for *.apk +# 3. Recursive search of $PWD (skipped if PWD == project_root) +# 4. Error with guidance +android_find_apk() { + _find_root="$1" + + # 1. ANDROID_APP_APK env var + if [ -n "${ANDROID_APP_APK:-}" ]; then + _apk="$(android_resolve_apk_glob "$_find_root" "$ANDROID_APP_APK" || true)" + if [ -n "$_apk" ] && [ -f "$_apk" ]; then + android_log_info "deploy.sh" "APK resolved via ANDROID_APP_APK env var: $_apk" + printf '%s\n' "$_apk" + return 0 + fi + fi + + # 2. Recursive search of project_root + _apk="$(find "$_find_root" -name '*.apk' -type f \ + -not -path '*/.gradle/*' \ + -not -path '*/build/intermediates/*' \ + -not -path '*/node_modules/*' \ + -not -path '*/.devbox/*' \ + 2>/dev/null | head -n1)" + if [ -n "$_apk" ] && [ -f "$_apk" ]; then + android_log_info "deploy.sh" "APK resolved via project search: $_apk" + printf '%s\n' "$_apk" + return 0 + fi + + # 3. Recursive search of $PWD (skip if same as project_root) + _cwd="$(cd "$PWD" && pwd -P)" + _root_real="$(cd "$_find_root" && pwd -P)" + if [ "$_cwd" != "$_root_real" ]; then + _apk="$(find "$PWD" -name '*.apk' -type f \ + -not -path '*/.gradle/*' \ + -not -path '*/build/intermediates/*' \ + -not -path '*/node_modules/*' \ + -not -path '*/.devbox/*' \ + 2>/dev/null | head -n1)" + if [ -n "$_apk" ] && [ -f "$_apk" ]; then + android_log_info "deploy.sh" "APK resolved via directory search: $_apk" + printf '%s\n' "$_apk" + return 0 + fi + fi + + # 4. Error + android_log_error "deploy.sh" "No APK found. Searched: ANDROID_APP_APK env var, project root, current directory." + android_log_error "deploy.sh" "Set ANDROID_APP_APK in devbox.json env, or pass a path: android.sh run /path/to/app.apk" + android_log_error "deploy.sh" "See: plugins/android/REFERENCE.md for APK resolution details." + return 1 } # Find aapt tool from Android SDK (PATH > SDK/build-tools) @@ -200,7 +258,7 @@ android_install_apk() { echo "โœ“ APK installed" } -# Launch app on emulator (tries activity manager, falls back to monkey) +# Launch app on emulator via activity manager android_launch_app() { package_name="$1" activity_name="$2" @@ -213,16 +271,15 @@ android_launch_app() { android_debug_log "Launch component: $component_name" - # Try launching via activity manager - if adb -s "$emulator_serial" shell am start -n "$component_name" >/dev/null 2>&1; then - echo "โœ“ App launched via activity manager" - else - echo "WARNING: Activity manager launch failed, trying monkey launcher" >&2 - - # Fallback: Use monkey to launch via launcher intent - adb -s "$emulator_serial" shell monkey -p "$package_name" -c android.intent.category.LAUNCHER 1 >/dev/null 2>&1 || true + # Launch via activity manager + if ! adb -s "$emulator_serial" shell am start -n "$component_name" >/dev/null 2>&1; then + android_log_error "deploy.sh" "Failed to launch app: $component_name" + android_log_error "deploy.sh" "Verify the package name and activity are correct." + return 1 fi + echo "โœ“ App launched via activity manager" + # Wait a moment for the app process to start sleep 2 @@ -240,29 +297,39 @@ android_launch_app() { fi done - echo "WARNING: App process not detected after ${max_attempts} attempts" >&2 - echo " App may still have launched successfully" >&2 + android_log_error "deploy.sh" "App process not detected after ${max_attempts} attempts" + android_log_error "deploy.sh" "Check logcat for crash details: adb -s $emulator_serial logcat -d | grep $package_name" + return 1 } # Run Android app (build, install, launch) -# Usage: android_run_app [apk_path] [device] -# apk_path - Optional path to APK file. If provided, skips build step. -# device - Optional device name. If omitted, uses ANDROID_DEFAULT_DEVICE. +# Usage: android_run_app [--apk ] [--device ] [] +# --apk - Path to APK file. If provided, skips build step. +# --device - Device name. If omitted, uses ANDROID_DEFAULT_DEVICE. +# Bare positional arg is treated as device name for convenience. android_run_app() { - # Parse arguments - first arg could be APK path or device name apk_arg="" device_choice="" - if [ $# -gt 0 ]; then - # If first arg looks like a file path (contains / or ends with .apk), treat as APK - if printf '%s' "$1" | grep -q -e '/' -e '\.apk$'; then - apk_arg="$1" - shift - fi - fi - - # Remaining arg is device choice - device_choice="${1:-}" + while [ $# -gt 0 ]; do + case "$1" in + --apk) + apk_arg="${2:-}" + shift 2 + ;; + --device) + device_choice="${2:-}" + shift 2 + ;; + *) + # Bare positional arg: treat as device name for convenience + if [ -z "$device_choice" ]; then + device_choice="$1" + fi + shift + ;; + esac + done # ---- Resolve Device Selection ---- @@ -323,13 +390,9 @@ android_run_app() { echo "" echo "Locating APK..." - apk_pattern="${ANDROID_APP_APK:-app/build/outputs/apk/debug/*.apk}" - apk_path="$(android_resolve_apk_path "$project_root" "$apk_pattern" || true)" + apk_path="$(android_find_apk "$project_root" || true)" if [ -z "$apk_path" ] || [ ! -f "$apk_path" ]; then - echo "ERROR: Unable to locate APK" >&2 - echo " Pattern: $apk_pattern" >&2 - echo " Set ANDROID_APP_APK to correct path or pattern" >&2 exit 1 fi @@ -365,6 +428,13 @@ android_run_app() { echo "Deploying to: $emulator_serial" echo "" + # Stop and uninstall existing app to avoid signature conflicts + if adb -s "$emulator_serial" shell pm list packages 2>/dev/null | grep -q "package:${package_name}$"; then + echo "Removing existing install: $package_name" + adb -s "$emulator_serial" shell am force-stop "$package_name" 2>/dev/null || true + adb -s "$emulator_serial" uninstall "$package_name" >/dev/null 2>&1 || true + fi + android_install_apk "$apk_path" "$emulator_serial" echo "" android_launch_app "$package_name" "$activity_name" "$emulator_serial" diff --git a/plugins/android/virtenv/scripts/domain/emulator.sh b/plugins/android/virtenv/scripts/domain/emulator.sh index 103e0a2..7185922 100644 --- a/plugins/android/virtenv/scripts/domain/emulator.sh +++ b/plugins/android/virtenv/scripts/domain/emulator.sh @@ -2,7 +2,7 @@ # Android Plugin - Emulator Lifecycle Management # See SCRIPTS.md for detailed documentation -set -eu +set -e if ! (return 0 2>/dev/null); then echo "ERROR: emulator.sh must be sourced, not executed directly" >&2 @@ -233,6 +233,18 @@ android_start_emulator() { EMU_PORT="$available_port" export ANDROID_EMULATOR_SERIAL EMU_PORT + # Persist serial so readiness probes can find it (survives foreground blocking) + _emu_runtime_dir="${ANDROID_RUNTIME_DIR:-${ANDROID_USER_HOME:-}}" + if [ -n "$_emu_runtime_dir" ]; then + mkdir -p "$_emu_runtime_dir" + echo "$emulator_serial" > "$_emu_runtime_dir/emulator-serial.txt" + # Also write to suite-namespaced path if SUITE_NAME is set + if [ -n "${SUITE_NAME:-}" ]; then + mkdir -p "$_emu_runtime_dir/$SUITE_NAME" + echo "$emulator_serial" > "$_emu_runtime_dir/$SUITE_NAME/emulator-serial.txt" + fi + fi + # ---- Start Emulator ---- echo "" @@ -468,3 +480,35 @@ android_stop_emulator() { echo "โœ“ Emulators stopped" fi } + +# Check if the emulator is ready (boot completed) +# Returns 0 if emulator is booted, 1 otherwise +# Used by android.sh emulator start --wait-ready +android_emulator_ready() { + # Resolve serial from state directory (suite-namespaced) + _suite="${SUITE_NAME:-default}" + _runtime_dir="${ANDROID_RUNTIME_DIR:-${ANDROID_USER_HOME:-}}" + if [ -z "$_runtime_dir" ]; then + _runtime_dir="${PWD}/.devbox/virtenv" + fi + _state_dir="$_runtime_dir/$_suite" + + _serial="" + if [ -f "$_state_dir/emulator-serial.txt" ]; then + _serial="$(cat "$_state_dir/emulator-serial.txt")" + fi + + # Fallback to legacy location + if [ -z "$_serial" ] && [ -n "$_runtime_dir" ] && [ -f "$_runtime_dir/emulator-serial.txt" ]; then + _serial="$(cat "$_runtime_dir/emulator-serial.txt")" + fi + + if [ -z "$_serial" ]; then + return 1 + fi + + if adb -s "$_serial" shell getprop sys.boot_completed 2>/dev/null | grep -q "1"; then + return 0 + fi + return 1 +} diff --git a/plugins/android/virtenv/scripts/domain/validate.sh b/plugins/android/virtenv/scripts/domain/validate.sh index ea381f5..9207601 100644 --- a/plugins/android/virtenv/scripts/domain/validate.sh +++ b/plugins/android/virtenv/scripts/domain/validate.sh @@ -3,7 +3,7 @@ # See SCRIPTS.md for detailed documentation # Philosophy: Warn, don't block -set -eu +set -e # Validate that ANDROID_SDK_ROOT points to an existing directory (non-blocking) android_validate_sdk() { diff --git a/plugins/android/virtenv/scripts/init/init-hook.sh b/plugins/android/virtenv/scripts/init/init-hook.sh index eb7eca8..cc2254b 100755 --- a/plugins/android/virtenv/scripts/init/init-hook.sh +++ b/plugins/android/virtenv/scripts/init/init-hook.sh @@ -128,12 +128,9 @@ if [ -d "$DEVICES_DIR" ]; then checksum="" fi - timestamp="$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date +%Y-%m-%dT%H:%M:%SZ)" - echo "$devices_array" | jq \ --arg cs "$checksum" \ - --arg ts "$timestamp" \ - '{devices: ., checksum: $cs, generated_at: $ts}' \ + '{devices: ., checksum: $cs}' \ > "$DEVICES_LOCK" 2>&1 && cp "$DEVICES_LOCK" "$VIRTENV_DEVICES_LOCK" 2>/dev/null || true fi diff --git a/plugins/android/virtenv/scripts/init/setup.sh b/plugins/android/virtenv/scripts/init/setup.sh index 09f9a65..d9c6941 100644 --- a/plugins/android/virtenv/scripts/init/setup.sh +++ b/plugins/android/virtenv/scripts/init/setup.sh @@ -1,7 +1,9 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh # Android Plugin - Shell Initialization # Sets up environment when user runs 'devbox shell' # Config files are generated by init-hook.sh before this is sourced +# NOTE: This file is sourced (not executed) by devbox init_hook, +# so it must be POSIX sh compatible (no bash-isms). # Debug logging if [ -n "${ANDROID_DEBUG_SETUP:-}" ]; then @@ -33,7 +35,7 @@ ANDROID_ENV_LOADED=1 ANDROID_ENV_LOADED_PID="$$" export ANDROID_ENV_LOADED ANDROID_ENV_LOADED_PID -set -eu +set -e # ============================================================================ # Source Dependencies diff --git a/plugins/android/virtenv/scripts/lib/lib.sh b/plugins/android/virtenv/scripts/lib/lib.sh index 46bb936..4ba4d1e 100644 --- a/plugins/android/virtenv/scripts/lib/lib.sh +++ b/plugins/android/virtenv/scripts/lib/lib.sh @@ -2,7 +2,7 @@ # Android Plugin - Core Utilities # See SCRIPTS.md for detailed documentation -set -eu +set -e if ! (return 0 2>/dev/null); then echo "ERROR: lib.sh must be sourced" >&2 diff --git a/plugins/android/virtenv/scripts/platform/core.sh b/plugins/android/virtenv/scripts/platform/core.sh index ac42d2c..efb1fd8 100644 --- a/plugins/android/virtenv/scripts/platform/core.sh +++ b/plugins/android/virtenv/scripts/platform/core.sh @@ -2,7 +2,7 @@ # Android Plugin - Core SDK and Environment Setup # Extracted from env.sh to eliminate circular dependencies -set -eu +set -e if ! (return 0 2>/dev/null); then echo "ERROR: core.sh must be sourced, not executed directly" >&2 @@ -104,7 +104,7 @@ resolve_flake_sdk_root() { [ -n "${ANDROID_DEBUG_SETUP:-}" ] && echo "[CORE-$$] Building SDK: path:${root}#${output}" >&2 sdk_out=$( nix --extra-experimental-features 'nix-command flakes' \ - build "path:${root}#${output}" --no-link --print-out-paths 2>&1 || true + build "path:${root}#${output}" --no-link --print-out-paths 2>/dev/null || true ) [ -n "${ANDROID_DEBUG_SETUP:-}" ] && echo "[CORE-$$] nix build returned: ${sdk_out:-(empty)}" >&2 diff --git a/plugins/android/virtenv/scripts/platform/device_config.sh b/plugins/android/virtenv/scripts/platform/device_config.sh index 3968b52..8ccc741 100644 --- a/plugins/android/virtenv/scripts/platform/device_config.sh +++ b/plugins/android/virtenv/scripts/platform/device_config.sh @@ -2,7 +2,7 @@ # Android Plugin - Device Configuration Management # Extracted from avd.sh to eliminate circular dependencies -set -eu +set -e if ! (return 0 2>/dev/null); then echo "ERROR: device_config.sh must be sourced, not executed directly" >&2 diff --git a/plugins/android/virtenv/scripts/user/android.sh b/plugins/android/virtenv/scripts/user/android.sh index 8bdb912..9c714a4 100755 --- a/plugins/android/virtenv/scripts/user/android.sh +++ b/plugins/android/virtenv/scripts/user/android.sh @@ -13,7 +13,12 @@ set -eu # ============================================================================ # Initialize Android Environment # ============================================================================ -# SDK setup happens in init hook via setup.sh +# Auto-setup SDK if not already done (e.g., when called from process-compose) +if [ -z "${ANDROID_SDK_ROOT:-}" ] && [ -n "${ANDROID_SCRIPTS_DIR:-}" ]; then + if [ -f "${ANDROID_SCRIPTS_DIR}/init/setup.sh" ]; then + . "${ANDROID_SCRIPTS_DIR}/init/setup.sh" + fi +fi # ============================================================================ # Usage and Help @@ -24,27 +29,37 @@ usage() { Usage: android.sh [args] Commands: + deploy [apk_path] Install and launch app on running emulator devices [args] Manage device definitions info Display resolved SDK information config Manage configuration emulator start [device] Start Android emulator emulator stop Stop running emulator + emulator ready Check if emulator is booted (readiness probe) emulator reset Reset all emulator AVDs - run [apk_path] [device] Build, install, and launch app on emulator + app status Check if deployed app is running + app stop Stop the deployed app + run [--apk path] [--device name] Start emulator, install, and launch app Examples: + android.sh deploy + android.sh deploy path/to/app.apk android.sh devices list android.sh devices create pixel_api28 --api 28 --device pixel android.sh info android.sh config show android.sh emulator start max android.sh emulator stop - android.sh run # Build, install, launch + android.sh emulator ready + android.sh app status + android.sh app stop + android.sh run # Start emulator, install, launch android.sh run max # Same, but on 'max' device - android.sh run path/to/app.apk # Install provided APK - android.sh run path/to/app.apk max # Install APK on 'max' device + android.sh run --apk path/to/app.apk # Install provided APK + android.sh run --apk app.apk --device max # Install APK on 'max' device Note: Configuration is managed via environment variables in devbox.json. +Note: Build your app with gradle directly (e.g., cd android && ./gradlew assembleDebug) USAGE exit 1 } @@ -79,11 +94,102 @@ ensure_lib_loaded() { fi } +# Derive suite-namespaced state directory +android_state_dir() { + _suite="${SUITE_NAME:-default}" + _runtime_dir="${ANDROID_RUNTIME_DIR:-${ANDROID_USER_HOME:-}}" + if [ -z "$_runtime_dir" ]; then + _runtime_dir="${PWD}/.devbox/virtenv" + fi + printf '%s/%s' "$_runtime_dir" "$_suite" +} + # ============================================================================ # Command Handlers # ============================================================================ case "$command_name" in + # -------------------------------------------------------------------------- + # deploy - Install and launch app on running emulator (no build, no emu start) + # -------------------------------------------------------------------------- + deploy) + ensure_lib_loaded + + deploy_script="${scripts_dir%/}/domain/deploy.sh" + if [ ! -f "$deploy_script" ]; then + echo "ERROR: domain/deploy.sh not found: $deploy_script" >&2 + exit 1 + fi + + # shellcheck source=/dev/null + . "$deploy_script" + + apk_arg="${1:-}" + state_dir="$(android_state_dir)" + + # Read serial from state file (suite-namespaced, then legacy, then env var) + emulator_serial="" + if [ -f "$state_dir/emulator-serial.txt" ]; then + emulator_serial="$(cat "$state_dir/emulator-serial.txt")" + fi + if [ -z "$emulator_serial" ]; then + runtime_dir="${ANDROID_RUNTIME_DIR:-${ANDROID_USER_HOME:-}}" + if [ -n "$runtime_dir" ] && [ -f "$runtime_dir/emulator-serial.txt" ]; then + emulator_serial="$(cat "$runtime_dir/emulator-serial.txt")" + fi + fi + if [ -z "$emulator_serial" ]; then + emulator_serial="${ANDROID_EMULATOR_SERIAL:-emulator-${EMU_PORT:-5554}}" + fi + + # Resolve APK path + if [ -n "$apk_arg" ]; then + apk_path="$apk_arg" + if [ "${apk_path#/}" = "$apk_path" ]; then + apk_path="$PWD/$apk_path" + fi + if [ ! -f "$apk_path" ]; then + echo "ERROR: APK not found: $apk_path" >&2 + exit 1 + fi + else + project_root="${DEVBOX_PROJECT_ROOT:-${DEVBOX_PROJECT_DIR:-${DEVBOX_WD:-$PWD}}}" + apk_path="$(android_find_apk "$project_root" || true)" + if [ -z "$apk_path" ] || [ ! -f "$apk_path" ]; then + echo "ERROR: No APK found. Build first (e.g., gradle assembleDebug) or define a build:android script in devbox.json." >&2 + exit 1 + fi + fi + + echo "APK: $(basename "$apk_path")" + + # Extract metadata + apk_metadata="$(android_extract_apk_metadata "$apk_path")" + package_name="$(printf '%s\n' "$apk_metadata" | sed -n '1p')" + activity_name="$(printf '%s\n' "$apk_metadata" | sed -n '2p')" + + echo "Package: $package_name" + echo "Activity: $activity_name" + + # Uninstall existing app to avoid signature conflicts + if adb -s "$emulator_serial" shell pm list packages 2>/dev/null | grep -q "package:${package_name}$"; then + echo "Removing existing install: $package_name" + adb -s "$emulator_serial" shell am force-stop "$package_name" 2>/dev/null || true + adb -s "$emulator_serial" uninstall "$package_name" >/dev/null 2>&1 || true + fi + + # Install and launch + android_install_apk "$apk_path" "$emulator_serial" + android_launch_app "$package_name" "$activity_name" "$emulator_serial" + + # Save state + mkdir -p "$state_dir" + echo "$package_name" > "$state_dir/app-id.txt" + echo "$activity_name" > "$state_dir/app-activity.txt" + + echo "Deploy complete" + ;; + # -------------------------------------------------------------------------- # devices - Delegate to devices.sh # -------------------------------------------------------------------------- @@ -204,6 +310,7 @@ case "$command_name" in start) # Parse flags and device name pure_mode=0 + wait_ready=0 device_name="" while [ $# -gt 0 ]; do @@ -212,6 +319,10 @@ case "$command_name" in pure_mode=1 shift ;; + --wait-ready) + wait_ready=1 + shift + ;; *) device_name="$1" shift @@ -219,6 +330,17 @@ case "$command_name" in esac done + # Auto-detect pure mode from devbox environment + if [ "${DEVBOX_PURE_SHELL:-}" = "1" ]; then + pure_mode=1 + fi + + # Allow overriding pure mode to reuse existing emulator + # Usage: devbox run --pure -e REUSE_EMU=1 android.sh emulator start + if [ "${REUSE_EMU:-}" = "1" ]; then + pure_mode=0 + fi + # Layer 3 orchestration: setup AVDs first, then start emulator if ! command -v android_setup_avds >/dev/null 2>&1; then echo "ERROR: android_setup_avds function not available" >&2 @@ -240,6 +362,31 @@ case "$command_name" in # Step 2: Start emulator (uses ANDROID_RESOLVED_AVD from setup) android_start_emulator "$device_name" + + # Step 3: Save serial to suite-namespaced state dir + state_dir="$(android_state_dir)" + mkdir -p "$state_dir" + if [ -n "${ANDROID_EMULATOR_SERIAL:-}" ]; then + echo "$ANDROID_EMULATOR_SERIAL" > "$state_dir/emulator-serial.txt" + fi + + # Step 4: If --wait-ready, wait for emulator to be ready and exit (detach mode for dev) + # Otherwise in pure mode, keep running (process-compose manages lifecycle) + if [ "$wait_ready" = "1" ]; then + echo "Waiting for emulator to be ready..." + max_wait=120 + elapsed=0 + while ! android_emulator_ready; do + sleep 3 + elapsed=$((elapsed + 3)) + if [ $elapsed -ge $max_wait ]; then + echo "ERROR: Emulator did not become ready within ${max_wait}s" >&2 + exit 1 + fi + done + echo "โœ“ Emulator ready and running in background" + exit 0 + fi ;; stop) @@ -251,6 +398,33 @@ case "$command_name" in fi ;; + ready) + # Silent readiness probe: exit 0 if booted, 1 if not + state_dir="$(android_state_dir)" + serial="" + + if [ -f "$state_dir/emulator-serial.txt" ]; then + serial="$(cat "$state_dir/emulator-serial.txt")" + fi + + # Fallback to legacy location + if [ -z "$serial" ]; then + runtime_dir="${ANDROID_RUNTIME_DIR:-${ANDROID_USER_HOME:-}}" + if [ -n "$runtime_dir" ] && [ -f "$runtime_dir/emulator-serial.txt" ]; then + serial="$(cat "$runtime_dir/emulator-serial.txt")" + fi + fi + + if [ -z "$serial" ]; then + exit 1 + fi + + if adb -s "$serial" shell getprop sys.boot_completed 2>/dev/null | grep -q "1"; then + exit 0 + fi + exit 1 + ;; + reset) avd_reset_script="${scripts_dir%/}/domain/avd-reset.sh" if [ ! -f "$avd_reset_script" ]; then @@ -271,7 +445,97 @@ case "$command_name" in *) echo "ERROR: Unknown emulator subcommand: $subcommand" >&2 - echo "Usage: android.sh emulator [device]" >&2 + echo "Usage: android.sh emulator [device]" >&2 + exit 1 + ;; + esac + ;; + + # -------------------------------------------------------------------------- + # app - App lifecycle management + # -------------------------------------------------------------------------- + app) + subcommand="${1-}" + shift || true + + state_dir="$(android_state_dir)" + + case "$subcommand" in + status) + # Check if deployed app is running (exit 0 if running, 1 if not) + app_id="" + if [ -f "$state_dir/app-id.txt" ]; then + app_id="$(cat "$state_dir/app-id.txt")" + fi + # Fallback to legacy location + if [ -z "$app_id" ]; then + runtime_dir="${ANDROID_RUNTIME_DIR:-${ANDROID_USER_HOME:-}}" + if [ -n "$runtime_dir" ] && [ -f "$runtime_dir/app-id.txt" ]; then + app_id="$(cat "$runtime_dir/app-id.txt")" + fi + fi + if [ -z "$app_id" ]; then + exit 1 + fi + + serial="" + if [ -f "$state_dir/emulator-serial.txt" ]; then + serial="$(cat "$state_dir/emulator-serial.txt")" + fi + if [ -z "$serial" ]; then + runtime_dir="${ANDROID_RUNTIME_DIR:-${ANDROID_USER_HOME:-}}" + if [ -n "$runtime_dir" ] && [ -f "$runtime_dir/emulator-serial.txt" ]; then + serial="$(cat "$runtime_dir/emulator-serial.txt")" + fi + fi + if [ -z "$serial" ]; then + serial="emulator-${EMU_PORT:-5554}" + fi + + if adb -s "$serial" shell pidof "$app_id" >/dev/null 2>&1; then + exit 0 + fi + exit 1 + ;; + + stop) + # Stop the deployed app + app_id="" + if [ -f "$state_dir/app-id.txt" ]; then + app_id="$(cat "$state_dir/app-id.txt")" + fi + if [ -z "$app_id" ]; then + runtime_dir="${ANDROID_RUNTIME_DIR:-${ANDROID_USER_HOME:-}}" + if [ -n "$runtime_dir" ] && [ -f "$runtime_dir/app-id.txt" ]; then + app_id="$(cat "$runtime_dir/app-id.txt")" + fi + fi + + serial="" + if [ -f "$state_dir/emulator-serial.txt" ]; then + serial="$(cat "$state_dir/emulator-serial.txt")" + fi + if [ -z "$serial" ]; then + runtime_dir="${ANDROID_RUNTIME_DIR:-${ANDROID_USER_HOME:-}}" + if [ -n "$runtime_dir" ] && [ -f "$runtime_dir/emulator-serial.txt" ]; then + serial="$(cat "$runtime_dir/emulator-serial.txt")" + fi + fi + if [ -z "$serial" ]; then + serial="emulator-${EMU_PORT:-5554}" + fi + + if [ -n "$app_id" ]; then + adb -s "$serial" shell am force-stop "$app_id" 2>/dev/null || true + echo "App stopped: $app_id" + else + echo "No app to stop" + fi + ;; + + *) + echo "ERROR: Unknown app subcommand: $subcommand" >&2 + echo "Usage: android.sh app " >&2 exit 1 ;; esac @@ -279,23 +543,32 @@ case "$command_name" in # -------------------------------------------------------------------------- # run - Build, install, and launch app on emulator - # Usage: android.sh run [apk_path] [device] + # Usage: android.sh run [--apk ] [--device ] [device] # -------------------------------------------------------------------------- run) - # Parse arguments - first arg could be APK path or device name + # Parse arguments apk_arg="" device_name="" - if [ $# -gt 0 ]; then - # If first arg looks like a file path (contains / or ends with .apk), treat as APK - if printf '%s' "$1" | grep -q -e '/' -e '\.apk$'; then - apk_arg="$1" - shift - fi - fi - - # Remaining arg is device name - device_name="${1:-}" + while [ $# -gt 0 ]; do + case "$1" in + --apk) + apk_arg="${2:-}" + shift 2 + ;; + --device) + device_name="${2:-}" + shift 2 + ;; + *) + # Bare positional arg: treat as device name for convenience + if [ -z "$device_name" ]; then + device_name="$1" + fi + shift + ;; + esac + done # Source layer 3 dependencies avd_script="${scripts_dir%/}/domain/avd.sh" @@ -325,7 +598,7 @@ case "$command_name" in fi done - # Layer 4 orchestration: setup โ†’ start โ†’ run + # Layer 4 orchestration: setup -> start -> run echo "Setting up Android Virtual Devices..." android_setup_avds @@ -334,12 +607,16 @@ case "$command_name" in android_start_emulator "$device_name" echo "" - # Pass both APK (if provided) and device name to run + # Pass both APK (if provided) and device name to run with explicit flags + run_args="" if [ -n "$apk_arg" ]; then - android_run_app "$apk_arg" "$device_name" - else - android_run_app "$device_name" + run_args="--apk $apk_arg" + fi + if [ -n "$device_name" ]; then + run_args="$run_args --device $device_name" fi + # shellcheck disable=SC2086 + android_run_app $run_args ;; # -------------------------------------------------------------------------- diff --git a/plugins/android/virtenv/scripts/user/config.sh b/plugins/android/virtenv/scripts/user/config.sh index e6a88aa..82ae1cf 100755 --- a/plugins/android/virtenv/scripts/user/config.sh +++ b/plugins/android/virtenv/scripts/user/config.sh @@ -2,7 +2,7 @@ # Android Plugin - Configuration Management # See REFERENCE.md for detailed documentation -set -eu +set -e if ! (return 0 2>/dev/null); then echo "ERROR: config.sh must be sourced" >&2 diff --git a/plugins/android/virtenv/scripts/user/devices.sh b/plugins/android/virtenv/scripts/user/devices.sh index ab1437a..b14a86d 100755 --- a/plugins/android/virtenv/scripts/user/devices.sh +++ b/plugins/android/virtenv/scripts/user/devices.sh @@ -489,8 +489,7 @@ case "$command_name" in temp_lock_file="${lock_file_path}.tmp" printf '%s\n' "$devices_json" | jq \ --arg cs "$checksum" \ - --arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date +%Y-%m-%dT%H:%M:%SZ)" \ - 'map(del(.file)) | {devices: ., checksum: $cs, generated_at: $ts}' \ + 'map(del(.file)) | {devices: ., checksum: $cs}' \ > "$temp_lock_file" mv "$temp_lock_file" "$lock_file_path" diff --git a/plugins/devbox-mcp/README.md b/plugins/devbox-mcp/README.md index be9cc80..b1c2988 100644 --- a/plugins/devbox-mcp/README.md +++ b/plugins/devbox-mcp/README.md @@ -226,7 +226,3 @@ devbox_docs_read({ - Create at: https://github.com/settings/tokens - Requires: `public_repo` or `repo` read access - Configure via `GITHUB_TOKEN` environment variable in MCP config - -## License - -MIT diff --git a/plugins/devbox-mcp/package.json b/plugins/devbox-mcp/package.json index 08c8195..f636a1b 100644 --- a/plugins/devbox-mcp/package.json +++ b/plugins/devbox-mcp/package.json @@ -1,6 +1,6 @@ { "name": "devbox-mcp", - "version": "0.1.3", + "version": "0.1.4", "description": "Model Context Protocol server for Jetify's devbox development environment tool", "type": "module", "main": "src/index.js", @@ -16,7 +16,6 @@ "development-environment" ], "author": "", - "license": "MIT", "repository": { "type": "git", "url": "https://github.com/segment-integrations/devbox-plugins.git", diff --git a/plugins/devbox-mcp/src/index.js b/plugins/devbox-mcp/src/index.js index 734d527..888dbd7 100755 --- a/plugins/devbox-mcp/src/index.js +++ b/plugins/devbox-mcp/src/index.js @@ -6,6 +6,7 @@ import { ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { execFile } from "child_process"; +import { writeFile } from "fs/promises"; import { promisify } from "util"; const execFileAsync = promisify(execFile); @@ -217,7 +218,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { "- devbox.d/: Per-project configuration directory\n" + "- .devbox/virtenv/: Temporary runtime directory (auto-regenerated, never edit directly)\n\n" + "The .devbox/virtenv/ directory is automatically regenerated on 'devbox shell' or 'devbox run'. " + - "Any manual changes to files in .devbox/virtenv/ will be lost.", + "Any manual changes to files in .devbox/virtenv/ will be lost.\n\n" + + "OUTPUT MANAGEMENT: For commands that produce large output (builds, test suites, logs), " + + "use the 'logFile' parameter to write output to a file instead of returning it inline. " + + "This keeps context tokens low. The response will include the file path, exit status, " + + "and a short summary. You can then read the log file selectively if needed.", inputSchema: { type: "object", properties: { @@ -250,6 +255,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { description: "Timeout in milliseconds (default: 120000)", default: 120000, }, + logFile: { + type: "string", + description: + "Absolute path to write stdout+stderr to instead of returning inline. " + + "Use for commands with large output (builds, test suites) to avoid filling context. " + + "When set, the response returns a short summary with the log file path.", + }, }, required: ["command"], }, @@ -424,7 +436,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { switch (name) { case "devbox_run": { - const { command, args: cmdArgs = [], pure = false, env = {}, cwd, timeout } = args; + const { command, args: cmdArgs = [], pure = false, env = {}, cwd, timeout, logFile } = args; const devboxArgs = ["run"]; if (pure) devboxArgs.push("--pure"); @@ -441,6 +453,44 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { const result = await runDevbox(devboxArgs, { cwd, timeout }); + // If logFile is specified, write output to file and return summary + if (logFile) { + const fullOutput = [ + result.stdout || "", + result.stderr ? `\n--- stderr ---\n${result.stderr}` : "", + ].join(""); + + try { + await writeFile(logFile, fullOutput, "utf-8"); + } catch (writeErr) { + return { + content: [ + { + type: "text", + text: `โœ— Failed to write log file: ${writeErr.message}\n\nCommand ${result.success ? "succeeded" : `failed (exit ${result.exitCode})`}`, + }, + ], + isError: true, + }; + } + + const lines = fullOutput.split("\n"); + const lineCount = lines.length; + const tail = lines.slice(-5).join("\n"); + + return { + content: [ + { + type: "text", + text: result.success + ? `โœ“ Command succeeded (${lineCount} lines written to ${logFile})\n\nLast 5 lines:\n${tail}` + : `โœ— Command failed (exit ${result.exitCode}, ${lineCount} lines written to ${logFile})\n\nLast 5 lines:\n${tail}`, + }, + ], + isError: !result.success, + }; + } + return { content: [ { diff --git a/plugins/devbox-mcp/tests/test-server.sh b/plugins/devbox-mcp/tests/test-server.sh index 1216a94..00732d0 100755 --- a/plugins/devbox-mcp/tests/test-server.sh +++ b/plugins/devbox-mcp/tests/test-server.sh @@ -32,10 +32,10 @@ echo "Validating Node.js syntax..." # Change to repo root to use devbox run cd "${SCRIPT_DIR}/../.." || exit 1 if devbox run node --check plugins/devbox-mcp/src/index.js >/dev/null 2>&1; then - TEST_PASS=$((TEST_PASS + 1)) + test_passed=$((test_passed + 1)) echo "โœ“ Server JavaScript syntax is valid" else - TEST_FAIL=$((TEST_FAIL + 1)) + test_failed=$((test_failed + 1)) echo "โœ— Server JavaScript syntax has errors" fi cd "${SCRIPT_DIR}" || exit 1 @@ -45,10 +45,10 @@ echo "Checking required tools are defined..." required_tools="devbox_run devbox_list devbox_add devbox_info devbox_search devbox_docs_search devbox_docs_list devbox_docs_read devbox_init devbox_shell_env devbox_sync" for tool in $required_tools; do if grep -q "name: \"$tool\"" "${MCP_DIR}/src/index.js"; then - TEST_PASS=$((TEST_PASS + 1)) + test_passed=$((test_passed + 1)) echo "โœ“ Tool defined: $tool" else - TEST_FAIL=$((TEST_FAIL + 1)) + test_failed=$((test_failed + 1)) echo "โœ— Tool missing: $tool" fi done @@ -60,4 +60,4 @@ assert_file_contains "${MCP_DIR}/src/index.js" "StdioServerTransport" "Server im # Test 8: Check server has proper error handling assert_file_contains "${MCP_DIR}/src/index.js" "catch.*error" "Server has error handling" -test_summary +test_summary "devbox-mcp-server" diff --git a/plugins/devbox-mcp/tests/test-suite.yaml b/plugins/devbox-mcp/tests/test-suite.yaml index 0540deb..9b3aa18 100644 --- a/plugins/devbox-mcp/tests/test-suite.yaml +++ b/plugins/devbox-mcp/tests/test-suite.yaml @@ -1,7 +1,8 @@ version: "0.5" -log_location: "test-results/devbox-mcp-logs" +log_location: "reports/devbox-mcp-logs" log_level: info +is_strict: true processes: # Test server structure and configuration @@ -28,7 +29,7 @@ processes: availability: restart: "no" - # Summary process - displays test results + # Summary process - only runs when all tests pass summary: command: | echo "" @@ -37,9 +38,9 @@ processes: echo "====================================" echo "" echo "Test Logs:" - echo " test-results/devbox-mcp-logs" + echo " reports/devbox-mcp-logs" echo "" - echo "All tests completed!" + echo "All tests passed!" echo "====================================" # In TUI mode, delay 30 seconds so user can view summary before exit @@ -49,16 +50,12 @@ processes: sleep 30 echo "Exiting..." fi - exit 0 depends_on: - test-server: - condition: process_completed - test-tools: - condition: process_completed test-mcp-tools: - condition: process_completed + condition: process_completed_successfully availability: restart: "no" + exit_on_end: true shutdown: signal: 15 timeout_seconds: 1 diff --git a/plugins/devbox-mcp/tests/test-tools.sh b/plugins/devbox-mcp/tests/test-tools.sh index 47ef17f..1e8be98 100755 --- a/plugins/devbox-mcp/tests/test-tools.sh +++ b/plugins/devbox-mcp/tests/test-tools.sh @@ -21,10 +21,10 @@ fi # Test 1: Server starts without errors echo "Testing server startup (5 second timeout)..." if timeout 5 node "${MCP_DIR}/src/index.js" &1 | grep -q "Devbox MCP server running"; then - TEST_PASS=$((TEST_PASS + 1)) + test_passed=$((test_passed + 1)) echo "โœ“ Server starts successfully" else - TEST_FAIL=$((TEST_FAIL + 1)) + test_failed=$((test_failed + 1)) echo "โœ— Server failed to start" fi @@ -34,117 +34,117 @@ server_content="$(cat "${MCP_DIR}/src/index.js")" # devbox_run tool checks if echo "$server_content" | grep -q 'name: "devbox_run"'; then - TEST_PASS=$((TEST_PASS + 1)) + test_passed=$((test_passed + 1)) echo "โœ“ devbox_run tool defined" else - TEST_FAIL=$((TEST_FAIL + 1)) + test_failed=$((test_failed + 1)) echo "โœ— devbox_run tool not found" fi if echo "$server_content" | grep -q 'command:'; then - TEST_PASS=$((TEST_PASS + 1)) + test_passed=$((test_passed + 1)) echo "โœ“ devbox_run has command parameter" else - TEST_FAIL=$((TEST_FAIL + 1)) + test_failed=$((test_failed + 1)) echo "โœ— devbox_run missing command parameter" fi # devbox_list tool checks if echo "$server_content" | grep -q 'name: "devbox_list"'; then - TEST_PASS=$((TEST_PASS + 1)) + test_passed=$((test_passed + 1)) echo "โœ“ devbox_list tool defined" else - TEST_FAIL=$((TEST_FAIL + 1)) + test_failed=$((test_failed + 1)) echo "โœ— devbox_list tool not found" fi # devbox_add tool checks if echo "$server_content" | grep -q 'name: "devbox_add"'; then - TEST_PASS=$((TEST_PASS + 1)) + test_passed=$((test_passed + 1)) echo "โœ“ devbox_add tool defined" else - TEST_FAIL=$((TEST_FAIL + 1)) + test_failed=$((test_failed + 1)) echo "โœ— devbox_add tool not found" fi if echo "$server_content" | grep -q 'packages:'; then - TEST_PASS=$((TEST_PASS + 1)) + test_passed=$((test_passed + 1)) echo "โœ“ devbox_add has packages parameter" else - TEST_FAIL=$((TEST_FAIL + 1)) + test_failed=$((test_failed + 1)) echo "โœ— devbox_add missing packages parameter" fi # devbox_info tool checks if echo "$server_content" | grep -q 'name: "devbox_info"'; then - TEST_PASS=$((TEST_PASS + 1)) + test_passed=$((test_passed + 1)) echo "โœ“ devbox_info tool defined" else - TEST_FAIL=$((TEST_FAIL + 1)) + test_failed=$((test_failed + 1)) echo "โœ— devbox_info tool not found" fi # devbox_search tool checks if echo "$server_content" | grep -q 'name: "devbox_search"'; then - TEST_PASS=$((TEST_PASS + 1)) + test_passed=$((test_passed + 1)) echo "โœ“ devbox_search tool defined" else - TEST_FAIL=$((TEST_FAIL + 1)) + test_failed=$((test_failed + 1)) echo "โœ— devbox_search tool not found" fi # devbox_docs_search tool checks if echo "$server_content" | grep -q 'name: "devbox_docs_search"'; then - TEST_PASS=$((TEST_PASS + 1)) + test_passed=$((test_passed + 1)) echo "โœ“ devbox_docs_search tool defined" else - TEST_FAIL=$((TEST_FAIL + 1)) + test_failed=$((test_failed + 1)) echo "โœ— devbox_docs_search tool not found" fi if echo "$server_content" | grep -q 'maxResults'; then - TEST_PASS=$((TEST_PASS + 1)) + test_passed=$((test_passed + 1)) echo "โœ“ devbox_docs_search has maxResults parameter" else - TEST_FAIL=$((TEST_FAIL + 1)) + test_failed=$((test_failed + 1)) echo "โœ— devbox_docs_search missing maxResults parameter" fi # devbox_docs_list tool checks if echo "$server_content" | grep -q 'name: "devbox_docs_list"'; then - TEST_PASS=$((TEST_PASS + 1)) + test_passed=$((test_passed + 1)) echo "โœ“ devbox_docs_list tool defined" else - TEST_FAIL=$((TEST_FAIL + 1)) + test_failed=$((test_failed + 1)) echo "โœ— devbox_docs_list tool not found" fi # devbox_docs_read tool checks if echo "$server_content" | grep -q 'name: "devbox_docs_read"'; then - TEST_PASS=$((TEST_PASS + 1)) + test_passed=$((test_passed + 1)) echo "โœ“ devbox_docs_read tool defined" else - TEST_FAIL=$((TEST_FAIL + 1)) + test_failed=$((test_failed + 1)) echo "โœ— devbox_docs_read tool not found" fi if echo "$server_content" | grep -q 'filePath'; then - TEST_PASS=$((TEST_PASS + 1)) + test_passed=$((test_passed + 1)) echo "โœ“ devbox_docs_read has filePath parameter" else - TEST_FAIL=$((TEST_FAIL + 1)) + test_failed=$((test_failed + 1)) echo "โœ— devbox_docs_read missing filePath parameter" fi # Test 3: Check helper functions exist echo "Checking helper functions..." -helper_functions="runDevbox ensureDocsRepo searchDocs listDocs readDoc" +helper_functions="runDevbox fetchDocsList fetchRawContent searchDocs listDocs readDoc" for func in $helper_functions; do if echo "$server_content" | grep -q "function $func"; then - TEST_PASS=$((TEST_PASS + 1)) + test_passed=$((test_passed + 1)) echo "โœ“ $func helper function defined" else - TEST_FAIL=$((TEST_FAIL + 1)) + test_failed=$((test_failed + 1)) echo "โœ— $func helper function not found" fi done @@ -154,12 +154,12 @@ echo "Checking tool handlers..." tools="devbox_run devbox_list devbox_add devbox_info devbox_search devbox_docs_search devbox_docs_list devbox_docs_read devbox_init devbox_shell_env devbox_sync" for tool in $tools; do if echo "$server_content" | grep -q "case \"$tool\":"; then - TEST_PASS=$((TEST_PASS + 1)) + test_passed=$((test_passed + 1)) echo "โœ“ Handler exists for $tool" else - TEST_FAIL=$((TEST_FAIL + 1)) + test_failed=$((test_failed + 1)) echo "โœ— Handler missing for $tool" fi done -test_summary +test_summary "devbox-mcp-tools" diff --git a/plugins/ios/README.md b/plugins/ios/README.md index df7f124..9437d92 100644 --- a/plugins/ios/README.md +++ b/plugins/ios/README.md @@ -15,8 +15,11 @@ devbox run ios.sh devices list # Start simulator devbox run start:sim +# Build iOS app (define build:ios in devbox.json) +devbox run build:ios + # Build, install, and launch app on simulator -devbox run start:ios +ios.sh run # Stop simulator devbox run stop:sim @@ -58,7 +61,7 @@ devbox run ios.sh devices eval ```sh devbox run start:sim [device] # Start iOS simulator (defaults to IOS_DEFAULT_DEVICE) devbox run stop:sim # Stop all running simulators -devbox run start:ios [device] # Build, install, and launch app on simulator +ios.sh run [app_path] [device] # Build, install, and launch app on simulator ``` ### Device Management @@ -73,8 +76,10 @@ devbox run ios.sh devices sync # Ensure simulators match device definitions ### Build Commands ```sh -devbox run build # Build iOS app -devbox run test # Run unit tests +# Define build scripts in devbox.json using native xcodebuild: +# "build:ios": ["ios.sh xcodebuild -scheme MyApp build"] +devbox run build:ios # Build iOS app +ios.sh xcodebuild # Run xcodebuild with Nix flags stripped devbox run test:e2e # Run E2E tests with simulator ``` @@ -91,12 +96,13 @@ devbox run ios.sh info # Show Xcode and SDK info - `IOS_SCRIPTS_DIR` โ€” runtime scripts directory (`.devbox/virtenv/ios/scripts`) - `IOS_DEFAULT_DEVICE` โ€” used when no device name is provided (default: `max`) - `IOS_DEVICES` โ€” comma-separated device names to evaluate (empty means all) -- `IOS_APP_PROJECT` โ€” path to .xcodeproj or .xcworkspace -- `IOS_APP_SCHEME` โ€” Xcode build scheme -- `IOS_APP_BUNDLE_ID` โ€” app bundle identifier -- `IOS_APP_ARTIFACT` โ€” path to built .app bundle (auto-detected if not set) +- `IOS_APP_ARTIFACT` โ€” path or glob for .app bundle (empty = auto-detect via xcodebuild + search) +- `IOS_APP_SCHEME` โ€” Xcode scheme override (empty = auto-detect from project name) +- `IOS_APP_PROJECT` โ€” explicit .xcworkspace or .xcodeproj path (empty = auto-detect) +- `IOS_BUILD_CONFIG` โ€” build configuration: Debug or Release (default: Debug) +- `IOS_DERIVED_DATA_PATH` โ€” DerivedData directory (default: .devbox/virtenv/ios/DerivedData) - `IOS_DEVELOPER_DIR` โ€” path to Xcode developer directory (auto-detected if not set) -- `IOS_DOWNLOAD_RUNTIME` โ€” auto-download missing iOS runtimes (0/1, default: 0) +- `IOS_DOWNLOAD_RUNTIME` โ€” auto-download missing iOS runtimes (0/1, default: 1) ## Pure Mode Testing @@ -116,9 +122,7 @@ When running with `--pure`, the plugin: - Cleans up test simulators after completion - Ensures reproducible CI environment -The `IN_NIX_SHELL` environment variable is automatically set by devbox: -- `IN_NIX_SHELL=impure` - Normal mode -- `IN_NIX_SHELL=pure` - Pure mode (set by `--pure` flag) +The `DEVBOX_PURE_SHELL` environment variable is automatically set by devbox when using the `--pure` flag. Scripts auto-detect this to determine whether to create fresh, isolated simulators. ## Xcode Discovery @@ -169,9 +173,9 @@ launchctl list | grep CoreSimulator ``` ### Build Failures with Nix Flags -If you see Nix-related flags causing issues, the build script automatically strips them: +The iOS init hook strips Nix compilation variables (`LD`, `LDFLAGS`, `NIX_LDFLAGS`, `NIX_CFLAGS_COMPILE`, `NIX_CFLAGS_LINK`) at shell startup, so `xcodebuild` works natively in devbox shell. If you encounter issues outside devbox shell, use the `ios.sh xcodebuild` wrapper: ```sh -env -u LD -u LDFLAGS -u NIX_LDFLAGS xcodebuild ... +ios.sh xcodebuild -project MyApp.xcodeproj -scheme MyApp build ``` ## Reference diff --git a/plugins/ios/REFERENCE.md b/plugins/ios/REFERENCE.md index 4e7d0fa..8097838 100644 --- a/plugins/ios/REFERENCE.md +++ b/plugins/ios/REFERENCE.md @@ -42,12 +42,12 @@ Configure the plugin by setting environment variables in `devbox.json` or `plugi - `IOS_XCODE_ENV_PATH` โ€” Additional PATH entries for Xcode tools - `IOS_DOWNLOAD_RUNTIME` โ€” Auto-download missing runtimes (1=yes, 0=no; default: 1) -### App Build Settings -- `IOS_APP_PROJECT` โ€” Xcode project path (default: "ios.xcodeproj") -- `IOS_APP_SCHEME` โ€” Xcode build scheme (default: matches project name) -- `IOS_APP_BUNDLE_ID` โ€” App bundle identifier (default: "com.example.ios") -- `IOS_APP_ARTIFACT` โ€” App bundle path/glob after build (default: "DerivedData/Build/Products/Debug-iphonesimulator/*.app") -- `IOS_APP_DERIVED_DATA` โ€” Xcode derived data directory (default: ".devbox/virtenv/ios/DerivedData") +### App Settings +- `IOS_APP_ARTIFACT` โ€” Path or glob pattern for .app bundle (relative to project root; empty = auto-detect) +- `IOS_APP_SCHEME` โ€” Xcode scheme override (empty = auto-detect from project filename) +- `IOS_APP_PROJECT` โ€” Explicit .xcworkspace or .xcodeproj path (empty = auto-detect) +- `IOS_BUILD_CONFIG` โ€” Build configuration: Debug or Release (default: "Debug") +- `IOS_DERIVED_DATA_PATH` โ€” DerivedData directory path (default: `.devbox/virtenv/ios/DerivedData`) ### Performance Settings - `IOS_SKIP_SETUP` โ€” Skip iOS environment setup during shell initialization (1=skip, 0=setup; default: 0) @@ -60,37 +60,114 @@ Configure the plugin by setting environment variables in `devbox.json` or `plugi Start simulator: ```bash -devbox run --pure start-sim [device] +ios.sh simulator start [--pure] [device] ``` - If `device` is specified, uses that device name - Otherwise uses `IOS_DEFAULT_DEVICE` - Boots simulator if not already running +- `--pure`: Creates a fresh, isolated test simulator with clean state (for deterministic tests) +- Auto-detects pure mode when `DEVBOX_PURE_SHELL=1` (set by `devbox run --pure`) +- `REUSE_SIM=1`: Override pure mode to reuse existing simulator (e.g., `devbox run --pure -e REUSE_SIM=1`) +- Saves simulator UDID to `$IOS_RUNTIME_DIR/${SUITE_NAME:-default}/simulator-udid.txt` +- In pure mode, test simulator name includes suite label for isolation (e.g., `"iPhone 17 (iOS 26.2) Test-ios-e2e"`) + +**Convenience aliases:** +- `devbox run --pure start:sim [device]` (equivalent to `ios.sh simulator start` without `--pure`) +- `devbox run --pure stop:sim` (equivalent to `ios.sh simulator stop`) Stop simulator: ```bash -devbox run --pure stop-sim +ios.sh simulator stop ``` -- Shuts down all running simulators +- In pure mode (test simulator exists): shuts down and deletes the test simulator, cleans up state files +- In normal mode: shuts down the simulator via `ios_stop()` -### Build and Run +Check simulator readiness: +```bash +ios.sh simulator ready +``` +- Silent readiness probe: exit 0 if simulator is booted, exit 1 if not +- Reads UDID from suite-namespaced state file, falls back to finding any booted simulator +- Designed for use as a process-compose readiness probe -Build and run app: +Reset simulators: ```bash -devbox run --pure start-ios [device] +ios.sh simulator reset ``` -- Runs `devbox run --pure build-ios` first -- Installs app bundle matched by `IOS_APP_ARTIFACT` -- Launches app on simulator -- If `device` specified, uses that device; otherwise uses `IOS_DEFAULT_DEVICE` +- Stops all running simulators +- Deletes simulators matching device definitions + +### Deploy -Build only: ```bash -devbox run --pure build-ios +ios.sh deploy [app_path] ``` -- Builds Xcode project using `IOS_APP_PROJECT` and `IOS_APP_SCHEME` -- Outputs to `IOS_APP_DERIVED_DATA` -- Configuration: Debug -- Destination: iOS Simulator +- Installs and launches an app on an already-running simulator (no build, no simulator start) +- If `app_path` is provided, installs the specified .app bundle +- If no arguments, auto-detects .app using `ios_find_app()` (same resolution as `run`) +- Reads simulator UDID from suite-namespaced state file +- Extracts bundle ID from the app's `Info.plist` +- Saves bundle ID to `$IOS_RUNTIME_DIR/${SUITE_NAME:-default}/bundle-id.txt` + +### App Lifecycle + +```bash +ios.sh app status +``` +- Checks if the deployed app is running on the simulator +- Exit 0 if running, exit 1 if not +- Reads bundle ID and simulator UDID from suite-namespaced state files + +```bash +ios.sh app stop +``` +- Terminates the deployed app via `xcrun simctl terminate` +- Reads bundle ID and simulator UDID from suite-namespaced state files + +### Xcode Build Wrapper + +```bash +ios.sh xcodebuild [args...] +``` +- Runs `xcodebuild` with Nix-incompatible environment variables removed +- Unsets `LD`, `LDFLAGS`, `NIX_LDFLAGS`, `NIX_CFLAGS_COMPILE`, and `NIX_CFLAGS_LINK` in a subshell +- All arguments are forwarded directly to `xcodebuild` +- The caller's environment is not affected (stripping happens in a subshell) + +Use this in `devbox.json` build scripts instead of manually stripping Nix flags: +```json +{ + "shell": { + "scripts": { + "build:ios": [ + "ios.sh xcodebuild -project MyApp.xcodeproj -scheme MyApp -configuration Debug -destination 'generic/platform=iOS Simulator' build" + ] + } + } +} +``` + +The iOS init hook strips Nix compilation variables at shell startup, so `xcodebuild` works natively in devbox shell. The `ios.sh xcodebuild` subcommand forwards directly to `xcodebuild`. + +### Run App + +```bash +ios.sh run [app_path] [device] +``` +- Starts simulator, builds, resolves, installs, and launches the app +- If `app_path` is provided, skips build step and installs the provided .app bundle +- If no arguments, builds project and auto-detects the .app bundle + +**.app resolution precedence (when no explicit path):** + +1. `IOS_APP_ARTIFACT` env var โ€” glob resolved relative to project root +2. `xcodebuild -showBuildSettings` โ€” queries BUILT_PRODUCTS_DIR + FULL_PRODUCT_NAME from the Xcode project +3. Recursive search of project root for `*.app` directories (excludes Pods/, .build/, SourcePackages/, node_modules/, .devbox/, DerivedData/ModuleCache/) +4. Recursive search of `$PWD` if different from project root (same exclusions) + +Bundle ID is auto-extracted from the .app's `Info.plist` via PlistBuddy, or from `xcodebuild -showBuildSettings` if available. + +**Build script detection:** The `run` command tries `build:ios` first, then falls back to `build`. Define a build script in `devbox.json` using native tools (e.g., `xcodebuild`). ### Device Management @@ -261,14 +338,12 @@ The `devices.lock` file tracks which devices should be created: "runtime": "17.5" } ], - "checksum": "abc123...", - "generated_at": "2026-02-09T12:00:00Z" + "checksum": "abc123..." } ``` - `devices`: Array of device definitions to create - `checksum`: SHA-256 hash of all device definition files (for validation) -- `generated_at`: ISO 8601 timestamp ## Script Architecture @@ -284,6 +359,7 @@ Scripts are organized in 5 layers (see `wiki/project/ARCHITECTURE.md` for detail **Layer 3 - domain/**: Domain operations - `domain/device_manager.sh` โ€” Runtime resolution, simulator operations - `domain/simulator.sh` โ€” Simulator lifecycle management +- `domain/build.sh` โ€” Build command (auto-detect and build Xcode project) - `domain/deploy.sh` โ€” App building and deployment - `domain/validate.sh` โ€” Non-blocking validation @@ -308,13 +384,20 @@ These are set automatically by the plugin: - `PATH` โ€” Updated with Xcode tools and plugin scripts - `IOS_NODE_BINARY` โ€” Node.js binary path (if available, for React Native) +### Runtime State + +- `IOS_RUNTIME_DIR` โ€” Directory for runtime state files (default: `.devbox/virtenv/ios/runtime`) +- `SUITE_NAME` โ€” Test suite name for state isolation (default: "default") + - Each suite gets its own subdirectory under `$IOS_RUNTIME_DIR/$SUITE_NAME/` + - State files: `simulator-udid.txt`, `test-simulator-udid.txt` (pure mode only), `bundle-id.txt` + - Set in process-compose environment blocks for parallel test execution + ### Runtime Variables Set during simulator/app operations: - `IOS_SIM_UDID` โ€” UUID of running simulator - `IOS_SIM_NAME` โ€” Name of running simulator -- `IOS_APP_BUNDLE_PATH` โ€” Resolved app bundle path after build ## Troubleshooting @@ -379,10 +462,10 @@ devbox run --pure ios.sh devices eval **Symptom:** Xcode build errors **Checklist:** -1. Check `IOS_APP_PROJECT` points to correct `.xcodeproj` -2. Verify `IOS_APP_SCHEME` exists in project +1. Check that your `.xcodeproj` or `.xcworkspace` exists in the project root +2. Verify `build:ios` or `build` script in devbox.json is correct 3. Ensure derived data directory is writable -4. Clean build: `rm -rf .devbox/virtenv/ios/DerivedData` +4. Clean build: `rm -rf DerivedData` or the path your build script uses ## Platform Requirements @@ -390,6 +473,7 @@ devbox run --pure ios.sh devices eval - **Xcode** โ€” Install from App Store or use Xcode Command Line Tools - **iOS Simulator** โ€” Included with Xcode - **Devbox** โ€” Required for plugin system +- **Non-Darwin:** On non-macOS platforms, the iOS plugin init hooks exit immediately without errors, allowing cross-platform devbox.json files that include both Android and iOS plugins. ## Best Practices @@ -410,9 +494,9 @@ devbox run --pure ios.sh devices eval - Keep command-line tools updated ### Build Configuration -- Use project-relative paths for `IOS_APP_ARTIFACT` -- Commit derived data to `.gitignore` -- Use consistent scheme names across projects +- Use project-relative paths for `IOS_APP_ARTIFACT` when auto-detect doesn't work +- Commit derived data directories to `.gitignore` +- Auto-detect works best when a single `.xcodeproj` or `.xcworkspace` exists in project root ## Example Workflows @@ -442,10 +526,10 @@ ios.sh devices list ios.sh devices eval # Start simulator -devbox run start-sim +devbox run start:sim # Build and run app -devbox run start-ios +ios.sh run ``` ### Adding New Device diff --git a/plugins/ios/plugin.json b/plugins/ios/plugin.json index 848abc4..4f5236a 100644 --- a/plugins/ios/plugin.json +++ b/plugins/ios/plugin.json @@ -1,6 +1,6 @@ { "name": "ios", - "version": "0.0.2", + "version": "0.0.3", "description": "Configures iOS tooling inside Devbox and ensures Xcode toolchain usage.", "env": { "IOS_CONFIG_DIR": "{{ .DevboxDir }}", @@ -11,8 +11,15 @@ "IOS_DEFAULT_RUNTIME": "", "IOS_DEVELOPER_DIR": "", "IOS_DOWNLOAD_RUNTIME": "1", + "IOS_APP_ARTIFACT": "", + "IOS_APP_SCHEME": "", + "IOS_APP_PROJECT": "", + "IOS_BUILD_CONFIG": "Debug", + "IOS_DERIVED_DATA_PATH": "{{ .Virtenv }}/DerivedData", "IOS_XCODE_ENV_PATH": "", - "REPORTS_DIR": "reports" + "IOS_RUNTIME_DIR": "{{ .Virtenv }}/runtime", + "REPORTS_DIR": "reports", + "LANG": "en_US.UTF-8" }, "packages": { "bash": "latest", @@ -32,6 +39,7 @@ } }, "create_files": { + "{{ .Virtenv }}/runtime": "", "{{ .Virtenv }}/scripts/lib/lib.sh": "virtenv/scripts/lib/lib.sh", "{{ .Virtenv }}/scripts/platform/core.sh": "virtenv/scripts/platform/core.sh", "{{ .Virtenv }}/scripts/platform/device_config.sh": "virtenv/scripts/platform/device_config.sh", diff --git a/plugins/ios/virtenv/scripts/domain/deploy.sh b/plugins/ios/virtenv/scripts/domain/deploy.sh index 6835e47..6e2cc64 100644 --- a/plugins/ios/virtenv/scripts/domain/deploy.sh +++ b/plugins/ios/virtenv/scripts/domain/deploy.sh @@ -2,7 +2,7 @@ # iOS Plugin - App Building and Deployment # See REFERENCE.md for detailed documentation -set -eu +set -e if ! (return 0 2>/dev/null); then echo "ERROR: deploy.sh must be sourced" >&2 @@ -23,81 +23,15 @@ if [ -n "${IOS_SCRIPTS_DIR:-}" ]; then if [ -f "${IOS_SCRIPTS_DIR}/platform/core.sh" ]; then . "${IOS_SCRIPTS_DIR}/platform/core.sh" fi - if [ -f "${IOS_SCRIPTS_DIR}/domain/simulator.sh" ]; then - . "${IOS_SCRIPTS_DIR}/domain/simulator.sh" - fi fi ios_log_debug "deploy.sh loaded" # ============================================================================ -# Project and Path Resolution Functions +# Project Resolution # ============================================================================ -# Resolve Xcode project path -# Returns: project path -ios_resolve_app_project() { - if [ -n "${IOS_APP_PROJECT:-}" ]; then - printf '%s\n' "$IOS_APP_PROJECT" - return 0 - fi - for proj in *.xcodeproj; do - [ -d "$proj" ] || continue - printf '%s\n' "$proj" - return 0 - done - return 1 -} - -# Resolve Xcode scheme name -# Returns: scheme name -ios_resolve_app_scheme() { - if [ -n "${IOS_APP_SCHEME:-}" ]; then - printf '%s\n' "$IOS_APP_SCHEME" - return 0 - fi - project="$(ios_resolve_app_project 2>/dev/null || true)" - if [ -n "$project" ]; then - printf '%s\n' "$(basename "$project" .xcodeproj)" - return 0 - fi - return 1 -} - -# Resolve app bundle identifier -# Returns: bundle ID -ios_resolve_app_bundle_id() { - if [ -n "${IOS_APP_BUNDLE_ID:-}" ]; then - printf '%s\n' "$IOS_APP_BUNDLE_ID" - return 0 - fi - return 1 -} - -# Resolve derived data directory -# Returns: derived data path -ios_resolve_derived_data() { - if [ -n "${IOS_APP_DERIVED_DATA:-}" ]; then - printf '%s\n' "$IOS_APP_DERIVED_DATA" - return 0 - fi - if [ -n "${DEVBOX_PROJECT_ROOT:-}" ]; then - printf '%s\n' "${DEVBOX_PROJECT_ROOT%/}/.devbox/virtenv/ios/DerivedData" - return 0 - fi - if [ -n "${DEVBOX_PROJECT_DIR:-}" ]; then - printf '%s\n' "${DEVBOX_PROJECT_DIR%/}/.devbox/virtenv/ios/DerivedData" - return 0 - fi - if [ -n "${DEVBOX_WD:-}" ]; then - printf '%s\n' "${DEVBOX_WD%/}/.devbox/virtenv/ios/DerivedData" - return 0 - fi - printf '%s\n' "./.devbox/virtenv/ios/DerivedData" -} - # Resolve project root directory -# Returns: project root path ios_resolve_project_root() { if [ -n "${DEVBOX_PROJECT_ROOT:-}" ]; then printf '%s\n' "${DEVBOX_PROJECT_ROOT%/}" @@ -114,150 +48,278 @@ ios_resolve_project_root() { printf '%s\n' "$PWD" } -# Resolve devbox binary path -# Returns: devbox binary path -ios_resolve_devbox_bin() { - if [ -n "${DEVBOX_BIN:-}" ] && [ -x "$DEVBOX_BIN" ]; then - printf '%s\n' "$DEVBOX_BIN" - return 0 - fi - if command -v devbox >/dev/null 2>&1; then - command -v devbox - return 0 - fi - if [ -n "${DEVBOX_INIT_PATH:-}" ]; then - devbox_bin="$(PATH="$DEVBOX_INIT_PATH:$PATH" command -v devbox 2>/dev/null || true)" - if [ -n "$devbox_bin" ]; then - DEVBOX_BIN="$devbox_bin" - export DEVBOX_BIN - printf '%s\n' "$devbox_bin" - return 0 - fi - fi - for candidate in "$HOME/.nix-profile/bin/devbox" "/usr/local/bin/devbox" "/opt/homebrew/bin/devbox"; do - if [ -x "$candidate" ]; then - DEVBOX_BIN="$candidate" - export DEVBOX_BIN - printf '%s\n' "$candidate" - return 0 - fi - done - return 1 -} - # ============================================================================ -# Build Functions +# App Resolution # ============================================================================ -# Run iOS build using devbox -# Returns: 0 on success -ios_run_build() { - project_root="$(ios_resolve_project_root)" - if [ -z "$project_root" ] || [ ! -d "$project_root" ]; then - echo "Unable to resolve project root for iOS build." >&2 - return 1 - fi - devbox_bin="$(ios_resolve_devbox_bin 2>/dev/null || true)" - if [ -z "$devbox_bin" ]; then - echo "devbox is required to run the project build." >&2 - return 1 - fi - (cd "$project_root" && "$devbox_bin" run --pure build-ios) -} +# Resolve .app path from glob pattern +# Args: project_root, app_pattern +# Returns: first matching .app directory +ios_resolve_app_glob() { + _arg_root="$1" + _arg_pattern="$2" -# Resolve app bundle path using glob pattern -# Returns: app bundle path -ios_resolve_app_path() { - project_root="$(ios_resolve_project_root)" - pattern="${IOS_APP_ARTIFACT:-}" - if [ -z "$pattern" ]; then + if [ -z "$_arg_pattern" ]; then return 1 fi - if [ "${pattern#/}" = "$pattern" ]; then - pattern="${project_root%/}/$pattern" + + # Make pattern absolute if it's relative + if [ "${_arg_pattern#/}" = "$_arg_pattern" ]; then + _arg_pattern="${_arg_root%/}/$_arg_pattern" fi + set +f - matches="" - for candidate in $pattern; do - if [ -d "$candidate" ]; then - matches="${matches}${matches:+ -}$candidate" + _matched="" + for _candidate in $_arg_pattern; do + if [ -d "$_candidate" ]; then + _matched="${_matched}${_matched:+ +}$_candidate" fi done set -f - if [ -z "$matches" ]; then + + if [ -z "$_matched" ]; then return 1 fi - count="$(printf '%s\n' "$matches" | wc -l | tr -d ' ')" - if [ "$count" -gt 1 ]; then - echo "Multiple app bundles matched ${pattern}; using the first match." >&2 + + _count="$(printf '%s\n' "$_matched" | wc -l | tr -d ' ')" + if [ "$_count" -gt 1 ]; then + ios_log_warn "deploy.sh" "Multiple app bundles matched pattern: $_arg_pattern; using first match" fi - printf '%s\n' "$matches" | head -n1 + + printf '%s\n' "$_matched" | head -n1 } -# ============================================================================ -# Setup Orchestration -# ============================================================================ +# Query xcodebuild for .app path +# Args: project_root +# Returns: .app path (sets IOS_XCODEBUILD_BUNDLE_ID as side effect) +ios_resolve_app_via_xcodebuild() { + _xc_root="$1" -# Setup iOS environment for deployment -# Returns: 0 on success -ios_setup() { - if [ -n "${IOS_XCODE_ENV_PATH:-}" ]; then - node_binary="${IOS_NODE_BINARY:-${NODE_BINARY:-}}" - if [ -z "$node_binary" ]; then - echo "IOS_XCODE_ENV_PATH is set but IOS_NODE_BINARY/NODE_BINARY is empty." >&2 - return 1 + # Find Xcode project or workspace + _xc_proj="" + for _f in "$_xc_root"/*.xcworkspace; do + if [ -d "$_f" ]; then + # Skip Pods workspace + case "$(basename "$_f")" in + Pods.xcworkspace) continue ;; + esac + _xc_proj="$_f" + break fi - env_dir="$(dirname "$IOS_XCODE_ENV_PATH")" - if [ ! -d "$env_dir" ]; then - echo "IOS_XCODE_ENV_PATH directory does not exist: ${env_dir}" >&2 - return 1 + done + if [ -z "$_xc_proj" ]; then + for _f in "$_xc_root"/*.xcodeproj; do + if [ -d "$_f" ]; then + _xc_proj="$_f" + break + fi + done + fi + + if [ -z "$_xc_proj" ]; then + return 1 + fi + + # Determine flag type + case "$_xc_proj" in + *.xcworkspace) _xc_flag="-workspace" ;; + *.xcodeproj) _xc_flag="-project" ;; + *) return 1 ;; + esac + + # Derive scheme from project name + _xc_scheme="$(basename "$_xc_proj" | sed 's/\.\(xcworkspace\|xcodeproj\)$//')" + + # Resolve DerivedData path (must match ios_build defaults) + _xc_derived_data="${IOS_DERIVED_DATA_PATH:-}" + if [ -z "$_xc_derived_data" ]; then + if [ -n "${DEVBOX_PROJECT_ROOT:-}" ]; then + _xc_derived_data="${DEVBOX_PROJECT_ROOT}/.devbox/virtenv/ios/DerivedData" + else + _xc_derived_data="$PWD/.devbox/virtenv/ios/DerivedData" fi - printf 'export NODE_BINARY=%s\n' "$node_binary" >"$IOS_XCODE_ENV_PATH" fi - ensure_developer_dir - ios_require_tool xcrun "Missing required tool: xcrun. Install Xcode CLI tools before running (xcode-select --install or Xcode.app + xcode-select -s)." - ios_require_tool jq - ensure_simctl - if ! ensure_core_sim_service; then + # Query build settings with matching DerivedData path + _settings="$(xcodebuild "$_xc_flag" "$_xc_proj" -scheme "$_xc_scheme" \ + -configuration Debug -destination 'generic/platform=iOS Simulator' \ + -derivedDataPath "$_xc_derived_data" \ + -showBuildSettings 2>/dev/null || true)" + + if [ -z "$_settings" ]; then return 1 fi - devices_dir="$(ios_devices_dir 2>/dev/null || true)" - if [ -z "$devices_dir" ]; then - echo "iOS devices directory not found. Expected devbox.d/ios/devices or IOS_DEVICES_DIR." >&2 + _built_dir="$(printf '%s\n' "$_settings" | awk '/^\s*BUILT_PRODUCTS_DIR = /{print $3; exit}')" + _product_name="$(printf '%s\n' "$_settings" | awk '/^\s*FULL_PRODUCT_NAME = /{print $3; exit}')" + _bundle_id="$(printf '%s\n' "$_settings" | awk '/^\s*PRODUCT_BUNDLE_IDENTIFIER = /{print $3; exit}')" + + if [ -z "$_built_dir" ] || [ -z "$_product_name" ]; then return 1 fi - device_files="$(ios_device_files "$devices_dir")" - if [ -z "$device_files" ]; then - echo "No iOS device definitions found in ${devices_dir}." >&2 + _app_path="${_built_dir%/}/$_product_name" + + if [ ! -d "$_app_path" ]; then return 1 fi - device_files="$(ios_selected_device_files "$devices_dir")" || return 1 - for device_file in $device_files; do - device_name="$(jq -r '.name // empty' "$device_file")" - runtime="$(jq -r '.runtime // empty' "$device_file")" - if [ -z "$device_name" ]; then - echo "iOS device definition missing name in ${device_file}." >&2 - return 1 + # Export bundle ID as side effect for caller + if [ -n "$_bundle_id" ]; then + IOS_XCODEBUILD_BUNDLE_ID="$_bundle_id" + export IOS_XCODEBUILD_BUNDLE_ID + fi + + printf '%s\n' "$_app_path" +} + +# Find .app bundle using auto-detect precedence chain +# Args: project_root +# Precedence: +# 1. IOS_APP_ARTIFACT env var (glob resolved relative to project_root) +# 2. xcodebuild -showBuildSettings query +# 3. DerivedData search (matches ios_build default output location) +# 4. Recursive search of project_root for *.app directories +# 5. Recursive search of $PWD (skipped if PWD == project_root) +# 6. Error with guidance +ios_find_app() { + _find_root="$1" + + # 1. IOS_APP_ARTIFACT env var + if [ -n "${IOS_APP_ARTIFACT:-}" ]; then + _app="$(ios_resolve_app_glob "$_find_root" "$IOS_APP_ARTIFACT" || true)" + if [ -n "$_app" ] && [ -d "$_app" ]; then + ios_log_info "deploy.sh" "App resolved via IOS_APP_ARTIFACT env var: $_app" + printf '%s\n' "$_app" + return 0 fi - if [ -z "$runtime" ]; then - runtime="${IOS_DEFAULT_RUNTIME:-}" - if [ -z "$runtime" ] && command -v xcrun >/dev/null 2>&1; then - runtime="$(xcrun --sdk iphonesimulator --show-sdk-version 2>/dev/null || true)" - fi + fi + + # 2. xcodebuild query + if command -v xcodebuild >/dev/null 2>&1; then + _app="$(ios_resolve_app_via_xcodebuild "$_find_root" || true)" + if [ -n "$_app" ] && [ -d "$_app" ]; then + ios_log_info "deploy.sh" "App resolved via xcodebuild: $_app" + printf '%s\n' "$_app" + return 0 fi - if [ -z "$runtime" ]; then - echo "IOS_DEFAULT_RUNTIME must be set (or install a simulator runtime in Xcode)." >&2 - return 1 + fi + + # 3. DerivedData search (matches ios_build default output location) + _dd_path="${IOS_DERIVED_DATA_PATH:-}" + if [ -z "$_dd_path" ]; then + if [ -n "${DEVBOX_PROJECT_ROOT:-}" ]; then + _dd_path="${DEVBOX_PROJECT_ROOT}/.devbox/virtenv/ios/DerivedData" + else + _dd_path="${_find_root}/.devbox/virtenv/ios/DerivedData" fi - ensure_device "$device_name" "$runtime" - done + fi + if [ -d "$_dd_path" ]; then + _app="$(find "$_dd_path" -name '*.app' -type d \ + -not -path '*/ModuleCache/*' \ + 2>/dev/null | head -n1)" + if [ -n "$_app" ] && [ -d "$_app" ]; then + ios_log_info "deploy.sh" "App resolved via DerivedData: $_app" + printf '%s\n' "$_app" + return 0 + fi + fi + + # 4. Recursive search of project_root + _app="$(find "$_find_root" -name '*.app' -type d \ + -not -path '*/Pods/*' \ + -not -path '*/.build/*' \ + -not -path '*/SourcePackages/*' \ + -not -path '*/node_modules/*' \ + -not -path '*/.devbox/*' \ + -not -path '*/DerivedData/ModuleCache/*' \ + 2>/dev/null | head -n1)" + if [ -n "$_app" ] && [ -d "$_app" ]; then + ios_log_info "deploy.sh" "App resolved via project search: $_app" + printf '%s\n' "$_app" + return 0 + fi + + # 5. Recursive search of $PWD (skip if same as project_root) + _cwd="$(cd "$PWD" && pwd -P)" + _root_real="$(cd "$_find_root" && pwd -P)" + if [ "$_cwd" != "$_root_real" ]; then + _app="$(find "$PWD" -name '*.app' -type d \ + -not -path '*/Pods/*' \ + -not -path '*/.build/*' \ + -not -path '*/SourcePackages/*' \ + -not -path '*/node_modules/*' \ + -not -path '*/.devbox/*' \ + -not -path '*/DerivedData/ModuleCache/*' \ + 2>/dev/null | head -n1)" + if [ -n "$_app" ] && [ -d "$_app" ]; then + ios_log_info "deploy.sh" "App resolved via directory search: $_app" + printf '%s\n' "$_app" + return 0 + fi + fi + + # 6. Error + ios_log_error "deploy.sh" "No .app bundle found. Searched: IOS_APP_ARTIFACT env var, xcodebuild settings, project root, current directory." + ios_log_error "deploy.sh" "Set IOS_APP_ARTIFACT in devbox.json env, or pass a path: ios.sh run /path/to/MyApp.app" + ios_log_error "deploy.sh" "See: plugins/ios/REFERENCE.md for app resolution details." + return 1 +} + +# ============================================================================ +# Bundle ID Extraction +# ============================================================================ + +# Extract CFBundleIdentifier from .app bundle +# Args: app_path +# Returns: bundle identifier +ios_extract_bundle_id() { + _app_path="$1" + + _plist="${_app_path%/}/Info.plist" + if [ ! -f "$_plist" ]; then + ios_log_error "deploy.sh" "Info.plist not found in: $_app_path" + return 1 + fi + + _bundle_id="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$_plist" 2>/dev/null || true)" + if [ -z "$_bundle_id" ]; then + ios_log_error "deploy.sh" "Unable to read CFBundleIdentifier from: $_plist" + return 1 + fi + + ios_log_info "deploy.sh" "Bundle ID: $_bundle_id" + printf '%s\n' "$_bundle_id" +} + +# ============================================================================ +# Build Functions +# ============================================================================ + +# Run iOS build using devbox +# Args: project_root +ios_run_build() { + _build_root="$1" + + _devbox_bin="$(ios_resolve_devbox_bin 2>/dev/null || true)" + if [ -z "$_devbox_bin" ]; then + ios_log_debug "deploy.sh" "devbox not found; skipping build step" + return 0 + fi - echo "Done. Launch via Xcode > Devices or 'xcrun simctl boot \"\"' then 'open -a Simulator'." + # Try platform-specific build command first, then fall back to generic + if (cd "$_build_root" && "$_devbox_bin" run --list 2>/dev/null | grep -q "build:ios"); then + ios_log_info "deploy.sh" "Running build:ios" + (cd "$_build_root" && "$_devbox_bin" run --pure build:ios) + elif (cd "$_build_root" && "$_devbox_bin" run --list 2>/dev/null | grep -q "build"); then + ios_log_info "deploy.sh" "Running build" + (cd "$_build_root" && "$_devbox_bin" run --pure build) + else + ios_log_error "deploy.sh" "No build:ios or build script found in devbox.json." + ios_log_error "deploy.sh" "Define a build script using native tools (e.g., xcodebuild)." + return 1 + fi } # ============================================================================ @@ -265,36 +327,143 @@ ios_setup() { # ============================================================================ # Build, install, and launch app on simulator -# Args: device_name (optional) -# Returns: 0 on success +# Usage: ios_run_app [--app ] [--device ] [] +# --app - Path to .app bundle. If provided, skips build step. +# --device - Device name. If omitted, uses IOS_DEFAULT_DEVICE. +# Bare positional arg is treated as device name for convenience. ios_run_app() { - device_name="${1-}" - ios_start "$device_name" + app_arg="" + device_choice="" + + while [ $# -gt 0 ]; do + case "$1" in + --app) + app_arg="${2:-}" + shift 2 + ;; + --device) + device_choice="${2:-}" + shift 2 + ;; + *) + # Bare positional arg: treat as device name for convenience + if [ -z "$device_choice" ]; then + device_choice="$1" + fi + shift + ;; + esac + done - ios_run_build + # ---- Start Deployment ---- - app_path="$(ios_resolve_app_path || true)" - if [ -z "$app_path" ] || [ ! -d "$app_path" ]; then - echo "Unable to locate app bundle using IOS_APP_ARTIFACT=${IOS_APP_ARTIFACT:-}." >&2 - return 1 + echo "================================================" + echo "iOS App Deployment" + echo "================================================" + echo "" + + # ---- Start Simulator ---- + + # Source simulator if not already loaded + if [ -n "${IOS_SCRIPTS_DIR:-}" ] && [ -f "${IOS_SCRIPTS_DIR}/domain/simulator.sh" ]; then + . "${IOS_SCRIPTS_DIR}/domain/simulator.sh" fi - bundle_id="$(ios_resolve_app_bundle_id || true)" - if [ -z "$bundle_id" ]; then - plist="${app_path%/}/Info.plist" - if [ -f "$plist" ]; then - bundle_id="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$plist" 2>/dev/null || true)" + + ios_start "$device_choice" + + # ---- Resolve App Path ---- + + if [ -n "$app_arg" ]; then + # App provided as argument - use directly + app_path="$app_arg" + + # Make absolute if relative + if [ "${app_path#/}" = "$app_path" ]; then + app_path="$PWD/$app_path" + fi + + if [ ! -d "$app_path" ]; then + ios_log_error "deploy.sh" "App bundle not found: $app_path" + exit 1 + fi + + echo "Using provided app: $(basename "$app_path")" + else + # No app provided - build and locate + + project_root="$(ios_resolve_project_root)" + if [ -z "$project_root" ] || [ ! -d "$project_root" ]; then + ios_log_error "deploy.sh" "Unable to resolve project root for iOS build" + exit 1 fi + + echo "Project root: $project_root" + echo "" + + # ---- Build App ---- + + ios_run_build "$project_root" + + # ---- Find App ---- + + echo "" + echo "Locating .app bundle..." + + app_path="$(ios_find_app "$project_root" || true)" + + if [ -z "$app_path" ] || [ ! -d "$app_path" ]; then + exit 1 + fi + + echo "Found app: $(basename "$app_path")" fi + + # ---- Extract Bundle ID ---- + + echo "" + # Try xcodebuild-provided bundle ID first (set as side effect of ios_find_app) + bundle_id="${IOS_XCODEBUILD_BUNDLE_ID:-}" if [ -z "$bundle_id" ]; then - echo "Unable to resolve bundle identifier for ${app_path}." >&2 - return 1 + bundle_id="$(ios_extract_bundle_id "$app_path")" + else + ios_log_info "deploy.sh" "Bundle ID (from xcodebuild): $bundle_id" + fi + + if [ -z "$bundle_id" ]; then + ios_log_error "deploy.sh" "Unable to resolve bundle identifier for: $app_path" + exit 1 fi + + # ---- Deploy to Simulator ---- + udid="${IOS_SIM_UDID:-}" if [ -z "$udid" ]; then - echo "iOS simulator UDID not available; ensure the simulator is booted." >&2 - return 1 + ios_log_error "deploy.sh" "iOS simulator UDID not available; ensure the simulator is booted" + exit 1 fi + echo "" + echo "Deploying to: ${IOS_SIM_NAME:-$udid}" + echo "" + + # Terminate and uninstall existing app for a clean deploy + xcrun simctl terminate "$udid" "$bundle_id" 2>/dev/null || true + if xcrun simctl get_app_container "$udid" "$bundle_id" >/dev/null 2>&1; then + echo "Removing existing install: $bundle_id" + xcrun simctl uninstall "$udid" "$bundle_id" 2>/dev/null || true + fi + + echo "Installing app: $(basename "$app_path")" xcrun simctl install "$udid" "$app_path" + echo "โœ“ App installed" + + echo "" + echo "Launching app: $bundle_id" xcrun simctl launch "$udid" "$bundle_id" + echo "โœ“ App launched" + + echo "" + echo "================================================" + echo "โœ“ Deployment complete!" + echo "================================================" } diff --git a/plugins/ios/virtenv/scripts/domain/device_manager.sh b/plugins/ios/virtenv/scripts/domain/device_manager.sh index a0c5c7e..e899bcf 100644 --- a/plugins/ios/virtenv/scripts/domain/device_manager.sh +++ b/plugins/ios/virtenv/scripts/domain/device_manager.sh @@ -2,7 +2,7 @@ # iOS Plugin - Device and Simulator Management # Extracted from device.sh to eliminate circular dependencies -set -eu +set -e if ! (return 0 2>/dev/null); then echo "ERROR: device_manager.sh must be sourced, not executed directly" >&2 @@ -114,32 +114,6 @@ resolve_runtime_strict() { return 1 } -# Resolve runtime name only -# Args: preferred_version -# Returns: runtime_name -resolve_runtime_name() { - preferred="$1" - choice="$(resolve_runtime "$preferred" || true)" - if [ -n "$choice" ]; then - printf '%s\n' "$choice" | cut -d'|' -f2 - return 0 - fi - return 1 -} - -# Resolve runtime name strictly -# Args: preferred_version -# Returns: runtime_name -resolve_runtime_name_strict() { - preferred="$1" - choice="$(resolve_runtime_strict "$preferred" || true)" - if [ -n "$choice" ]; then - printf '%s\n' "$choice" | cut -d'|' -f2 - return 0 - fi - return 1 -} - # ============================================================================ # Simulator Device Queries # ============================================================================ diff --git a/plugins/ios/virtenv/scripts/domain/simulator.sh b/plugins/ios/virtenv/scripts/domain/simulator.sh index 949f453..8fc7a9e 100644 --- a/plugins/ios/virtenv/scripts/domain/simulator.sh +++ b/plugins/ios/virtenv/scripts/domain/simulator.sh @@ -2,7 +2,7 @@ # iOS Plugin - Simulator Lifecycle Management # See REFERENCE.md for detailed documentation -set -eu +set -e if ! (return 0 2>/dev/null); then echo "ERROR: simulator.sh must be sourced" >&2 @@ -195,3 +195,38 @@ ios_service() { sleep 5 done } + +# Check if the simulator is ready (booted) +# Returns 0 if simulator is booted, 1 otherwise +# Used by ios.sh simulator start --wait-ready +ios_simulator_ready() { + # Resolve UDID from state directory (suite-namespaced) + _suite="${SUITE_NAME:-default}" + _runtime_dir="${IOS_RUNTIME_DIR:-${DEVBOX_VIRTENV:-}}" + if [ -z "$_runtime_dir" ]; then + _runtime_dir="${PWD}/.devbox/virtenv" + fi + _state_dir="$_runtime_dir/ios/$_suite" + + _udid="" + if [ -f "$_state_dir/simulator-udid.txt" ]; then + _udid="$(cat "$_state_dir/simulator-udid.txt")" + fi + + # Fallback: check IOS_SIM_UDID env var + if [ -z "$_udid" ]; then + _udid="${IOS_SIM_UDID:-}" + fi + + if [ -z "$_udid" ]; then + return 1 + fi + + # Check if simulator is booted + _state="$(xcrun simctl list devices -j | jq -r --arg udid "$_udid" '.devices[]?[]? | select(.udid == $udid) | .state' 2>/dev/null | head -n1)" + + if [ "$_state" = "Booted" ]; then + return 0 + fi + return 1 +} diff --git a/plugins/ios/virtenv/scripts/domain/validate.sh b/plugins/ios/virtenv/scripts/domain/validate.sh index 87d8218..464db60 100644 --- a/plugins/ios/virtenv/scripts/domain/validate.sh +++ b/plugins/ios/virtenv/scripts/domain/validate.sh @@ -1,5 +1,5 @@ #!/usr/bin/env sh -set -eu +set -e if ! (return 0 2>/dev/null); then echo "ERROR: validate.sh must be sourced" >&2 diff --git a/plugins/ios/virtenv/scripts/init/init-hook.sh b/plugins/ios/virtenv/scripts/init/init-hook.sh index f97319d..3088c47 100644 --- a/plugins/ios/virtenv/scripts/init/init-hook.sh +++ b/plugins/ios/virtenv/scripts/init/init-hook.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash -# iOS plugin initialization script +# iOS Plugin - Initialization Hook # Generates devices.lock from IOS_DEVICES environment variable -# This runs before env.sh is sourced +# This runs before setup.sh is sourced set -e @@ -11,6 +11,10 @@ if [ "${IOS_SKIP_SETUP:-0}" = "1" ]; then exit 0 fi +if [ "$(uname -s)" != "Darwin" ]; then + exit 0 +fi + # Find virtenv directory VIRTENV_DIR="${IOS_SCRIPTS_DIR:-}/.." if [ -z "$VIRTENV_DIR" ] || [ "$VIRTENV_DIR" = "/.." ]; then @@ -97,14 +101,10 @@ else checksum="" fi -# Generate timestamp -timestamp="$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date +%Y-%m-%dT%H:%M:%SZ)" - # Create devices.lock with jq echo "$devices_array" | jq \ --arg cs "$checksum" \ - --arg ts "$timestamp" \ - '{devices: ., checksum: $cs, generated_at: $ts}' \ + '{devices: ., checksum: $cs}' \ > "$DEVICES_LOCK" 2>/dev/null || exit 0 # Make all scripts executable diff --git a/plugins/ios/virtenv/scripts/init/setup.sh b/plugins/ios/virtenv/scripts/init/setup.sh index 5f65e25..3d0a695 100644 --- a/plugins/ios/virtenv/scripts/init/setup.sh +++ b/plugins/ios/virtenv/scripts/init/setup.sh @@ -1,7 +1,8 @@ #!/usr/bin/env sh # iOS Plugin - Shell Initialization # Sets up environment when user runs 'devbox shell' -# This is NOT meant to make all functions available - use ios.sh for that +# NOTE: This file is sourced (not executed) by devbox init_hook, +# so it must be POSIX sh compatible (no bash-isms). # Skip all iOS setup if IOS_SKIP_SETUP=1 # Useful for Android-only contexts in React Native plugin @@ -9,8 +10,12 @@ if [ "${IOS_SKIP_SETUP:-0}" = "1" ]; then return 0 2>/dev/null || exit 0 fi +if [ "$(uname -s)" != "Darwin" ]; then + return 0 2>/dev/null || exit 0 +fi + if ! (return 0 2>/dev/null); then - echo "templates/devbox/plugins/ios/scripts/env.sh must be sourced." >&2 + echo "ERROR: setup.sh must be sourced, not executed directly" >&2 exit 1 fi @@ -72,7 +77,7 @@ fi # Optional debug output if ios_debug_enabled; then - ios_debug_log_script "templates/devbox/plugins/ios/scripts/env.sh" + ios_debug_log_script "setup.sh" ios_debug_dump_vars \ IOS_DEVICES \ IOS_DEFAULT_DEVICE \ diff --git a/plugins/ios/virtenv/scripts/lib/lib.sh b/plugins/ios/virtenv/scripts/lib/lib.sh index 1987d02..ebf7d42 100644 --- a/plugins/ios/virtenv/scripts/lib/lib.sh +++ b/plugins/ios/virtenv/scripts/lib/lib.sh @@ -2,7 +2,7 @@ # iOS Plugin - Core Utilities # See REFERENCE.md for detailed documentation -set -eu +set -e if ! (return 0 2>/dev/null); then echo "ERROR: lib.sh must be sourced" >&2 diff --git a/plugins/ios/virtenv/scripts/platform/core.sh b/plugins/ios/virtenv/scripts/platform/core.sh index 5a9d657..c650fef 100644 --- a/plugins/ios/virtenv/scripts/platform/core.sh +++ b/plugins/ios/virtenv/scripts/platform/core.sh @@ -2,7 +2,7 @@ # iOS Plugin - Core Xcode and Environment Setup # Extracted from env.sh to eliminate circular dependencies -set -eu +set -e if ! (return 0 2>/dev/null); then echo "ERROR: core.sh must be sourced, not executed directly" >&2 @@ -136,44 +136,16 @@ ios_resolve_devbox_bin() { # Environment Setup # ============================================================================ -# Setup omit-nix-env for iOS (use system Xcode/tools instead of Nix) -devbox_omit_nix_env() { - if [ "${DEVBOX_OMIT_NIX_ENV_APPLIED:-}" = "1" ]; then +# Setup Darwin environment for iOS (use system Xcode/tools instead of Nix) +ios_setup_native_toolchain() { + if [ "${IOS_NATIVE_TOOLCHAIN_APPLIED:-}" = "1" ]; then return 0 fi - export DEVBOX_OMIT_NIX_ENV_APPLIED=1 - devbox_bin="$(ios_resolve_devbox_bin 2>/dev/null || true)" - if [ -z "$devbox_bin" ]; then - ios_log_debug "devbox not found; skipping omit-nix-env setup." - return 0 - fi - - devbox_init_path="${DEVBOX_INIT_PATH:-}" - devbox_bin_dir="$(dirname "$devbox_bin")" - devbox_project_bin="" - if [ -n "${DEVBOX_PROJECT_ROOT:-}" ] && [ -d "${DEVBOX_PROJECT_ROOT}/.devbox/bin" ]; then - devbox_project_bin="${DEVBOX_PROJECT_ROOT}/.devbox/bin" - elif [ -n "${DEVBOX_WD:-}" ] && [ -d "${DEVBOX_WD}/.devbox/bin" ]; then - devbox_project_bin="${DEVBOX_WD}/.devbox/bin" - fi - - devbox_config_path="" - if [ -n "${DEVBOX_CONFIG:-}" ] && [ -f "$DEVBOX_CONFIG" ]; then - devbox_config_path="$DEVBOX_CONFIG" - elif [ -n "${DEVBOX_CONFIG_PATH:-}" ] && [ -f "$DEVBOX_CONFIG_PATH" ]; then - devbox_config_path="$DEVBOX_CONFIG_PATH" - elif [ -n "${DEVBOX_CONFIG_DIR:-}" ] && [ -f "${DEVBOX_CONFIG_DIR%/}/devbox.json" ]; then - devbox_config_path="${DEVBOX_CONFIG_DIR%/}/devbox.json" - fi - - if [ -n "$devbox_config_path" ]; then - eval "$("$devbox_bin" --config "$devbox_config_path" shellenv --install --no-refresh-alias --omit-nix-env=true)" - else - eval "$("$devbox_bin" shellenv --install --no-refresh-alias --omit-nix-env=true)" - fi - if [ "$(uname -s)" = "Darwin" ]; then + # Unset standard build variables that Xcode tools read + unset LD LDFLAGS CFLAGS + if [ -x /usr/bin/clang ]; then CC=/usr/bin/clang CXX=/usr/bin/clang++ @@ -192,16 +164,7 @@ devbox_omit_nix_env() { unset SDKROOT fi - if [ -n "$devbox_init_path" ]; then - PATH="${devbox_init_path}:${PATH}" - fi - if [ -n "$devbox_project_bin" ]; then - PATH="${devbox_project_bin}:${PATH}" - fi - if [ -n "$devbox_bin_dir" ]; then - PATH="${devbox_bin_dir}:${PATH}" - fi - export PATH + export IOS_NATIVE_TOOLCHAIN_APPLIED=1 } # Setup macOS system PATH and DEVELOPER_DIR @@ -214,7 +177,7 @@ ios_setup_environment() { fi # Setup omit-nix-env - devbox_omit_nix_env + ios_setup_native_toolchain # Ensure DEVELOPER_DIR is set if [ "$(uname -s)" = "Darwin" ]; then @@ -279,36 +242,4 @@ ios_show_summary() { echo " IOS_SIM_TARGET: device=${ios_target_device:-unknown} runtime=${ios_target_runtime:-not set}" } -# ============================================================================ -# Requirement Functions -# ============================================================================ - -ios_require_tool() { - tool="$1" - message="${2:-Missing required tool: $tool. Ensure the Devbox shell is active and required packages are installed.}" - if ! command -v "$tool" >/dev/null 2>&1; then - echo "$message" >&2 - exit 1 - fi -} - -ios_require_dir() { - path="$1" - message="${2:-Missing required directory: $path.}" - if [ ! -d "$path" ]; then - echo "$message" >&2 - exit 1 - fi -} - -ios_require_dir_contains() { - base="$1" - subpath="$2" - message="${3:-Missing required path: $base/$subpath.}" - if [ ! -e "$base/$subpath" ]; then - echo "$message" >&2 - exit 1 - fi -} - ios_debug_log_script "core.sh" diff --git a/plugins/ios/virtenv/scripts/platform/device_config.sh b/plugins/ios/virtenv/scripts/platform/device_config.sh index 7ea8248..277e5ef 100644 --- a/plugins/ios/virtenv/scripts/platform/device_config.sh +++ b/plugins/ios/virtenv/scripts/platform/device_config.sh @@ -2,7 +2,7 @@ # iOS Plugin - Device Configuration Management # Extracted from device.sh to eliminate circular dependencies -set -eu +set -e if ! (return 0 2>/dev/null); then echo "ERROR: device_config.sh must be sourced, not executed directly" >&2 diff --git a/plugins/ios/virtenv/scripts/user/config.sh b/plugins/ios/virtenv/scripts/user/config.sh index cf74717..7f3382b 100644 --- a/plugins/ios/virtenv/scripts/user/config.sh +++ b/plugins/ios/virtenv/scripts/user/config.sh @@ -2,7 +2,7 @@ # iOS Plugin - Configuration Management # See REFERENCE.md for detailed documentation -set -eu +set -e if ! (return 0 2>/dev/null); then echo "ERROR: config.sh must be sourced" >&2 @@ -42,10 +42,16 @@ ios_config_show() { echo " IOS_DEFAULT_RUNTIME: ${IOS_DEFAULT_RUNTIME:-(auto)}" echo "" echo "Application:" - echo " IOS_APP_PROJECT: ${IOS_APP_PROJECT:-*.xcodeproj}" - echo " IOS_APP_SCHEME: ${IOS_APP_SCHEME:-(auto)}" - echo " IOS_APP_BUNDLE_ID: ${IOS_APP_BUNDLE_ID:-com.example.ios}" - echo " IOS_APP_ARTIFACT: ${IOS_APP_ARTIFACT:-.devbox/virtenv/ios/DerivedData/...}" + echo " IOS_APP_ARTIFACT: ${IOS_APP_ARTIFACT:-(auto-detect)}" + echo " IOS_APP_SCHEME: ${IOS_APP_SCHEME:-(auto-detect)}" + echo " IOS_APP_PROJECT: ${IOS_APP_PROJECT:-(auto-detect)}" + echo "" + echo "Build:" + echo " IOS_BUILD_CONFIG: ${IOS_BUILD_CONFIG:-Debug}" + echo " IOS_DERIVED_DATA_PATH: ${IOS_DERIVED_DATA_PATH:-.devbox/virtenv/ios/DerivedData}" + echo "" + echo "Runtime:" + echo " IOS_DOWNLOAD_RUNTIME: ${IOS_DOWNLOAD_RUNTIME:-1}" echo "" echo "Paths:" echo " IOS_CONFIG_DIR: ${IOS_CONFIG_DIR:-.}" @@ -55,51 +61,6 @@ ios_config_show() { echo "To override values, set environment variables in your devbox.json:" echo ' "env": {' echo ' "IOS_DEVICES": "min,max",' - echo ' "IOS_DEFAULT_DEVICE": "min",' - echo ' "IOS_APP_PROJECT": "MyApp.xcodeproj"' + echo ' "IOS_DEFAULT_DEVICE": "min"' echo ' }' } - -# Set configuration values -# Args: key=value pairs -ios_config_set() { - echo "Configuration is now managed via environment variables." >&2 - echo "" >&2 - echo "To override configuration values, add them to your devbox.json:" >&2 - echo "" >&2 - echo '{' >&2 - echo ' "include": [' >&2 - echo ' "plugin:ios"' >&2 - echo ' ],' >&2 - echo ' "env": {' >&2 - - # Show the key=value pairs they wanted to set as examples - if [ -n "${1-}" ]; then - echo " # Add these overrides:" >&2 - while [ "${1-}" != "" ]; do - pair="$1" - key="${pair%%=*}" - value="${pair#*=}" - echo " \"${key}\": \"${value}\"," >&2 - shift - done - fi - - echo ' }' >&2 - echo '}' >&2 - echo "" >&2 - echo "After updating devbox.json, run 'devbox shell' to apply changes." >&2 - return 1 -} - -# Reset configuration to defaults -ios_config_reset() { - echo "Configuration is now managed via environment variables." >&2 - echo "" >&2 - echo "To reset to defaults, remove any IOS_* environment variable" >&2 - echo "overrides from your devbox.json env section." >&2 - echo "" >&2 - echo "Plugin defaults are defined in the ios plugin.json file." >&2 - echo "Run 'ios.sh config show' to see current values." >&2 - return 1 -} diff --git a/plugins/ios/virtenv/scripts/user/devices.sh b/plugins/ios/virtenv/scripts/user/devices.sh index 6c188b1..126faa1 100644 --- a/plugins/ios/virtenv/scripts/user/devices.sh +++ b/plugins/ios/virtenv/scripts/user/devices.sh @@ -209,8 +209,7 @@ case "$command_name" in echo "$devices_json" | jq \ --arg cs "$checksum" \ - --arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date +%Y-%m-%dT%H:%M:%SZ)" \ - '{devices: ., checksum: $cs, generated_at: $ts}' > "$temp_lock" + '{devices: ., checksum: $cs}' > "$temp_lock" mv "$temp_lock" "$lock_path" device_count="$(echo "$devices_json" | jq '. | length')" @@ -252,8 +251,7 @@ case "$command_name" in echo "$devices_json" | jq \ --arg cs "$checksum" \ - --arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date +%Y-%m-%dT%H:%M:%SZ)" \ - '{devices: ., checksum: $cs, generated_at: $ts}' > "$temp_lock" + '{devices: ., checksum: $cs}' > "$temp_lock" mv "$temp_lock" "$lock_path" echo "Lock file generated: ${device_count} devices" diff --git a/plugins/ios/virtenv/scripts/user/ios.sh b/plugins/ios/virtenv/scripts/user/ios.sh index 9e95508..bee3734 100644 --- a/plugins/ios/virtenv/scripts/user/ios.sh +++ b/plugins/ios/virtenv/scripts/user/ios.sh @@ -6,21 +6,38 @@ usage() { Usage: ios.sh [args] Commands: + deploy [app_path] Install and launch app on running simulator devices [args] simulator start [device] [--pure] simulator stop + simulator ready simulator reset + app status Check if deployed app is running + app stop Stop the deployed app + run [--app path] [--device name] Start simulator, install, and launch app + xcodebuild [args...] config show info Examples: + ios.sh deploy + ios.sh deploy /path/to/MyApp.app ios.sh devices list ios.sh devices create iphone15 --runtime 17.5 ios.sh simulator start max ios.sh simulator start max --pure ios.sh simulator stop + ios.sh simulator ready ios.sh simulator reset + ios.sh app status + ios.sh app stop + ios.sh run + ios.sh run max + ios.sh run /path/to/MyApp.app + ios.sh xcodebuild -project MyApp.xcodeproj -scheme MyApp build ios.sh config show + +Note: Build your app with xcodebuild directly (e.g., cd ios && xcodebuild ...) USAGE exit 1 } @@ -36,6 +53,21 @@ if [ -n "${IOS_SCRIPTS_DIR:-}" ] && [ -d "${IOS_SCRIPTS_DIR}" ]; then script_dir="${IOS_SCRIPTS_DIR}" fi +# Derive suite-namespaced state directory +ios_state_dir() { + _suite="${SUITE_NAME:-default}" + _runtime_dir="${IOS_RUNTIME_DIR:-}" + if [ -z "$_runtime_dir" ]; then + # Fallback for environments where plugin.json hasn't set it + if [ -n "${DEVBOX_PROJECT_ROOT:-}" ]; then + _runtime_dir="${DEVBOX_PROJECT_ROOT}/.devbox/virtenv/ios/runtime" + else + _runtime_dir="${PWD}/.devbox/virtenv/ios/runtime" + fi + fi + printf '%s/%s' "$_runtime_dir" "$_suite" +} + case "$command_name" in devices) exec "${script_dir}/user/devices.sh" "$@" @@ -49,8 +81,9 @@ case "$command_name" in case "$sub" in start) - # Parse arguments for device name and --pure flag + # Parse arguments for device name and flags pure_mode=0 + wait_ready=0 device_name="" while [ $# -gt 0 ]; do @@ -59,6 +92,10 @@ case "$command_name" in pure_mode=1 shift ;; + --wait-ready) + wait_ready=1 + shift + ;; *) if [ -z "$device_name" ]; then device_name="$1" @@ -68,6 +105,21 @@ case "$command_name" in esac done + # Auto-detect pure mode from devbox environment + if [ "${DEVBOX_PURE_SHELL:-}" = "1" ]; then + pure_mode=1 + fi + + # Allow overriding pure mode to reuse existing simulator + # Usage: devbox run --pure -e REUSE_SIM=1 ios.sh simulator start + if [ "${REUSE_SIM:-}" = "1" ]; then + pure_mode=0 + fi + + # Prepare state directory + state_dir="$(ios_state_dir)" + mkdir -p "$state_dir" + # If --pure mode, create a separate test-specific simulator if [ "$pure_mode" = "1" ]; then export IOS_SIMULATOR_PURE=1 @@ -102,8 +154,9 @@ case "$command_name" in runtime_id="$(printf '%s' "$choice" | cut -d'|' -f1)" runtime_name="$(printf '%s' "$choice" | cut -d'|' -f2)" - # Create test-specific simulator name - test_sim_name="${device_base} (${runtime_name}) Test" + # Create test-specific simulator name (includes suite name for isolation) + suite_label="${SUITE_NAME:-default}" + test_sim_name="${device_base} (${runtime_name}) Test-${suite_label}" # Delete test simulator if it already exists existing_test_udid="$(xcrun simctl list devices -j | jq -r --arg name "$test_sim_name" '.devices[]?[]? | select(.name == $name) | .udid' | head -n1)" @@ -145,6 +198,10 @@ case "$command_name" in IOS_TEST_SIMULATOR="$test_udid" export IOS_SIM_UDID IOS_SIM_NAME IOS_TEST_SIMULATOR + # Save state to runtime dir + echo "$test_udid" > "$state_dir/simulator-udid.txt" + echo "$test_udid" > "$state_dir/test-simulator-udid.txt" + # Open Simulator app if not headless headless="${SIM_HEADLESS:-}" if [ -z "$headless" ]; then @@ -159,23 +216,81 @@ case "$command_name" in export IOS_DEFAULT_DEVICE="$device_name" fi ios_start + + # Save UDID to state dir + if [ -n "${IOS_SIM_UDID:-}" ]; then + echo "$IOS_SIM_UDID" > "$state_dir/simulator-udid.txt" + fi + fi + + # If --wait-ready, wait for simulator to be ready and exit (detach mode for dev) + # Otherwise in pure mode, keep running (process-compose manages lifecycle) + if [ "$wait_ready" = "1" ]; then + echo "Waiting for simulator to be ready..." + max_wait=60 + elapsed=0 + while ! ios_simulator_ready; do + sleep 2 + elapsed=$((elapsed + 2)) + if [ $elapsed -ge $max_wait ]; then + echo "ERROR: Simulator did not become ready within ${max_wait}s" >&2 + exit 1 + fi + done + echo "โœ“ Simulator ready and running in background" + exit 0 fi ;; stop) - # shellcheck disable=SC1090 - . "${script_dir}/domain/simulator.sh" + state_dir="$(ios_state_dir)" + + # Check for test simulator in state dir + test_udid="" + if [ -f "$state_dir/test-simulator-udid.txt" ]; then + test_udid="$(cat "$state_dir/test-simulator-udid.txt")" + fi - # If this was a test simulator, delete it after stopping - if [ -n "${IOS_TEST_SIMULATOR:-}" ]; then + if [ -n "$test_udid" ]; then echo "Stopping and deleting test simulator..." - xcrun simctl shutdown "$IOS_TEST_SIMULATOR" >/dev/null 2>&1 || true - xcrun simctl delete "$IOS_TEST_SIMULATOR" >/dev/null 2>&1 || true - echo "Test simulator deleted: $IOS_TEST_SIMULATOR" - unset IOS_TEST_SIMULATOR + xcrun simctl shutdown "$test_udid" >/dev/null 2>&1 || true + xcrun simctl delete "$test_udid" >/dev/null 2>&1 || true + echo "Test simulator deleted: $test_udid" + + # Clean up all state files + rm -f "$state_dir/simulator-udid.txt" + rm -f "$state_dir/test-simulator-udid.txt" + rm -f "$state_dir/bundle-id.txt" else # Normal mode - just stop the simulator ios_stop + rm -f "$state_dir/simulator-udid.txt" 2>/dev/null || true + fi + ;; + ready) + # Silent readiness probe: exit 0 if booted, 1 if not + state_dir="$(ios_state_dir)" + udid="" + + # Try state file first + if [ -f "$state_dir/simulator-udid.txt" ]; then + udid="$(cat "$state_dir/simulator-udid.txt")" fi + + # Fallback: find any booted simulator + if [ -z "$udid" ]; then + udid="$(xcrun simctl list devices -j | jq -r '.devices[]?[]? | select(.state == "Booted") | .udid' | head -n1 || true)" + fi + + if [ -z "$udid" ]; then + exit 1 + fi + + # Check if booted + sim_state="$(xcrun simctl list devices -j | jq -r --arg udid "$udid" '.devices[]?[]? | select(.udid == $udid) | .state' | head -n1 || true)" + if [ "$sim_state" = "Booted" ]; then + exit 0 + fi + exit 1 ;; reset) # Stop all simulators and delete those matching device definitions @@ -187,7 +302,7 @@ case "$command_name" in # Stop all running simulators echo "Stopping all running simulators..." xcrun simctl shutdown all >/dev/null 2>&1 || true - echo " โœ“ All simulators stopped" + echo " All simulators stopped" echo "" # Get device definitions @@ -226,7 +341,7 @@ case "$command_name" in echo "" echo "================================================" - echo "โœ“ Reset complete!" + echo "Reset complete!" echo "================================================" echo "" echo "Deleted $deleted_count simulator(s) matching device definitions." @@ -240,6 +355,179 @@ case "$command_name" in ;; esac ;; + deploy) + # Install and launch app on already-running simulator (no build, no sim start) + # shellcheck disable=SC1090 + . "${script_dir}/domain/deploy.sh" + + app_arg="${1:-}" + + state_dir="$(ios_state_dir)" + + # Resolve UDID from state file or find booted simulator + udid="" + if [ -f "$state_dir/simulator-udid.txt" ]; then + udid="$(cat "$state_dir/simulator-udid.txt")" + fi + if [ -z "$udid" ]; then + udid="$(xcrun simctl list devices -j | jq -r '.devices[]?[]? | select(.state == "Booted") | .udid' | head -n1 || true)" + fi + if [ -z "$udid" ]; then + echo "ERROR: No booted simulator found. Run 'ios.sh simulator start' first." >&2 + exit 1 + fi + + # Resolve app path + if [ -n "$app_arg" ]; then + app_path="$app_arg" + if [ "${app_path#/}" = "$app_path" ]; then + app_path="$PWD/$app_path" + fi + if [ ! -d "$app_path" ]; then + echo "ERROR: App bundle not found: $app_path" >&2 + exit 1 + fi + else + project_root="$(ios_resolve_project_root)" + app_path="$(ios_find_app "$project_root" || true)" + if [ -z "$app_path" ] || [ ! -d "$app_path" ]; then + echo "ERROR: No .app bundle found. Build first (e.g., xcodebuild) or define a build:ios script in devbox.json." >&2 + exit 1 + fi + fi + + echo "App: $(basename "$app_path")" + + # Extract bundle ID + bundle_id="${IOS_XCODEBUILD_BUNDLE_ID:-}" + if [ -z "$bundle_id" ]; then + bundle_id="$(ios_extract_bundle_id "$app_path")" + fi + if [ -z "$bundle_id" ]; then + echo "ERROR: Unable to resolve bundle identifier" >&2 + exit 1 + fi + + # Terminate and uninstall existing app for a clean deploy + xcrun simctl terminate "$udid" "$bundle_id" 2>/dev/null || true + if xcrun simctl get_app_container "$udid" "$bundle_id" >/dev/null 2>&1; then + echo "Removing existing install: $bundle_id" + xcrun simctl uninstall "$udid" "$bundle_id" 2>/dev/null || true + fi + + # Install and launch + echo "Installing on simulator: $udid" + xcrun simctl install "$udid" "$app_path" + + echo "Launching: $bundle_id" + xcrun simctl launch "$udid" "$bundle_id" + + # Save state + mkdir -p "$state_dir" + echo "$bundle_id" > "$state_dir/bundle-id.txt" + + echo "Deploy complete" + ;; + app) + sub="${1-}" + shift || true + + state_dir="$(ios_state_dir)" + + case "$sub" in + status) + # Check if deployed app is running (exit 0 if running, 1 if not) + bundle_id="" + if [ -f "$state_dir/bundle-id.txt" ]; then + bundle_id="$(cat "$state_dir/bundle-id.txt")" + fi + if [ -z "$bundle_id" ]; then + exit 1 + fi + + udid="" + if [ -f "$state_dir/simulator-udid.txt" ]; then + udid="$(cat "$state_dir/simulator-udid.txt")" + fi + if [ -z "$udid" ]; then + udid="$(xcrun simctl list devices -j | jq -r '.devices[]?[]? | select(.state == "Booted") | .udid' | head -n1 || true)" + fi + if [ -z "$udid" ]; then + exit 1 + fi + + if xcrun simctl spawn "$udid" launchctl list 2>/dev/null | grep -q "$bundle_id"; then + exit 0 + fi + exit 1 + ;; + stop) + # Stop the deployed app + bundle_id="" + if [ -f "$state_dir/bundle-id.txt" ]; then + bundle_id="$(cat "$state_dir/bundle-id.txt")" + fi + + udid="" + if [ -f "$state_dir/simulator-udid.txt" ]; then + udid="$(cat "$state_dir/simulator-udid.txt")" + fi + if [ -z "$udid" ]; then + udid="$(xcrun simctl list devices -j | jq -r '.devices[]?[]? | select(.state == "Booted") | .udid' | head -n1 || true)" + fi + + if [ -n "$udid" ] && [ -n "$bundle_id" ]; then + xcrun simctl terminate "$udid" "$bundle_id" 2>/dev/null || true + echo "App stopped: $bundle_id" + else + echo "No app to stop" + fi + ;; + *) + echo "ERROR: Unknown app subcommand: $sub" >&2 + echo "Usage: ios.sh app " >&2 + exit 1 + ;; + esac + ;; + run) + # shellcheck disable=SC1090 + . "${script_dir}/domain/deploy.sh" + + # Parse arguments + _run_app_arg="" + _run_device="" + while [ $# -gt 0 ]; do + case "$1" in + --app) + _run_app_arg="${2:-}" + shift 2 + ;; + --device) + _run_device="${2:-}" + shift 2 + ;; + *) + # Bare positional arg: treat as device name for convenience + if [ -z "$_run_device" ]; then + _run_device="$1" + fi + shift + ;; + esac + done + + # Forward with explicit flags + _run_args="" + if [ -n "$_run_app_arg" ]; then + _run_args="--app $_run_app_arg" + fi + if [ -n "$_run_device" ]; then + _run_args="$_run_args --device $_run_device" + fi + # shellcheck disable=SC2086 + ios_run_app $_run_args + ;; config) sub="${1-}" shift || true @@ -249,12 +537,6 @@ case "$command_name" in show) ios_config_show ;; - set) - ios_config_set "$@" - ;; - reset) - ios_config_reset - ;; *) usage ;; @@ -271,6 +553,11 @@ case "$command_name" in exit 1 fi ;; + xcodebuild) + # shellcheck disable=SC1090 + . "${script_dir}/platform/core.sh" + xcodebuild "$@" + ;; *) usage ;; diff --git a/plugins/react-native/REFERENCE.md b/plugins/react-native/REFERENCE.md index b1ce877..0e9f4b5 100644 --- a/plugins/react-native/REFERENCE.md +++ b/plugins/react-native/REFERENCE.md @@ -21,8 +21,8 @@ This significantly speeds up iOS workflows and prevents unnecessary Android SDK ## Commands -- Android: `devbox run --pure start-emu`, `devbox run --pure start-android`, `devbox run --pure stop-emu` -- iOS: `devbox run --pure start-sim`, `devbox run --pure start-ios`, `devbox run --pure stop-sim` +- Android: `devbox run --pure start:emu`, `devbox run --pure start:android`, `devbox run --pure stop:emu` +- iOS: `devbox run --pure start:sim`, `devbox run --pure start:ios`, `devbox run --pure stop:sim` - Web/Metro: `devbox run --pure start-web` ## Files diff --git a/plugins/react-native/plugin.json b/plugins/react-native/plugin.json index bbb5cb2..d4c406f 100644 --- a/plugins/react-native/plugin.json +++ b/plugins/react-native/plugin.json @@ -1,10 +1,10 @@ { "name": "react-native", - "version": "0.0.3", + "version": "0.0.4", "description": "Aggregates the Android and iOS Devbox plugins for React Native projects.", "include": [ - "github:segment-integrations/devbox-plugins?dir=plugins/android&ref=main", - "github:segment-integrations/devbox-plugins?dir=plugins/ios&ref=main" + "path:../android/plugin.json", + "path:../ios/plugin.json" ], "packages": { "curl": "latest", @@ -40,7 +40,7 @@ }, "shell": { "init_hook": [ - "source {{ .Virtenv }}/scripts/init/init-hook.sh", + ". {{ .Virtenv }}/scripts/init/init-hook.sh", "export NODE_BINARY=\"$(command -v node)\"" ], "scripts": { diff --git a/plugins/react-native/tests/test-metro-shutdown.yaml b/plugins/react-native/tests/test-metro-shutdown.yaml index fc7db3f..5e726b2 100644 --- a/plugins/react-native/tests/test-metro-shutdown.yaml +++ b/plugins/react-native/tests/test-metro-shutdown.yaml @@ -4,7 +4,7 @@ environment: - "TEST_SUITE_NAME=shutdown-test" - "TEST_TUI=false" -log_location: "test-results/metro-shutdown-logs" +log_location: "reports/metro-shutdown-logs" log_level: info is_strict: false @@ -133,7 +133,7 @@ processes: echo " โœ“ Metro stopped cleanly" echo " โœ“ No processes left running" echo "" - echo "Logs: test-results/metro-shutdown-logs" + echo "Logs: reports/metro-shutdown-logs" echo "====================================" echo "" diff --git a/plugins/react-native/virtenv/scripts/init/init-hook.sh b/plugins/react-native/virtenv/scripts/init/init-hook.sh index af97706..df3f6e6 100644 --- a/plugins/react-native/virtenv/scripts/init/init-hook.sh +++ b/plugins/react-native/virtenv/scripts/init/init-hook.sh @@ -1,8 +1,8 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh # React Native Plugin - Initialization Hook # Adds React Native scripts to PATH - -set -e +# NOTE: This file is sourced (not executed) by devbox init_hook, +# so it must be POSIX sh compatible (no bash-isms). # Add React Native scripts to PATH if not already present if [ -n "${REACT_NATIVE_SCRIPTS_DIR:-}" ] && [ -d "${REACT_NATIVE_SCRIPTS_DIR}" ]; then @@ -13,8 +13,9 @@ if [ -n "${REACT_NATIVE_SCRIPTS_DIR:-}" ] && [ -d "${REACT_NATIVE_SCRIPTS_DIR}" chmod +x "$USER_SCRIPTS_DIR"/*.sh 2>/dev/null || true # Add to PATH if not already present - if [[ ":$PATH:" != *":$USER_SCRIPTS_DIR:"* ]]; then - export PATH="$USER_SCRIPTS_DIR:$PATH" - fi + case ":$PATH:" in + *":$USER_SCRIPTS_DIR:"*) ;; + *) export PATH="$USER_SCRIPTS_DIR:$PATH" ;; + esac fi fi diff --git a/plugins/react-native/virtenv/scripts/lib/lib.sh b/plugins/react-native/virtenv/scripts/lib/lib.sh index 344b8a0..b083261 100755 --- a/plugins/react-native/virtenv/scripts/lib/lib.sh +++ b/plugins/react-native/virtenv/scripts/lib/lib.sh @@ -1,7 +1,7 @@ #!/usr/bin/env sh # React Native Plugin - Core Utilities -set -eu +set -e if ! (return 0 2>/dev/null); then echo "ERROR: lib.sh must be sourced" >&2 diff --git a/plugins/tests/README.md b/plugins/tests/README.md index bc9b59b..82f5410 100644 --- a/plugins/tests/README.md +++ b/plugins/tests/README.md @@ -7,11 +7,21 @@ This directory contains **pure unit tests** for plugin scripts. These tests veri ``` plugins/tests/ โ”œโ”€โ”€ android/ -โ”‚ โ”œโ”€โ”€ test-lib.sh # Tests for lib.sh utility functions -โ”‚ โ””โ”€โ”€ test-devices.sh # Tests for devices.sh CLI parsing +โ”‚ โ”œโ”€โ”€ test-lib.sh # Tests for lib.sh utility functions +โ”‚ โ”œโ”€โ”€ test-devices.sh # Tests for devices.sh CLI parsing +โ”‚ โ”œโ”€โ”€ test-apk-detection.sh # Tests for APK metadata extraction +โ”‚ โ”œโ”€โ”€ test-apk-resolution.sh # Tests for APK auto-detection +โ”‚ โ”œโ”€โ”€ test-emulator-detection.sh # Emulator detection tests +โ”‚ โ””โ”€โ”€ test-emulator-modes.sh # Emulator mode behavior docs โ”œโ”€โ”€ ios/ -โ”‚ โ””โ”€โ”€ test-lib.sh # Tests for lib.sh utility functions -โ””โ”€โ”€ test-framework.sh # Shared test utilities +โ”‚ โ”œโ”€โ”€ test-lib.sh # Tests for lib.sh utility functions +โ”‚ โ”œโ”€โ”€ test-devices.sh # Tests for devices.sh CLI parsing +โ”‚ โ”œโ”€โ”€ test-app-resolution.sh # Tests for .app auto-detection +โ”‚ โ”œโ”€โ”€ test-simulator-detection.sh # Simulator detection tests +โ”‚ โ””โ”€โ”€ test-simulator-modes.sh # Simulator mode behavior docs +โ”œโ”€โ”€ react-native/ +โ”‚ โ””โ”€โ”€ test-lib.sh # Tests for Metro port management +โ””โ”€โ”€ test-framework.sh # Shared test utilities ``` ## Running Tests @@ -51,45 +61,79 @@ devbox run test:plugin:ios:lib - Config directory resolution - Requirement validation +### React Native (`test-lib.sh`) +- Metro port allocation and retrieval +- Metro environment file management +- PID tracking +- Suite isolation + ## Test Framework -All tests use `test-framework.sh` utilities: +All tests source `test-framework.sh` which provides assertions, fixture helpers, and test summary reporting. ```bash #!/usr/bin/env bash set -euo pipefail -# Source test framework -. "path/to/test-framework.sh" +script_dir="$(cd "$(dirname "$0")" && pwd)" +. "$script_dir/../test-framework.sh" +setup_logging # Write tests -assert_equal "expected" "$(my_function)" -assert_command_success "Test description" my_command arg1 arg2 -assert_file_exists "path/to/file" +start_test "My feature" +assert_equal "expected" "$(my_function)" "Description" +assert_success "some_command" "Should succeed" +assert_failure "bad_command" "Should fail" + +# Use example project fixtures for read-only tests +devices_dir="$(fixture_android_devices_dir)" + +# Use project-local temp dirs for write tests +temp="$(make_temp_dir "my-test")" +# ... use temp dir ... +rm -rf "$temp" # Show summary -test_summary +test_summary "suite-name" ``` +### Available Assertions + +- `assert_equal expected actual message` - Value equality +- `assert_success "cmd" message` - Command succeeds (eval-based) +- `assert_failure "cmd" message` - Command fails (eval-based) +- `assert_not_empty value message` - Value is non-empty +- `assert_contains haystack needle message` - String contains substring +- `assert_output "cmd" expected message` - Command output contains string +- `assert_file_exists path message` - File exists +- `assert_file_contains path pattern message` - File contains pattern +- `assert_command_success message cmd args...` - Command succeeds (direct) + +### Fixture Helpers + +- `fixture_android_devices_dir` - Path to `examples/android/devbox.d/android/devices` +- `fixture_ios_devices_dir` - Path to `examples/ios/devbox.d/ios/devices` +- `make_temp_dir label` - Creates dir under `reports/tmp/` (project-local) + ## Adding New Tests 1. Create test file in appropriate directory (`plugins/tests/{platform}/`) -2. Use `test-framework.sh` utilities -3. Add command to `devbox.json` if needed -4. Ensure tests run in isolation (no external dependencies) +2. Source `test-framework.sh` and call `setup_logging` +3. Use example project fixtures for read-only tests, `make_temp_dir` for write tests +4. Call `test_summary "suite-name"` at the end +5. Add command to `devbox.json` if needed ## Guidelines - **Pure unit tests only** - Test individual functions directly -- **No integration** - Integration tests are in `/tests/integration/` +- **No /tmp/** - Use `make_temp_dir()` for project-local temp directories +- **Example fixtures** - Use `fixture_*_devices_dir()` for read-only device config tests - **Fast execution** - All tests should run in under 30 seconds total -- **Isolated** - Tests should not depend on external state or example projects -- **Self-contained** - Create any needed test data inline or in the test file +- **Isolated** - Tests clean up after themselves ## Related Testing -- **Integration tests**: `/tests/integration/` - Test plugin workflows with fixtures +- **Integration tests**: `/tests/integration/` - Test plugin workflows - **E2E tests**: `/tests/e2e/` - Test full application lifecycle -- **Test fixtures**: `/tests/fixtures/` - Shared test data for integration tests See `/tests/README.md` for complete testing guide. diff --git a/plugins/tests/android/test-apk-detection.sh b/plugins/tests/android/test-apk-detection.sh index abd443a..a569728 100644 --- a/plugins/tests/android/test-apk-detection.sh +++ b/plugins/tests/android/test-apk-detection.sh @@ -6,123 +6,14 @@ set -euo pipefail -# Setup logging -SCRIPT_DIR_NAME="$(basename "$(dirname "$0")")" -SCRIPT_NAME="$(basename "$0" .sh)" -mkdir -p "${TEST_LOGS_DIR:-reports/logs}" -LOG_FILE="${TEST_LOGS_DIR:-reports/logs}/${SCRIPT_DIR_NAME}-${SCRIPT_NAME}.txt" -exec > >(tee "$LOG_FILE") -exec 2>&1 - -# ============================================================================ -# Test Framework -# ============================================================================ - -test_passed=0 -test_failed=0 -test_name="" - -start_test() { - test_name="$1" - echo "" - echo "TEST: $test_name" -} - -assert_equal() { - expected="$1" - actual="$2" - message="${3:-}" - - if [ "$expected" = "$actual" ]; then - echo " PASS${message:+: $message}" - test_passed=$((test_passed + 1)) - else - echo " FAIL${message:+: $message}" - echo " Expected: '$expected'" - echo " Actual: '$actual'" - test_failed=$((test_failed + 1)) - fi -} - -assert_not_empty() { - actual="$1" - message="${2:-}" - - if [ -n "$actual" ]; then - echo " PASS${message:+: $message}" - test_passed=$((test_passed + 1)) - else - echo " FAIL${message:+: $message}" - echo " Value was empty" - test_failed=$((test_failed + 1)) - fi -} - -assert_success() { - command_str="$1" - message="${2:-}" - - if eval "$command_str" >/dev/null 2>&1; then - echo " PASS${message:+: $message}" - test_passed=$((test_passed + 1)) - else - echo " FAIL${message:+: $message}" - echo " Command failed: $command_str" - test_failed=$((test_failed + 1)) - fi -} - -assert_failure() { - command_str="$1" - message="${2:-}" - - if ! (eval "$command_str") >/dev/null 2>&1; then - echo " PASS${message:+: $message}" - test_passed=$((test_passed + 1)) - else - echo " FAIL${message:+: $message}" - echo " Command should have failed: $command_str" - test_failed=$((test_failed + 1)) - fi -} - -test_summary() { - total=$((test_passed + test_failed)) - echo "" - echo "========================================" - echo "Test Summary" - echo "========================================" - echo "Total: $total" - echo "Passed: $test_passed" - echo "Failed: $test_failed" - echo "" - - # Write results file for summary aggregation - results_dir="${TEST_RESULTS_DIR:-$(cd "$(dirname "$0")/../../../reports/results" 2>/dev/null && pwd || echo "/tmp")}" - mkdir -p "$results_dir" 2>/dev/null || true - cat > "$results_dir/android-apk-detection.json" << EOF -{ - "suite": "android-apk-detection", - "passed": $test_passed, - "failed": $test_failed, - "total": $total -} -EOF - - if [ "$test_failed" -gt 0 ]; then - echo "RESULT: FAILED" - exit 1 - else - echo "RESULT: ALL PASSED" - exit 0 - fi -} +script_dir="$(cd "$(dirname "$0")" && pwd)" +. "$script_dir/../test-framework.sh" +setup_logging # ============================================================================ # Setup # ============================================================================ -script_dir="$(cd "$(dirname "$0")" && pwd)" deploy_path="$script_dir/../../android/virtenv/scripts/domain/deploy.sh" lib_path="$script_dir/../../android/virtenv/scripts/lib/lib.sh" core_path="$script_dir/../../android/virtenv/scripts/platform/core.sh" @@ -199,24 +90,23 @@ assert_equal "com.example.app/.MainActivity" "$component" "Should pass through c # Tests: APK Path Resolution # ============================================================================ -start_test "android_resolve_apk_path - finds APK by exact path" -resolved=$(android_resolve_apk_path "$script_dir/fixtures" "test-app.apk") +start_test "android_resolve_apk_glob - finds APK by exact path" +resolved=$(android_resolve_apk_glob "$script_dir/fixtures" "test-app.apk") assert_equal "$fixture_apk" "$resolved" "Should resolve exact APK path" -start_test "android_resolve_apk_path - finds APK by glob pattern" -resolved=$(android_resolve_apk_path "$script_dir/fixtures" "*.apk") +start_test "android_resolve_apk_glob - finds APK by glob pattern" +resolved=$(android_resolve_apk_glob "$script_dir/fixtures" "*.apk") assert_equal "$fixture_apk" "$resolved" "Should resolve glob pattern" -start_test "android_resolve_apk_path - fails on no match" -assert_failure "android_resolve_apk_path '$script_dir/fixtures' 'nonexistent-*.apk'" "Should fail when no APK matches" +start_test "android_resolve_apk_glob - fails on no match" +assert_failure "android_resolve_apk_glob '$script_dir/fixtures' 'nonexistent-*.apk'" "Should fail when no APK matches" # ============================================================================ # Tests: Auto-detection File Output # ============================================================================ start_test "deploy saves app-id.txt after metadata extraction" -test_runtime="/tmp/android-apk-test-$$" -mkdir -p "$test_runtime" +test_runtime="$(make_temp_dir "android-apk")" ANDROID_RUNTIME_DIR="$test_runtime" ANDROID_USER_HOME="$test_runtime" # Simulate what android_run_app does after extraction @@ -239,4 +129,4 @@ rm -rf "$test_runtime" # Summary # ============================================================================ -test_summary +test_summary "android-apk-detection" diff --git a/plugins/tests/android/test-apk-resolution.sh b/plugins/tests/android/test-apk-resolution.sh new file mode 100755 index 0000000..ee4cc40 --- /dev/null +++ b/plugins/tests/android/test-apk-resolution.sh @@ -0,0 +1,148 @@ +#!/usr/bin/env bash +# Android Plugin - APK Auto-Detection Resolution Tests +# +# Tests for the android_find_apk() precedence chain in deploy.sh. +# These tests use temporary directories with fixture APKs. + +set -euo pipefail + +script_dir="$(cd "$(dirname "$0")" && pwd)" +. "$script_dir/../test-framework.sh" +setup_logging + +# ============================================================================ +# Setup +# ============================================================================ + +deploy_path="$script_dir/../../android/virtenv/scripts/domain/deploy.sh" +lib_path="$script_dir/../../android/virtenv/scripts/lib/lib.sh" +core_path="$script_dir/../../android/virtenv/scripts/platform/core.sh" + +if [ ! -f "$deploy_path" ]; then + echo "ERROR: deploy.sh not found at: $deploy_path" + exit 1 +fi + +# Source dependencies +. "$lib_path" +. "$core_path" +. "$deploy_path" + +# Create temp project structures +TMPDIR_BASE="$(make_temp_dir "android-apk-resolution")" +trap 'rm -rf "$TMPDIR_BASE"' EXIT + +echo "========================================" +echo "Android APK Resolution Tests" +echo "========================================" +echo "Testing: $deploy_path" + +# ============================================================================ +# Tests: android_find_apk with ANDROID_APP_APK env var (precedence 1) +# ============================================================================ + +start_test "android_find_apk - resolves via ANDROID_APP_APK env var" +test_root="$TMPDIR_BASE/test1" +mkdir -p "$test_root/app/build/outputs/apk/debug" +touch "$test_root/app/build/outputs/apk/debug/app-debug.apk" +ANDROID_APP_APK="app/build/outputs/apk/debug/app-debug.apk" \ + result=$(android_find_apk "$test_root" 2>/dev/null) +assert_equal "$test_root/app/build/outputs/apk/debug/app-debug.apk" "$result" \ + "Should find APK via ANDROID_APP_APK relative path" + +start_test "android_find_apk - resolves via ANDROID_APP_APK glob" +test_root="$TMPDIR_BASE/test1b" +mkdir -p "$test_root/app/build/outputs/apk/debug" +touch "$test_root/app/build/outputs/apk/debug/app-debug.apk" +ANDROID_APP_APK="app/build/outputs/apk/debug/*.apk" \ + result=$(android_find_apk "$test_root" 2>/dev/null) +assert_equal "$test_root/app/build/outputs/apk/debug/app-debug.apk" "$result" \ + "Should find APK via ANDROID_APP_APK glob pattern" + +start_test "android_find_apk - ANDROID_APP_APK takes priority over recursive search" +test_root="$TMPDIR_BASE/test1c" +mkdir -p "$test_root/specific" "$test_root/other" +touch "$test_root/specific/target.apk" +touch "$test_root/other/decoy.apk" +ANDROID_APP_APK="specific/target.apk" \ + result=$(android_find_apk "$test_root" 2>/dev/null) +assert_equal "$test_root/specific/target.apk" "$result" \ + "ANDROID_APP_APK should take priority" + +# ============================================================================ +# Tests: android_find_apk with recursive search (precedence 2) +# ============================================================================ + +start_test "android_find_apk - finds APK via recursive search" +test_root="$TMPDIR_BASE/test2" +mkdir -p "$test_root/app/build/outputs/apk/debug" +touch "$test_root/app/build/outputs/apk/debug/app-debug.apk" +ANDROID_APP_APK="" \ + result=$(android_find_apk "$test_root" 2>/dev/null) +assert_equal "$test_root/app/build/outputs/apk/debug/app-debug.apk" "$result" \ + "Should find APK via recursive search" + +start_test "android_find_apk - excludes .gradle directory" +test_root="$TMPDIR_BASE/test2b" +mkdir -p "$test_root/.gradle/caches" "$test_root/app/build/outputs" +touch "$test_root/.gradle/caches/cached.apk" +touch "$test_root/app/build/outputs/real.apk" +ANDROID_APP_APK="" \ + result=$(android_find_apk "$test_root" 2>/dev/null) +assert_equal "$test_root/app/build/outputs/real.apk" "$result" \ + "Should skip .gradle directory" + +start_test "android_find_apk - excludes build/intermediates directory" +test_root="$TMPDIR_BASE/test2c" +mkdir -p "$test_root/build/intermediates/apk" "$test_root/build/outputs" +touch "$test_root/build/intermediates/apk/intermediate.apk" +touch "$test_root/build/outputs/final.apk" +ANDROID_APP_APK="" \ + result=$(android_find_apk "$test_root" 2>/dev/null) +assert_equal "$test_root/build/outputs/final.apk" "$result" \ + "Should skip build/intermediates" + +start_test "android_find_apk - excludes node_modules directory" +test_root="$TMPDIR_BASE/test2d" +mkdir -p "$test_root/node_modules/some-pkg" "$test_root/android/app" +touch "$test_root/node_modules/some-pkg/bundled.apk" +touch "$test_root/android/app/app.apk" +ANDROID_APP_APK="" \ + result=$(android_find_apk "$test_root" 2>/dev/null) +assert_equal "$test_root/android/app/app.apk" "$result" \ + "Should skip node_modules" + +start_test "android_find_apk - excludes .devbox directory" +test_root="$TMPDIR_BASE/test2e" +mkdir -p "$test_root/.devbox/virtenv" "$test_root/app" +touch "$test_root/.devbox/virtenv/cached.apk" +touch "$test_root/app/real.apk" +ANDROID_APP_APK="" \ + result=$(android_find_apk "$test_root" 2>/dev/null) +assert_equal "$test_root/app/real.apk" "$result" \ + "Should skip .devbox" + +# ============================================================================ +# Tests: android_find_apk fails with clear error (precedence 4) +# ============================================================================ + +start_test "android_find_apk - fails when no APK found" +test_root="$TMPDIR_BASE/test4" +mkdir -p "$test_root" +# Run from a clean dir so $PWD fallback search doesn't find fixture APKs +(cd "$test_root" && ANDROID_APP_APK="" \ + assert_failure "android_find_apk '$test_root'" "Should fail when no APK exists") + +start_test "android_find_apk - error message includes guidance" +test_root="$TMPDIR_BASE/test4b" +mkdir -p "$test_root" +# Run from a clean dir so $PWD fallback search doesn't find fixture APKs +error_output=$(cd "$test_root" && ANDROID_APP_APK="" android_find_apk "$test_root" 2>&1 || true) +assert_contains "$error_output" "No APK found" "Error should mention no APK found" +assert_contains "$error_output" "ANDROID_APP_APK" "Error should mention env var" + +# ============================================================================ +# Summary +# ============================================================================ + +test_summary "android-apk-resolution" diff --git a/plugins/tests/android/test-devices.sh b/plugins/tests/android/test-devices.sh index 86f289e..6ef22f3 100644 --- a/plugins/tests/android/test-devices.sh +++ b/plugins/tests/android/test-devices.sh @@ -3,26 +3,9 @@ set -euo pipefail -# Setup logging - redirect all output to log file -SCRIPT_DIR_NAME="$(basename "$(dirname "$0")")" -SCRIPT_NAME="$(basename "$0" .sh)" -mkdir -p "${TEST_LOGS_DIR:-reports/logs}" -LOG_FILE="${TEST_LOGS_DIR:-reports/logs}/${SCRIPT_DIR_NAME}-${SCRIPT_NAME}.txt" -exec > >(tee "$LOG_FILE") -exec 2>&1 - -test_passed=0 -test_failed=0 - -assert_success() { - if eval "$1" >/dev/null 2>&1; then - echo " โœ“ PASS: $2" - test_passed=$((test_passed + 1)) - else - echo " โœ— FAIL: $2" - test_failed=$((test_failed + 1)) - fi -} +script_dir="$(cd "$(dirname "$0")" && pwd)" +. "$script_dir/../test-framework.sh" +setup_logging echo "========================================" echo "Android devices.sh Integration Tests" @@ -30,7 +13,7 @@ echo "========================================" echo "" # Setup test environment -test_root="/tmp/android-plugin-device-test-$$" +test_root="$(make_temp_dir "android-devices")" mkdir -p "$test_root/devices" mkdir -p "$test_root/scripts/lib" mkdir -p "$test_root/scripts/platform" @@ -38,7 +21,6 @@ mkdir -p "$test_root/scripts/domain" mkdir -p "$test_root/scripts/user" # Copy required scripts with new layer structure -script_dir="$(cd "$(dirname "$0")" && pwd)" cp "$script_dir/../../android/virtenv/scripts/lib/lib.sh" "$test_root/scripts/lib/" cp "$script_dir/../../android/virtenv/scripts/platform/core.sh" "$test_root/scripts/platform/" cp "$script_dir/../../android/virtenv/scripts/platform/device_config.sh" "$test_root/scripts/platform/" @@ -93,31 +75,8 @@ assert_success "[ ! -f '$test_root/devices/test_pixel.json' ]" "Device file remo # Cleanup rm -rf "$test_root" -# Summary -echo "" -echo "========================================" -total=$((test_passed + test_failed)) -echo "Total: $total" -echo "Passed: $test_passed" -echo "Failed: $test_failed" -echo "" +# ============================================================================ +# Test Summary +# ============================================================================ -# Write results file for summary aggregation -results_dir="${TEST_RESULTS_DIR:-$(cd "$(dirname "$0")/../../../reports/results" 2>/dev/null && pwd || echo "/tmp")}" -mkdir -p "$results_dir" 2>/dev/null || true -cat > "$results_dir/android-devices.json" << EOF -{ - "suite": "android-devices", - "passed": $test_passed, - "failed": $test_failed, - "total": $total -} -EOF - -if [ "$test_failed" -gt 0 ]; then - echo "RESULT: โœ— FAILED" - exit 1 -else - echo "RESULT: โœ“ ALL PASSED" - exit 0 -fi +test_summary "android-devices" diff --git a/plugins/tests/android/test-emulator-detection.sh b/plugins/tests/android/test-emulator-detection.sh index 2921c3b..7a77aaf 100755 --- a/plugins/tests/android/test-emulator-detection.sh +++ b/plugins/tests/android/test-emulator-detection.sh @@ -4,118 +4,10 @@ set -euo pipefail -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[0;33m' -NC='\033[0m' # No Color - -# Counters -tests_run=0 -tests_passed=0 -tests_failed=0 - -# Test result tracking -test_results=() - -# Helper functions -log_test() { - echo "" - echo "========================================" - echo "TEST: $1" - echo "========================================" - tests_run=$((tests_run + 1)) -} - -assert_success() { - local cmd="$1" - local description="$2" - - if eval "$cmd" >/dev/null 2>&1; then - echo -e "${GREEN}โœ“${NC} $description" - tests_passed=$((tests_passed + 1)) - test_results+=("PASS: $description") - return 0 - else - echo -e "${RED}โœ—${NC} $description" - echo " Command failed: $cmd" - tests_failed=$((tests_failed + 1)) - test_results+=("FAIL: $description") - return 1 - fi -} - -assert_failure() { - local cmd="$1" - local description="$2" - - if ! eval "$cmd" >/dev/null 2>&1; then - echo -e "${GREEN}โœ“${NC} $description" - tests_passed=$((tests_passed + 1)) - test_results+=("PASS: $description") - return 0 - else - echo -e "${RED}โœ—${NC} $description" - echo " Command should have failed: $cmd" - tests_failed=$((tests_failed + 1)) - test_results+=("FAIL: $description") - return 1 - fi -} - -assert_output() { - local cmd="$1" - local expected="$2" - local description="$3" - - local output - output=$(eval "$cmd" 2>&1 || true) - - if echo "$output" | grep -q "$expected"; then - echo -e "${GREEN}โœ“${NC} $description" - tests_passed=$((tests_passed + 1)) - test_results+=("PASS: $description") - return 0 - else - echo -e "${RED}โœ—${NC} $description" - echo " Expected to contain: $expected" - echo " Got: $output" - tests_failed=$((tests_failed + 1)) - test_results+=("FAIL: $description") - return 1 - fi -} - -print_summary() { - echo "" - echo "========================================" - echo "TEST SUMMARY" - echo "========================================" - echo "Total tests: $tests_run" - echo -e "${GREEN}Passed: $tests_passed${NC}" - if [ "$tests_failed" -gt 0 ]; then - echo -e "${RED}Failed: $tests_failed${NC}" - else - echo "Failed: $tests_failed" - fi - echo "" - - if [ "$tests_failed" -gt 0 ]; then - echo "Failed tests:" - for result in "${test_results[@]}"; do - if [[ "$result" == FAIL:* ]]; then - echo -e " ${RED}โœ—${NC} ${result#FAIL: }" - fi - done - echo "" - return 1 - fi - - return 0 -} - -# Setup: Source the emulator script functions +# Setup: Source the framework and emulator scripts SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "$SCRIPT_DIR/../test-framework.sh" + REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" # Source Android setup @@ -189,10 +81,10 @@ if [ "$existing_count" -eq 0 ]; then adb -s "$emulator_serial" emu kill >/dev/null 2>&1 || true sleep 2 else - echo -e "${YELLOW}โš ${NC} Could not start emulator (this is OK if no AVDs exist yet)" + echo "Could not start emulator (this is OK if no AVDs exist yet)" fi else - echo -e "${YELLOW}โš ${NC} Could not start emulator (this is OK if no AVDs exist yet)" + echo "Could not start emulator (this is OK if no AVDs exist yet)" fi else echo "Using existing emulator(s) for tests..." @@ -227,5 +119,5 @@ echo "Normal mode reuses existing emulator if AVD matches" assert_success "true" "Pure mode logic documented" assert_success "true" "Normal mode logic documented" -# Print summary -print_summary +# Summary +test_summary "android-emulator-detection" diff --git a/plugins/tests/android/test-emulator-modes.sh b/plugins/tests/android/test-emulator-modes.sh index 37e5adf..8f0b19a 100755 --- a/plugins/tests/android/test-emulator-modes.sh +++ b/plugins/tests/android/test-emulator-modes.sh @@ -4,20 +4,10 @@ set -euo pipefail -# Colors -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[0;33m' -BLUE='\033[0;34m' -NC='\033[0m' - -echo "========================================" -echo "Emulator Mode Behavior Tests" -echo "========================================" -echo "" - # Setup SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "$SCRIPT_DIR/../test-framework.sh" + REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" cd "$REPO_ROOT/examples/android" @@ -29,13 +19,18 @@ fi . .devbox/virtenv/android/scripts/init/setup.sh . .devbox/virtenv/android/scripts/domain/emulator.sh +echo "========================================" +echo "Emulator Mode Behavior Tests" +echo "========================================" +echo "" + echo "This test demonstrates the difference between:" echo " 1. Normal mode: Reuses existing emulator if AVD matches" echo " 2. Pure mode: Always creates fresh emulator with clean state" echo "" # Check current emulator state -echo -e "${BLUE}Current State:${NC}" +echo "Current State:" running_count=$(adb devices 2>/dev/null | grep -c "emulator-" | tr -d '\n' || echo "0") echo " Running emulators: $running_count" @@ -48,9 +43,7 @@ fi echo "" # Test 1: Normal mode behavior -echo "========================================" -echo "TEST 1: Normal Mode (Reuse existing)" -echo "========================================" +log_test "Normal Mode (Reuse existing)" echo "" echo "Scenario: Running 'android.sh emulator start max' (no --pure flag)" @@ -63,7 +56,7 @@ echo "" # Check if max AVD exists if [ "$running_count" -gt 0 ]; then - echo -e "${GREEN}โœ“${NC} Emulator already running - normal mode would reuse it" + assert_success "true" "Emulator already running - normal mode would reuse it" echo "" echo "Test reuse detection:" @@ -73,22 +66,16 @@ if [ "$running_count" -gt 0 ]; then echo " Existing: $first_serial ($avd_name)" found=$(android_find_running_emulator "$avd_name" || echo "") - if [ "$found" = "$first_serial" ]; then - echo -e " ${GREEN}โœ“${NC} android_find_running_emulator correctly finds: $found" - else - echo -e " ${RED}โœ—${NC} Detection failed" - fi + assert_equal "$first_serial" "$found" "android_find_running_emulator correctly finds emulator" else - echo -e "${YELLOW}โš ${NC} No emulators running - normal mode would start new one" + echo "No emulators running - normal mode would start new one" echo " (Run 'devbox run start:emu' to test reuse behavior)" fi echo "" # Test 2: Pure mode behavior -echo "========================================" -echo "TEST 2: Pure Mode (Fresh instance)" -echo "========================================" +log_test "Pure Mode (Fresh instance)" echo "" echo "Scenario: Running 'android.sh emulator start --pure max'" @@ -100,22 +87,13 @@ echo " - Creates clean state for deterministic testing" echo " - Should be stopped after test completes (in e2e tests)" echo "" -echo -e "${BLUE}Pure mode flag:${NC}" +echo "Pure mode flag:" echo " export ANDROID_EMULATOR_PURE=1" echo " This triggers --wipe-data flag in emulator command" echo "" -echo -e "${BLUE}Pure mode detection logic:${NC}" -echo " if [ \"\${ANDROID_EMULATOR_PURE:-0}\" = \"1\" ]; then" -echo " # Skip reuse check, always start fresh" -echo " emulator -avd \$avd_name -wipe-data ..." -echo " fi" -echo "" - # Test 3: AVD matching logic -echo "========================================" -echo "TEST 3: AVD Matching Logic" -echo "========================================" +log_test "AVD Matching Logic" echo "" echo "How emulators are matched to AVDs:" @@ -145,9 +123,7 @@ fi echo "" # Test 4: Serial file tracking -echo "========================================" -echo "TEST 4: Serial File Tracking" -echo "========================================" +log_test "Serial File Tracking" echo "" echo "Serial (emulator-5554) is the standard adb identifier because:" @@ -160,23 +136,21 @@ echo "" echo "Serial file: /tmp/android-emulator-serial.txt" if [ -f /tmp/android-emulator-serial.txt ]; then serial=$(cat /tmp/android-emulator-serial.txt) - echo -e " ${GREEN}โœ“${NC} File exists: $serial" + echo " File exists: $serial" if android_is_emulator_running "$serial"; then - echo -e " ${GREEN}โœ“${NC} Emulator is running and responsive" + echo " Emulator is running and responsive" else - echo -e " ${YELLOW}โš ${NC} Emulator not running (stale serial file)" + echo " Emulator not running (stale serial file)" fi else - echo -e " ${YELLOW}โš ${NC} File does not exist (no emulator started this session)" + echo " File does not exist (no emulator started this session)" fi echo "" # Test 5: Cleanup behavior -echo "========================================" -echo "TEST 5: Cleanup Behavior" -echo "========================================" +log_test "Cleanup Behavior" echo "" echo "Normal mode cleanup:" @@ -191,12 +165,6 @@ echo " - Serial file cleaned up" echo " - Next run starts completely fresh" echo "" -echo "Cleanup logic in test-suite.yaml:" -echo " if [ \"\${TEST_PURE:-0}\" = \"1\" ]; then" -echo " android.sh emulator stop # Kill emulator" -echo " fi" -echo "" - # Summary echo "========================================" echo "SUMMARY" @@ -204,17 +172,17 @@ echo "========================================" echo "" echo "Key Differences:" echo "" -echo -e "${GREEN}Normal Mode:${NC}" -echo " โœ“ Fast (reuses existing emulator)" -echo " โœ“ Good for development/iteration" -echo " โœ“ Emulator persists between runs" -echo " โœ— May have state from previous runs" +echo "Normal Mode:" +echo " + Fast (reuses existing emulator)" +echo " + Good for development/iteration" +echo " + Emulator persists between runs" +echo " - May have state from previous runs" echo "" -echo -e "${BLUE}Pure Mode:${NC}" -echo " โœ“ Deterministic (clean state every time)" -echo " โœ“ Good for CI/CD pipelines" -echo " โœ“ Isolated test runs" -echo " โœ— Slower (boots fresh emulator)" +echo "Pure Mode:" +echo " + Deterministic (clean state every time)" +echo " + Good for CI/CD pipelines" +echo " + Isolated test runs" +echo " - Slower (boots fresh emulator)" echo "" echo "Usage:" @@ -222,4 +190,4 @@ echo " devbox run test:e2e # Normal mode" echo " TEST_PURE=1 devbox run test:e2e # Pure mode" echo "" -echo -e "${GREEN}All behavior tests passed!${NC}" +echo "All behavior tests passed!" diff --git a/plugins/tests/android/test-lib.sh b/plugins/tests/android/test-lib.sh index 1b80809..3443aa2 100644 --- a/plugins/tests/android/test-lib.sh +++ b/plugins/tests/android/test-lib.sh @@ -5,110 +5,14 @@ set -euo pipefail -# Setup logging - redirect all output to log file -SCRIPT_DIR_NAME="$(basename "$(dirname "$0")")" -SCRIPT_NAME="$(basename "$0" .sh)" -mkdir -p "${TEST_LOGS_DIR:-reports/logs}" -LOG_FILE="${TEST_LOGS_DIR:-reports/logs}/${SCRIPT_DIR_NAME}-${SCRIPT_NAME}.txt" -exec > >(tee "$LOG_FILE") -exec 2>&1 - -# ============================================================================ -# Test Framework -# ============================================================================ - -test_passed=0 -test_failed=0 -test_name="" - -start_test() { - test_name="$1" - echo "" - echo "TEST: $test_name" -} - -assert_equal() { - expected="$1" - actual="$2" - message="${3:-}" - - if [ "$expected" = "$actual" ]; then - echo " โœ“ PASS${message:+: $message}" - test_passed=$((test_passed + 1)) - else - echo " โœ— FAIL${message:+: $message}" - echo " Expected: '$expected'" - echo " Actual: '$actual'" - test_failed=$((test_failed + 1)) - fi -} - -assert_success() { - command_str="$1" - message="${2:-}" - - if eval "$command_str" >/dev/null 2>&1; then - echo " โœ“ PASS${message:+: $message}" - test_passed=$((test_passed + 1)) - else - echo " โœ— FAIL${message:+: $message}" - echo " Command failed: $command_str" - test_failed=$((test_failed + 1)) - fi -} - -assert_failure() { - command_str="$1" - message="${2:-}" - - # Run in subshell to prevent exit from killing test script - if ! (eval "$command_str") >/dev/null 2>&1; then - echo " โœ“ PASS${message:+: $message}" - test_passed=$((test_passed + 1)) - else - echo " โœ— FAIL${message:+: $message}" - echo " Command should have failed: $command_str" - test_failed=$((test_failed + 1)) - fi -} - -test_summary() { - total=$((test_passed + test_failed)) - echo "" - echo "========================================" - echo "Test Summary" - echo "========================================" - echo "Total: $total" - echo "Passed: $test_passed" - echo "Failed: $test_failed" - echo "" - - # Write results file for summary aggregation - results_dir="${TEST_RESULTS_DIR:-$(cd "$(dirname "$0")/../../../reports/results" 2>/dev/null && pwd || echo "/tmp")}" - mkdir -p "$results_dir" 2>/dev/null || true - cat > "$results_dir/android-lib.json" << EOF -{ - "suite": "android-lib", - "passed": $test_passed, - "failed": $test_failed, - "total": $total -} -EOF - - if [ "$test_failed" -gt 0 ]; then - echo "RESULT: โœ— FAILED" - exit 1 - else - echo "RESULT: โœ“ ALL PASSED" - exit 0 - fi -} +script_dir="$(cd "$(dirname "$0")" && pwd)" +. "$script_dir/../test-framework.sh" +setup_logging # ============================================================================ # Setup # ============================================================================ -script_dir="$(cd "$(dirname "$0")" && pwd)" lib_path="$script_dir/../../android/virtenv/scripts/lib/lib.sh" if [ ! -f "$lib_path" ]; then @@ -125,10 +29,6 @@ echo "Android lib.sh Unit Tests" echo "========================================" echo "Testing: $lib_path" -# Prepare results directory -results_base="${TEST_RESULTS_DIR:-$(cd "$script_dir/../../.." 2>/dev/null && pwd)/reports/results}" -mkdir -p "$results_base" - # ============================================================================ # Tests: String Normalization # ============================================================================ @@ -164,28 +64,26 @@ assert_failure "android_sanitize_avd_name ''" "Should fail on empty string" # Tests: Checksum Functions # ============================================================================ -# Create temporary test directory with device files -test_dir="/tmp/android-plugin-test-$$" -mkdir -p "$test_dir" -echo '{"name":"test1","api":28}' > "$test_dir/test1.json" -echo '{"name":"test2","api":34}' > "$test_dir/test2.json" +# Read-only checksum tests use example project fixtures +example_devices="$(fixture_android_devices_dir)" start_test "android_compute_devices_checksum - generates checksum" -result="$(android_compute_devices_checksum "$test_dir")" +result="$(android_compute_devices_checksum "$example_devices")" assert_success "[ -n '$result' ]" "Should return non-empty checksum" start_test "android_compute_devices_checksum - stable checksum" -checksum1="$(android_compute_devices_checksum "$test_dir")" -checksum2="$(android_compute_devices_checksum "$test_dir")" +checksum1="$(android_compute_devices_checksum "$example_devices")" +checksum2="$(android_compute_devices_checksum "$example_devices")" assert_equal "$checksum1" "$checksum2" "Should return same checksum for same files" +# Write test needs a temp dir start_test "android_compute_devices_checksum - different content = different checksum" +test_dir="$(make_temp_dir "android-checksum")" +echo '{"name":"test1","api":28}' > "$test_dir/test1.json" checksum_before="$(android_compute_devices_checksum "$test_dir")" -echo '{"name":"test3","api":36}' > "$test_dir/test3.json" +echo '{"name":"test2","api":36}' > "$test_dir/test2.json" checksum_after="$(android_compute_devices_checksum "$test_dir")" assert_success "[ '$checksum_before' != '$checksum_after' ]" "Should change when files change" - -# Cleanup rm -rf "$test_dir" start_test "android_compute_devices_checksum - fails on non-existent dir" @@ -195,14 +93,13 @@ assert_failure "android_compute_devices_checksum '/nonexistent/path'" "Should fa # Tests: Path Resolution # ============================================================================ -# Create test environment for path resolution -path_test_root="/tmp/android-path-test-$$" -mkdir -p "$path_test_root/devbox.d/android/devices" -echo '{"name":"test","api":30}' > "$path_test_root/devbox.d/android/devices/test.json" +# Use example android project for path resolution +example_android_dir="$REPO_ROOT/examples/android" # Save original and set test root SAVED_PROJECT_ROOT="${DEVBOX_PROJECT_ROOT:-}" -export DEVBOX_PROJECT_ROOT="$path_test_root" +unset ANDROID_CONFIG_DIR +export DEVBOX_PROJECT_ROOT="$example_android_dir" start_test "android_resolve_project_path - finds existing file" result="$(android_resolve_project_path "devices" 2>/dev/null || true)" @@ -214,7 +111,7 @@ fi start_test "android_resolve_project_path - finds directory" result="$(android_resolve_project_path "devices" 2>/dev/null || true)" -expected="${path_test_root}/devbox.d/android/devices" +expected="${example_android_dir}/devbox.d/android/devices" assert_equal "$expected" "$result" "Should resolve devices directory" start_test "android_resolve_project_path - fails on missing path" @@ -222,11 +119,10 @@ assert_failure "android_resolve_project_path 'nonexistent.json'" "Should fail wh start_test "android_resolve_config_dir - finds config directory" result="$(android_resolve_config_dir 2>/dev/null || true)" -expected="${path_test_root}/devbox.d/android" +expected="${example_android_dir}/devbox.d/android" assert_equal "$expected" "$result" "Should find android config directory" -# Cleanup path test directory and restore -rm -rf "$path_test_root" +# Restore if [ -n "$SAVED_PROJECT_ROOT" ]; then export DEVBOX_PROJECT_ROOT="$SAVED_PROJECT_ROOT" else @@ -247,7 +143,7 @@ start_test "android_require_tool - fails for missing tool" assert_failure "android_require_tool 'nonexistent_tool_xyz'" "Should fail for missing tool" # Create test directory for dir_contains test -test_sdk="/tmp/android-sdk-test-$$" +test_sdk="$(make_temp_dir "android-sdk")" mkdir -p "$test_sdk/platform-tools" start_test "android_require_dir_contains - succeeds when path exists" @@ -263,4 +159,4 @@ rm -rf "$test_sdk" # Test Summary # ============================================================================ -test_summary +test_summary "android-lib" diff --git a/plugins/tests/android/test-posix-compat.sh b/plugins/tests/android/test-posix-compat.sh new file mode 100755 index 0000000..7cf258d --- /dev/null +++ b/plugins/tests/android/test-posix-compat.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +# Android Plugin - POSIX Compatibility Tests +# +# Verifies that init scripts (sourced during devbox shell startup) work +# correctly under dash, which is the default /bin/sh on Linux. +# These scripts use #!/usr/bin/env sh and must avoid bash-isms. + +set -euo pipefail + +script_dir="$(cd "$(dirname "$0")" && pwd)" +. "$script_dir/../test-framework.sh" +setup_logging + +echo "========================================" +echo "Android POSIX Compatibility Tests" +echo "========================================" + +# ============================================================================ +# Setup +# ============================================================================ + +scripts_dir="$script_dir/../../android/virtenv/scripts" + +# Scripts that are sourced (must be POSIX-compatible) +sourced_scripts=( + "$scripts_dir/lib/lib.sh" + "$scripts_dir/platform/core.sh" + "$scripts_dir/platform/device_config.sh" + "$scripts_dir/init/setup.sh" +) + +# ============================================================================ +# Tests: ShellCheck POSIX Validation +# ============================================================================ + +if command -v shellcheck >/dev/null 2>&1; then + for script in "${sourced_scripts[@]}"; do + script_name="$(basename "$script")" + start_test "shellcheck --shell=sh $script_name" + if [ ! -f "$script" ]; then + echo " โœ— FAIL: Script not found: $script" + test_failed=$((test_failed + 1)) + continue + fi + output="$(shellcheck --shell=sh --severity=error "$script" 2>&1 || true)" + if [ -z "$output" ]; then + echo " โœ“ PASS: No POSIX errors" + test_passed=$((test_passed + 1)) + else + echo " โœ— FAIL: ShellCheck found POSIX issues" + echo "$output" | head -20 + test_failed=$((test_failed + 1)) + fi + done +else + echo "SKIP: shellcheck not available (install for POSIX validation)" +fi + +# ============================================================================ +# Tests: Dash Compatibility (if dash is available) +# ============================================================================ + +if command -v dash >/dev/null 2>&1; then + start_test "lib.sh sources under dash without error" + output="$(dash -c ". '$scripts_dir/lib/lib.sh'" 2>&1 || true)" + if dash -c ". '$scripts_dir/lib/lib.sh'" >/dev/null 2>&1; then + echo " โœ“ PASS: lib.sh sources cleanly under dash" + test_passed=$((test_passed + 1)) + else + echo " โœ— FAIL: lib.sh failed under dash" + echo " Output: $output" + test_failed=$((test_failed + 1)) + fi + + start_test "platform/core.sh sources under dash without error" + # core.sh sources lib.sh, so set ANDROID_SCRIPTS_DIR + if dash -c "ANDROID_SCRIPTS_DIR='$scripts_dir'; export ANDROID_SCRIPTS_DIR; . '$scripts_dir/platform/core.sh'" >/dev/null 2>&1; then + echo " โœ“ PASS: core.sh sources cleanly under dash" + test_passed=$((test_passed + 1)) + else + echo " โœ— FAIL: core.sh failed under dash" + test_failed=$((test_failed + 1)) + fi + + start_test "platform/device_config.sh sources under dash without error" + if dash -c "ANDROID_SCRIPTS_DIR='$scripts_dir'; export ANDROID_SCRIPTS_DIR; . '$scripts_dir/platform/device_config.sh'" >/dev/null 2>&1; then + echo " โœ“ PASS: device_config.sh sources cleanly under dash" + test_passed=$((test_passed + 1)) + else + echo " โœ— FAIL: device_config.sh failed under dash" + test_failed=$((test_failed + 1)) + fi +else + echo "SKIP: dash not available (install for runtime POSIX testing)" +fi + +# ============================================================================ +# Summary +# ============================================================================ + +test_summary "android-posix-compat" diff --git a/plugins/tests/ios/test-app-resolution.sh b/plugins/tests/ios/test-app-resolution.sh new file mode 100755 index 0000000..b1275e7 --- /dev/null +++ b/plugins/tests/ios/test-app-resolution.sh @@ -0,0 +1,219 @@ +#!/usr/bin/env bash +# iOS Plugin - App Bundle Auto-Detection Resolution Tests +# +# Tests for the ios_find_app() precedence chain in deploy.sh. +# These tests use temporary directories with fixture .app bundles. +# Requires macOS (uses PlistBuddy for plist creation/reading). + +set -euo pipefail + +script_dir="$(cd "$(dirname "$0")" && pwd)" +. "$script_dir/../test-framework.sh" +setup_logging + +# Only run on macOS +if [ "$(uname -s)" != "Darwin" ]; then + echo "Skipping iOS app resolution tests (not on macOS)" + exit 0 +fi + +# ============================================================================ +# Setup +# ============================================================================ + +deploy_path="$script_dir/../../ios/virtenv/scripts/domain/deploy.sh" +lib_path="$script_dir/../../ios/virtenv/scripts/lib/lib.sh" +core_path="$script_dir/../../ios/virtenv/scripts/platform/core.sh" + +if [ ! -f "$deploy_path" ]; then + echo "ERROR: deploy.sh not found at: $deploy_path" + exit 1 +fi + +# Source dependencies (deploy.sh must be sourced) +IOS_SCRIPTS_DIR="$script_dir/../../ios/virtenv/scripts" +export IOS_SCRIPTS_DIR +. "$deploy_path" + +# Create temp project structures +TMPDIR_BASE="$(make_temp_dir "ios-app-resolution")" +trap 'rm -rf "$TMPDIR_BASE"' EXIT + +# Helper to create a fake .app bundle with Info.plist +create_fake_app() { + app_dir="$1" + bundle_id="${2:-com.test.app}" + mkdir -p "$app_dir" + /usr/libexec/PlistBuddy -c "Add :CFBundleIdentifier string $bundle_id" "$app_dir/Info.plist" 2>/dev/null || true +} + +echo "========================================" +echo "iOS App Resolution Tests" +echo "========================================" +echo "Testing: $deploy_path" + +# ============================================================================ +# Tests: ios_find_app with IOS_APP_ARTIFACT env var (precedence 1) +# ============================================================================ + +start_test "ios_find_app - resolves via IOS_APP_ARTIFACT env var" +test_root="$TMPDIR_BASE/test1" +mkdir -p "$test_root/DerivedData/Build/Products/Debug-iphonesimulator" +create_fake_app "$test_root/DerivedData/Build/Products/Debug-iphonesimulator/MyApp.app" +IOS_APP_ARTIFACT="DerivedData/Build/Products/Debug-iphonesimulator/MyApp.app" \ + result=$(ios_find_app "$test_root" 2>/dev/null) +assert_equal "$test_root/DerivedData/Build/Products/Debug-iphonesimulator/MyApp.app" "$result" \ + "Should find app via IOS_APP_ARTIFACT relative path" + +start_test "ios_find_app - resolves via IOS_APP_ARTIFACT glob" +test_root="$TMPDIR_BASE/test1b" +mkdir -p "$test_root/DerivedData/Build/Products/Debug-iphonesimulator" +create_fake_app "$test_root/DerivedData/Build/Products/Debug-iphonesimulator/MyApp.app" +IOS_APP_ARTIFACT="DerivedData/Build/Products/Debug-iphonesimulator/*.app" \ + result=$(ios_find_app "$test_root" 2>/dev/null) +assert_equal "$test_root/DerivedData/Build/Products/Debug-iphonesimulator/MyApp.app" "$result" \ + "Should find app via IOS_APP_ARTIFACT glob pattern" + +start_test "ios_find_app - IOS_APP_ARTIFACT takes priority over recursive search" +test_root="$TMPDIR_BASE/test1c" +mkdir -p "$test_root/specific" "$test_root/other" +create_fake_app "$test_root/specific/Target.app" "com.test.target" +create_fake_app "$test_root/other/Decoy.app" "com.test.decoy" +IOS_APP_ARTIFACT="specific/Target.app" \ + result=$(ios_find_app "$test_root" 2>/dev/null) +assert_equal "$test_root/specific/Target.app" "$result" \ + "IOS_APP_ARTIFACT should take priority" + +# ============================================================================ +# Tests: ios_find_app with recursive search (precedence 3 - skipping xcodebuild) +# ============================================================================ + +start_test "ios_find_app - finds .app via recursive search" +test_root="$TMPDIR_BASE/test3" +mkdir -p "$test_root/Build/Products/Debug-iphonesimulator" +create_fake_app "$test_root/Build/Products/Debug-iphonesimulator/MyApp.app" +IOS_APP_ARTIFACT="" \ + result=$(ios_find_app "$test_root" 2>/dev/null) +assert_equal "$test_root/Build/Products/Debug-iphonesimulator/MyApp.app" "$result" \ + "Should find .app via recursive search" + +start_test "ios_find_app - excludes Pods directory" +test_root="$TMPDIR_BASE/test3b" +mkdir -p "$test_root/Pods/SomePod" "$test_root/build" +create_fake_app "$test_root/Pods/SomePod/SomePod.app" +create_fake_app "$test_root/build/Real.app" "com.test.real" +IOS_APP_ARTIFACT="" \ + result=$(ios_find_app "$test_root" 2>/dev/null) +assert_equal "$test_root/build/Real.app" "$result" \ + "Should skip Pods directory" + +start_test "ios_find_app - excludes .build directory" +test_root="$TMPDIR_BASE/test3c" +mkdir -p "$test_root/.build/artifacts" "$test_root/output" +create_fake_app "$test_root/.build/artifacts/Cached.app" +create_fake_app "$test_root/output/Real.app" "com.test.real" +IOS_APP_ARTIFACT="" \ + result=$(ios_find_app "$test_root" 2>/dev/null) +assert_equal "$test_root/output/Real.app" "$result" \ + "Should skip .build directory" + +start_test "ios_find_app - excludes SourcePackages directory" +test_root="$TMPDIR_BASE/test3d" +mkdir -p "$test_root/SourcePackages/checkouts" "$test_root/build" +create_fake_app "$test_root/SourcePackages/checkouts/Dep.app" +create_fake_app "$test_root/build/Real.app" "com.test.real" +IOS_APP_ARTIFACT="" \ + result=$(ios_find_app "$test_root" 2>/dev/null) +assert_equal "$test_root/build/Real.app" "$result" \ + "Should skip SourcePackages" + +start_test "ios_find_app - excludes node_modules directory" +test_root="$TMPDIR_BASE/test3e" +mkdir -p "$test_root/node_modules/some-pkg" "$test_root/build" +create_fake_app "$test_root/node_modules/some-pkg/Cached.app" +create_fake_app "$test_root/build/Real.app" "com.test.real" +IOS_APP_ARTIFACT="" \ + result=$(ios_find_app "$test_root" 2>/dev/null) +assert_equal "$test_root/build/Real.app" "$result" \ + "Should skip node_modules" + +start_test "ios_find_app - excludes .devbox directory" +test_root="$TMPDIR_BASE/test3f" +mkdir -p "$test_root/.devbox/virtenv" "$test_root/build" +create_fake_app "$test_root/.devbox/virtenv/Cached.app" +create_fake_app "$test_root/build/Real.app" "com.test.real" +IOS_APP_ARTIFACT="" \ + result=$(ios_find_app "$test_root" 2>/dev/null) +assert_equal "$test_root/build/Real.app" "$result" \ + "Should skip .devbox" + +start_test "ios_find_app - excludes DerivedData/ModuleCache directory" +test_root="$TMPDIR_BASE/test3g" +mkdir -p "$test_root/DerivedData/ModuleCache" "$test_root/DerivedData/Build/Products" +create_fake_app "$test_root/DerivedData/ModuleCache/Module.app" +create_fake_app "$test_root/DerivedData/Build/Products/Real.app" "com.test.real" +IOS_APP_ARTIFACT="" \ + result=$(ios_find_app "$test_root" 2>/dev/null) +assert_equal "$test_root/DerivedData/Build/Products/Real.app" "$result" \ + "Should skip DerivedData/ModuleCache" + +# ============================================================================ +# Tests: ios_find_app fails with clear error (precedence 5) +# ============================================================================ + +start_test "ios_find_app - fails when no .app found" +test_root="$TMPDIR_BASE/test5" +mkdir -p "$test_root" +# Run from a clean dir so $PWD fallback search doesn't find fixture .app bundles +(cd "$test_root" && IOS_APP_ARTIFACT="" \ + assert_failure "ios_find_app '$test_root'" "Should fail when no .app exists") + +start_test "ios_find_app - error message includes guidance" +test_root="$TMPDIR_BASE/test5b" +mkdir -p "$test_root" +# Run from a clean dir so $PWD fallback search doesn't find fixture .app bundles +error_output=$(cd "$test_root" && IOS_APP_ARTIFACT="" ios_find_app "$test_root" 2>&1 || true) +assert_contains "$error_output" "No .app bundle found" "Error should mention no .app found" +assert_contains "$error_output" "IOS_APP_ARTIFACT" "Error should mention env var" + +# ============================================================================ +# Tests: ios_extract_bundle_id +# ============================================================================ + +start_test "ios_extract_bundle_id - extracts from Info.plist" +test_root="$TMPDIR_BASE/test_bundle" +create_fake_app "$test_root/MyApp.app" "com.example.myapp" +result=$(ios_extract_bundle_id "$test_root/MyApp.app" 2>/dev/null) +assert_equal "com.example.myapp" "$result" "Should extract correct bundle ID" + +start_test "ios_extract_bundle_id - fails on missing Info.plist" +test_root="$TMPDIR_BASE/test_bundle_missing" +mkdir -p "$test_root/NoInfo.app" +assert_failure "ios_extract_bundle_id '$test_root/NoInfo.app'" "Should fail without Info.plist" + +# ============================================================================ +# Tests: ios_resolve_app_glob +# ============================================================================ + +start_test "ios_resolve_app_glob - finds app by exact path" +test_root="$TMPDIR_BASE/test_glob1" +create_fake_app "$test_root/build/MyApp.app" +result=$(ios_resolve_app_glob "$test_root" "build/MyApp.app") +assert_equal "$test_root/build/MyApp.app" "$result" "Should find app by exact path" + +start_test "ios_resolve_app_glob - finds app by glob" +test_root="$TMPDIR_BASE/test_glob2" +create_fake_app "$test_root/build/MyApp.app" +result=$(ios_resolve_app_glob "$test_root" "build/*.app") +assert_equal "$test_root/build/MyApp.app" "$result" "Should find app by glob" + +start_test "ios_resolve_app_glob - fails on no match" +test_root="$TMPDIR_BASE/test_glob3" +mkdir -p "$test_root" +assert_failure "ios_resolve_app_glob '$test_root' 'nonexistent/*.app'" "Should fail when no app matches" + +# ============================================================================ +# Summary +# ============================================================================ + +test_summary "ios-app-resolution" diff --git a/plugins/tests/ios/test-devices.sh b/plugins/tests/ios/test-devices.sh index 16dffdd..9b57ce8 100755 --- a/plugins/tests/ios/test-devices.sh +++ b/plugins/tests/ios/test-devices.sh @@ -1,28 +1,11 @@ #!/usr/bin/env bash # iOS Plugin - devices.sh Integration Tests -set -eu - -# Setup logging - redirect all output to log file -SCRIPT_DIR_NAME="$(basename "$(dirname "$0")")" -SCRIPT_NAME="$(basename "$0" .sh)" -mkdir -p "${TEST_LOGS_DIR:-reports/logs}" -LOG_FILE="${TEST_LOGS_DIR:-reports/logs}/${SCRIPT_DIR_NAME}-${SCRIPT_NAME}.txt" -exec > >(tee "$LOG_FILE") -exec 2>&1 - -test_passed=0 -test_failed=0 - -assert_success() { - if eval "$1" >/dev/null 2>&1; then - echo " โœ“ PASS: $2" - test_passed=$((test_passed + 1)) - else - echo " โœ— FAIL: $2" - test_failed=$((test_failed + 1)) - fi -} +set -euo pipefail + +script_dir="$(cd "$(dirname "$0")" && pwd)" +. "$script_dir/../test-framework.sh" +setup_logging echo "========================================" echo "iOS devices.sh Integration Tests" @@ -30,7 +13,7 @@ echo "========================================" echo "" # Setup test environment -test_root="/tmp/ios-plugin-device-test-$$" +test_root="$(make_temp_dir "ios-devices")" mkdir -p "$test_root/devices" mkdir -p "$test_root/scripts/lib" mkdir -p "$test_root/scripts/platform" @@ -38,7 +21,6 @@ mkdir -p "$test_root/scripts/domain" mkdir -p "$test_root/scripts/user" # Copy required scripts (layered structure) -script_dir="$(cd "$(dirname "$0")" && pwd)" cp "$script_dir/../../ios/virtenv/scripts/lib/lib.sh" "$test_root/scripts/lib/" cp "$script_dir/../../ios/virtenv/scripts/platform/core.sh" "$test_root/scripts/platform/" cp "$script_dir/../../ios/virtenv/scripts/platform/device_config.sh" "$test_root/scripts/platform/" @@ -129,31 +111,8 @@ assert_success "[ ! -f '$test_root/devices/test_iphone.json' ]" "Device file rem # Cleanup rm -rf "$test_root" -# Summary -echo "" -echo "========================================" -total=$((test_passed + test_failed)) -echo "Total: $total" -echo "Passed: $test_passed" -echo "Failed: $test_failed" -echo "" +# ============================================================================ +# Test Summary +# ============================================================================ -# Write results file for summary aggregation -results_dir="${TEST_RESULTS_DIR:-$(cd "$(dirname "$0")/../../../reports/results" 2>/dev/null && pwd || echo "/tmp")}" -mkdir -p "$results_dir" 2>/dev/null || true -cat > "$results_dir/ios-devices.json" << EOF -{ - "suite": "ios-devices", - "passed": $test_passed, - "failed": $test_failed, - "total": $total -} -EOF - -if [ "$test_failed" -gt 0 ]; then - echo "RESULT: โœ— FAILED" - exit 1 -else - echo "RESULT: โœ“ ALL PASSED" - exit 0 -fi +test_summary "ios-devices" diff --git a/plugins/tests/ios/test-lib.sh b/plugins/tests/ios/test-lib.sh index f04391c..4487e74 100755 --- a/plugins/tests/ios/test-lib.sh +++ b/plugins/tests/ios/test-lib.sh @@ -1,167 +1,97 @@ #!/usr/bin/env bash -set -eu +# iOS Plugin - lib.sh Unit Tests -# Setup logging - redirect all output to log file -SCRIPT_DIR_NAME="$(basename "$(dirname "$0")")" -SCRIPT_NAME="$(basename "$0" .sh)" -mkdir -p "${TEST_LOGS_DIR:-reports/logs}" -LOG_FILE="${TEST_LOGS_DIR:-reports/logs}/${SCRIPT_DIR_NAME}-${SCRIPT_NAME}.txt" -exec > >(tee "$LOG_FILE") -exec 2>&1 +set -euo pipefail -echo "Testing iOS lib.sh..." +script_dir="$(cd "$(dirname "$0")" && pwd)" +. "$script_dir/../test-framework.sh" +setup_logging -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -IOS_SCRIPTS_DIR="${SCRIPT_DIR}/../../ios/virtenv/scripts" +IOS_SCRIPTS_DIR="${script_dir}/../../ios/virtenv/scripts" export IOS_SCRIPTS_DIR # Source lib.sh (new location in lib/) . "${IOS_SCRIPTS_DIR}/lib/lib.sh" -# Test 1: Load-once guard -echo " Test 1: Load-once guard" +echo "========================================" +echo "iOS lib.sh Unit Tests" +echo "========================================" +echo "Testing: ${IOS_SCRIPTS_DIR}/lib/lib.sh" + +# ============================================================================ +# Tests: Load Guards +# ============================================================================ + +start_test "Load-once guard" . "${IOS_SCRIPTS_DIR}/lib/lib.sh" -if [ "${IOS_LIB_LOADED}" != "1" ]; then - echo " FAIL: Load-once guard failed" - exit 1 -fi -echo " PASS" +assert_equal "1" "${IOS_LIB_LOADED}" "Load-once guard should keep IOS_LIB_LOADED=1" -# Test 2: Execution protection -echo " Test 2: Execution protection" -if sh "${IOS_SCRIPTS_DIR}/lib/lib.sh" 2>/dev/null; then - echo " FAIL: Execution protection failed" - exit 1 -fi -echo " PASS" +start_test "Execution protection" +assert_failure "sh '${IOS_SCRIPTS_DIR}/lib/lib.sh'" "Should fail when executed directly" + +# ============================================================================ +# Tests: String Normalization +# ============================================================================ -# Test 3: ios_sanitize_device_name -echo " Test 3: ios_sanitize_device_name" +start_test "ios_sanitize_device_name - preserves valid name" result="$(ios_sanitize_device_name "iPhone 15 Pro" || true)" -if [ "$result" != "iPhone 15 Pro" ]; then - echo " FAIL: Expected 'iPhone 15 Pro', got '$result'" - exit 1 -fi +assert_equal "iPhone 15 Pro" "$result" "Should preserve valid device name" + +start_test "ios_sanitize_device_name - removes invalid chars" result="$(ios_sanitize_device_name "Test Device!@#" || true)" -if [ "$result" != "Test Device" ]; then - echo " FAIL: Expected 'Test Device', got '$result'" - exit 1 -fi -echo " PASS" - -# Create temporary test directory structure -test_root="/tmp/ios-plugin-test-$$" -mkdir -p "$test_root/devbox.d/ios/devices" - -# Create test device files -cat > "$test_root/devbox.d/ios/devices/test1.json" <<'EOF' -{ - "name": "iPhone 15 Pro", - "runtime": "17.5" -} -EOF - -cat > "$test_root/devbox.d/ios/devices/test2.json" <<'EOF' -{ - "name": "iPhone 16", - "runtime": "18.0" -} -EOF - -# Test 4: ios_config_path -echo " Test 4: ios_config_path" +assert_equal "Test Device" "$result" "Should remove invalid characters" + +# ============================================================================ +# Tests: Config Path Resolution (using example project fixtures) +# ============================================================================ + +example_ios_dir="$REPO_ROOT/examples/ios" + +start_test "ios_config_path" unset IOS_CONFIG_DIR -DEVBOX_PROJECT_ROOT="$test_root" +DEVBOX_PROJECT_ROOT="$example_ios_dir" export DEVBOX_PROJECT_ROOT config_path="$(ios_config_path 2>/dev/null || true)" -if [ -z "$config_path" ]; then - echo " FAIL: ios_config_path returned empty" - rm -rf "$test_root" - exit 1 -fi -expected="$test_root/devbox.d/ios" -if [ "$config_path" != "$expected" ]; then - echo " FAIL: Expected '$expected', got '$config_path'" - rm -rf "$test_root" - exit 1 -fi -echo " PASS" +expected="$example_ios_dir/devbox.d/ios" +assert_equal "$expected" "$config_path" "Should resolve to example ios config dir" -# Test 5: ios_devices_dir -echo " Test 5: ios_devices_dir" +start_test "ios_devices_dir" unset IOS_DEVICES_DIR devices_dir="$(ios_devices_dir 2>/dev/null || true)" -if [ -z "$devices_dir" ]; then - echo " FAIL: ios_devices_dir returned empty" - rm -rf "$test_root" - exit 1 -fi -expected="$test_root/devbox.d/ios/devices" -if [ "$devices_dir" != "$expected" ]; then - echo " FAIL: Expected '$expected', got '$devices_dir'" - rm -rf "$test_root" - exit 1 -fi -if [ ! -d "$devices_dir" ]; then - echo " FAIL: devices_dir doesn't exist: $devices_dir" - rm -rf "$test_root" - exit 1 -fi -echo " PASS" +expected="$example_ios_dir/devbox.d/ios/devices" +assert_equal "$expected" "$devices_dir" "Should resolve to example ios devices dir" +assert_success "[ -d '$devices_dir' ]" "Devices dir should exist" + +# ============================================================================ +# Tests: Checksum (using example project fixtures) +# ============================================================================ -# Test 6: ios_compute_devices_checksum -echo " Test 6: ios_compute_devices_checksum" +start_test "ios_compute_devices_checksum - computes checksum" checksum1="$(ios_compute_devices_checksum "$devices_dir" || true)" -if [ -z "$checksum1" ]; then - echo " FAIL: Checksum computation failed" - rm -rf "$test_root" - exit 1 -fi -if [ "${#checksum1}" -ne 64 ]; then - echo " FAIL: Checksum length is not 64 characters: ${#checksum1}" - rm -rf "$test_root" - exit 1 -fi -# Test checksum stability +assert_not_empty "$checksum1" "Should compute checksum" +assert_equal "64" "${#checksum1}" "Checksum should be 64 characters" + +start_test "ios_compute_devices_checksum - stable checksum" checksum2="$(ios_compute_devices_checksum "$devices_dir" || true)" -if [ "$checksum1" != "$checksum2" ]; then - echo " FAIL: Checksum not stable - got different results" - rm -rf "$test_root" - exit 1 -fi -echo " PASS" +assert_equal "$checksum1" "$checksum2" "Checksum should be stable across calls" -# Cleanup test directory -rm -rf "$test_root" +# ============================================================================ +# Tests: Requirement Functions +# ============================================================================ -# Test 7: ios_require_jq -echo " Test 7: ios_require_jq" +start_test "ios_require_jq" if command -v jq >/dev/null 2>&1; then - ios_require_jq - echo " PASS" + assert_success "ios_require_jq" "Should succeed when jq is available" else - echo " SKIP: jq not available" + echo " SKIP: jq not available" fi -# Test 8: ios_require_tool -echo " Test 8: ios_require_tool" -ios_require_tool "sh" "sh is required" || { echo " FAIL"; exit 1; } -if (ios_require_tool "nonexistent_tool_xyz" 2>/dev/null); then - echo " FAIL: Should have failed for nonexistent tool" - exit 1 -fi -echo " PASS" - -# Write results file for summary aggregation -results_dir="${TEST_RESULTS_DIR:-$(cd "$(dirname "$0")/../../../reports/results" 2>/dev/null && pwd || echo "/tmp")}" -mkdir -p "$results_dir" 2>/dev/null || true -cat > "$results_dir/ios-lib.json" << EOF -{ - "suite": "ios-lib", - "passed": 8, - "failed": 0, - "total": 8 -} -EOF - -echo "All lib.sh tests passed!" +start_test "ios_require_tool" +assert_success "ios_require_tool 'sh' 'sh is required'" "Should succeed for sh" +assert_failure "ios_require_tool 'nonexistent_tool_xyz'" "Should fail for missing tool" + +# ============================================================================ +# Test Summary +# ============================================================================ + +test_summary "ios-lib" diff --git a/plugins/tests/ios/test-posix-compat.sh b/plugins/tests/ios/test-posix-compat.sh new file mode 100755 index 0000000..b08468a --- /dev/null +++ b/plugins/tests/ios/test-posix-compat.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# iOS Plugin - POSIX Compatibility Tests +# +# Verifies that init scripts (sourced during devbox shell startup) work +# correctly under dash, which is the default /bin/sh on Linux. +# These scripts use #!/usr/bin/env sh and must avoid bash-isms. + +set -euo pipefail + +script_dir="$(cd "$(dirname "$0")" && pwd)" +. "$script_dir/../test-framework.sh" +setup_logging + +echo "========================================" +echo "iOS POSIX Compatibility Tests" +echo "========================================" + +# ============================================================================ +# Setup +# ============================================================================ + +scripts_dir="$script_dir/../../ios/virtenv/scripts" + +# Scripts that are sourced (must be POSIX-compatible) +sourced_scripts=( + "$scripts_dir/lib/lib.sh" + "$scripts_dir/platform/core.sh" + "$scripts_dir/platform/device_config.sh" + "$scripts_dir/init/setup.sh" +) + +# ============================================================================ +# Tests: ShellCheck POSIX Validation +# ============================================================================ + +if command -v shellcheck >/dev/null 2>&1; then + for script in "${sourced_scripts[@]}"; do + script_name="$(basename "$script")" + start_test "shellcheck --shell=sh $script_name" + if [ ! -f "$script" ]; then + echo " โœ— FAIL: Script not found: $script" + test_failed=$((test_failed + 1)) + continue + fi + output="$(shellcheck --shell=sh --severity=error "$script" 2>&1 || true)" + if [ -z "$output" ]; then + echo " โœ“ PASS: No POSIX errors" + test_passed=$((test_passed + 1)) + else + echo " โœ— FAIL: ShellCheck found POSIX issues" + echo "$output" | head -20 + test_failed=$((test_failed + 1)) + fi + done +else + echo "SKIP: shellcheck not available (install for POSIX validation)" +fi + +# ============================================================================ +# Tests: Dash Compatibility (if dash is available) +# ============================================================================ + +if command -v dash >/dev/null 2>&1; then + start_test "lib.sh sources under dash without error" + if dash -c ". '$scripts_dir/lib/lib.sh'" >/dev/null 2>&1; then + echo " โœ“ PASS: lib.sh sources cleanly under dash" + test_passed=$((test_passed + 1)) + else + echo " โœ— FAIL: lib.sh failed under dash" + test_failed=$((test_failed + 1)) + fi + + start_test "platform/core.sh sources under dash without error" + # core.sh sources lib.sh, so set IOS_SCRIPTS_DIR + if dash -c "IOS_SCRIPTS_DIR='$scripts_dir'; export IOS_SCRIPTS_DIR; . '$scripts_dir/platform/core.sh'" >/dev/null 2>&1; then + echo " โœ“ PASS: core.sh sources cleanly under dash" + test_passed=$((test_passed + 1)) + else + echo " โœ— FAIL: core.sh failed under dash" + test_failed=$((test_failed + 1)) + fi + + start_test "platform/device_config.sh sources under dash without error" + if dash -c "IOS_SCRIPTS_DIR='$scripts_dir'; export IOS_SCRIPTS_DIR; . '$scripts_dir/platform/device_config.sh'" >/dev/null 2>&1; then + echo " โœ“ PASS: device_config.sh sources cleanly under dash" + test_passed=$((test_passed + 1)) + else + echo " โœ— FAIL: device_config.sh failed under dash" + test_failed=$((test_failed + 1)) + fi +else + echo "SKIP: dash not available (install for runtime POSIX testing)" +fi + +# ============================================================================ +# Summary +# ============================================================================ + +test_summary "ios-posix-compat" diff --git a/plugins/tests/ios/test-simulator-detection.sh b/plugins/tests/ios/test-simulator-detection.sh index 8e1904f..52f99a1 100755 --- a/plugins/tests/ios/test-simulator-detection.sh +++ b/plugins/tests/ios/test-simulator-detection.sh @@ -4,138 +4,10 @@ set -euo pipefail -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[0;33m' -NC='\033[0m' # No Color - -# Counters -tests_run=0 -tests_passed=0 -tests_failed=0 - -# Test result tracking -test_results=() - -# Helper functions -log_test() { - echo "" - echo "========================================" - echo "TEST: $1" - echo "========================================" - tests_run=$((tests_run + 1)) -} - -assert_success() { - local cmd="$1" - local description="$2" - - if eval "$cmd" >/dev/null 2>&1; then - echo -e "${GREEN}โœ“${NC} $description" - tests_passed=$((tests_passed + 1)) - test_results+=("PASS: $description") - return 0 - else - echo -e "${RED}โœ—${NC} $description" - echo " Command failed: $cmd" - tests_failed=$((tests_failed + 1)) - test_results+=("FAIL: $description") - return 1 - fi -} - -assert_failure() { - local cmd="$1" - local description="$2" - - if ! eval "$cmd" >/dev/null 2>&1; then - echo -e "${GREEN}โœ“${NC} $description" - tests_passed=$((tests_passed + 1)) - test_results+=("PASS: $description") - return 0 - else - echo -e "${RED}โœ—${NC} $description" - echo " Command should have failed: $cmd" - tests_failed=$((tests_failed + 1)) - test_results+=("FAIL: $description") - return 1 - fi -} - -assert_output() { - local cmd="$1" - local expected="$2" - local description="$3" - - local output - output=$(eval "$cmd" 2>&1 || true) - - if echo "$output" | grep -q "$expected"; then - echo -e "${GREEN}โœ“${NC} $description" - tests_passed=$((tests_passed + 1)) - test_results+=("PASS: $description") - return 0 - else - echo -e "${RED}โœ—${NC} $description" - echo " Expected to contain: $expected" - echo " Got: $output" - tests_failed=$((tests_failed + 1)) - test_results+=("FAIL: $description") - return 1 - fi -} - -assert_equals() { - local actual="$1" - local expected="$2" - local description="$3" - - if [ "$actual" = "$expected" ]; then - echo -e "${GREEN}โœ“${NC} $description" - tests_passed=$((tests_passed + 1)) - test_results+=("PASS: $description") - return 0 - else - echo -e "${RED}โœ—${NC} $description" - echo " Expected: $expected" - echo " Got: $actual" - tests_failed=$((tests_failed + 1)) - test_results+=("FAIL: $description") - return 1 - fi -} - -print_summary() { - echo "" - echo "========================================" - echo "TEST SUMMARY" - echo "========================================" - echo "Total tests: $tests_run" - echo -e "${GREEN}Passed: $tests_passed${NC}" - if [ "$tests_failed" -gt 0 ]; then - echo -e "${RED}Failed: $tests_failed${NC}" - else - echo "Failed: $tests_failed" - fi - echo "" - - if [ "$tests_failed" -gt 0 ]; then - echo "Failed tests:" - for result in "${test_results[@]}"; do - if [[ "$result" == FAIL:* ]]; then - echo -e " ${RED}โœ—${NC} ${result#FAIL: }" - fi - done - echo "" - return 1 - fi - - return 0 -} - -# Setup: Source the simulator script functions +# Setup: Source the framework and simulator scripts SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "$SCRIPT_DIR/../test-framework.sh" + REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" # Source iOS setup @@ -194,7 +66,7 @@ assert_success "[ -n '$device_type' ]" "Matches 'iPhone 13' device type" # Test that generic "iPhone" without model number fails gracefully device_type=$(devicetype_id_for_name "iPhone" || echo "") if [ -z "$device_type" ]; then - echo -e "${YELLOW}โš ${NC} Generic 'iPhone' (as expected - needs specific model)" + echo " Generic 'iPhone' not matched (as expected - needs specific model)" assert_success "true" "Generic device name handling works" else assert_success "[ -n '$device_type' ]" "Matches generic 'iPhone' device type" @@ -222,7 +94,7 @@ if [ "$booted_count" -gt 0 ]; then assert_success "xcrun simctl list devices | grep '$booted_udid' | grep -q 'Booted'" "Detects booted simulator" assert_success "xcrun simctl bootstatus '$booted_udid'" "Can query boot status" else - echo -e "${YELLOW}โš ${NC} No booted simulators for state detection tests" + echo " No booted simulators for state detection tests" assert_success "true" "Skipped - no booted simulators" fi @@ -242,12 +114,12 @@ if [ -d "$devices_dir" ]; then if [ -n "$device_type" ]; then assert_success "[ -n '$device_type' ]" "Resolved device type for '$device_name'" else - echo -e "${YELLOW}โš ${NC} Could not resolve device type for '$device_name' (may not be available)" + echo " Could not resolve device type for '$device_name' (may not be available)" fi fi done else - echo -e "${YELLOW}โš ${NC} No device definitions directory found" + echo " No device definitions directory found" fi # Test 7: Simulator UDID lookup by name @@ -263,9 +135,9 @@ if [ "$booted_count" -gt 0 ]; then # Find simulator by name found_udid=$(xcrun simctl list devices | grep "$device_name" | grep "Booted" | grep -oE '[0-9A-F-]{36}' | head -1 || echo "") - assert_equals "$found_udid" "$booted_udid" "Finds simulator UDID by device name" + assert_equal "$booted_udid" "$found_udid" "Finds simulator UDID by device name" else - echo -e "${YELLOW}โš ${NC} No booted simulators for lookup tests" + echo " No booted simulators for lookup tests" assert_success "true" "Skipped - no booted simulators" fi @@ -284,7 +156,6 @@ if [ -f "$lock_file" ]; then assert_success "jq -e '.devices' '$lock_file'" "Lock file has devices array" assert_success "jq -e '.checksum' '$lock_file'" "Lock file has checksum" - assert_success "jq -e '.generated_at' '$lock_file'" "Lock file has timestamp" device_count=$(jq '.devices | length' "$lock_file") echo "Devices in lock file: $device_count" @@ -301,7 +172,7 @@ if [ -f "$lock_file" ]; then if [ -n "$device_type" ]; then assert_success "[ -n '$device_type' ]" "Device type resolved: $device_name" else - echo -e "${YELLOW}โš ${NC} Device type not available: $device_name" + echo " Device type not available: $device_name" fi # Try to resolve runtime @@ -309,11 +180,11 @@ if [ -f "$lock_file" ]; then if [ -n "$runtime_id" ]; then assert_success "[ -n '$runtime_id' ]" "Runtime resolved: iOS $device_runtime" else - echo -e "${YELLOW}โš ${NC} Runtime not available: iOS $device_runtime (may need download)" + echo " Runtime not available: iOS $device_runtime (may need download)" fi done else - echo -e "${YELLOW}โš ${NC} No lock file found at: $lock_file" + echo " No lock file found at: $lock_file" echo "Run 'devbox run ios.sh devices eval' to generate" fi @@ -322,7 +193,7 @@ log_test "CoreSimulatorService health" if pgrep -q CoreSimulatorService; then assert_success "pgrep CoreSimulatorService" "CoreSimulatorService is running" else - echo -e "${YELLOW}โš ${NC} CoreSimulatorService not running (will start when needed)" + echo " CoreSimulatorService not running (will start when needed)" assert_success "true" "CoreSimulatorService state noted" fi @@ -339,5 +210,5 @@ test_sims=$(echo "$test_sims" | tr -d '\n') echo "Test simulators found: $test_sims" assert_success "[ '$test_sims' -ge 0 ]" "Can detect test simulators" -# Print summary -print_summary +# Summary +test_summary "ios-simulator-detection" diff --git a/plugins/tests/ios/test-simulator-modes.sh b/plugins/tests/ios/test-simulator-modes.sh index 65423f2..458ef0f 100755 --- a/plugins/tests/ios/test-simulator-modes.sh +++ b/plugins/tests/ios/test-simulator-modes.sh @@ -4,20 +4,10 @@ set -euo pipefail -# Colors -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[0;33m' -BLUE='\033[0;34m' -NC='\033[0m' - -echo "========================================" -echo "Simulator Mode Behavior Tests" -echo "========================================" -echo "" - # Setup SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "$SCRIPT_DIR/../test-framework.sh" + REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" cd "$REPO_ROOT/examples/ios" @@ -34,13 +24,18 @@ export IOS_SCRIPTS_DIR . "$IOS_SCRIPTS_DIR/domain/device_manager.sh" . "$IOS_SCRIPTS_DIR/domain/simulator.sh" +echo "========================================" +echo "Simulator Mode Behavior Tests" +echo "========================================" +echo "" + echo "This test demonstrates the difference between:" echo " 1. Normal mode: Reuses existing simulator if device matches" echo " 2. Pure mode: Always creates fresh test-specific simulator" echo "" # Check current simulator state -echo -e "${BLUE}Current State:${NC}" +echo "Current State:" booted_count=$(xcrun simctl list devices | grep -c "Booted" || echo "0") total_count=$(xcrun simctl list devices | grep -E "iPhone|iPad" | grep -c -E "Booted|Shutdown" || echo "0") echo " Total simulators: $total_count" @@ -57,9 +52,7 @@ fi echo "" # Test 1: Normal mode behavior -echo "========================================" -echo "TEST 1: Normal Mode (Reuse existing)" -echo "========================================" +log_test "Normal Mode (Reuse existing)" echo "" echo "Scenario: Running 'ios.sh simulator start max' (normal mode)" @@ -73,7 +66,7 @@ echo "" # Check if simulators exist if [ "$total_count" -gt 0 ]; then - echo -e "${GREEN}โœ“${NC} Simulators exist - normal mode would check for match" + assert_success "true" "Simulators exist - normal mode would check for match" echo "" echo "Test reuse detection:" @@ -86,27 +79,25 @@ if [ "$total_count" -gt 0 ]; then echo " UDID: $udid" if xcrun simctl list devices | grep "$udid" | grep -q "Booted"; then - echo -e " ${GREEN}โœ“${NC} Simulator detection correctly identifies booted: $udid" + assert_success "true" "Simulator detection correctly identifies booted: $udid" else - echo -e " ${RED}โœ—${NC} Detection failed" + assert_failure "false" "Detection failed" fi else echo " No booted simulators - normal mode would boot existing or create new" fi else - echo -e "${YELLOW}โš ${NC} No simulators found - normal mode would create new one" + echo "No simulators found - normal mode would create new one" echo " (Run 'devbox run start:sim' to test reuse behavior)" fi echo "" # Test 2: Pure mode behavior -echo "========================================" -echo "TEST 2: Pure Mode (Fresh instance)" -echo "========================================" +log_test "Pure Mode (Fresh instance)" echo "" -echo "Scenario: Running 'ios.sh simulator start --pure max' or IN_NIX_SHELL=pure" +echo "Scenario: Running 'ios.sh simulator start --pure max' or DEVBOX_PURE_SHELL=1" echo "" echo "Expected behavior:" echo " - Always creates fresh simulator with ' Test' suffix" @@ -115,21 +106,13 @@ echo " - Creates clean state for deterministic testing" echo " - Should be deleted after test completes (in e2e tests)" echo "" -echo -e "${BLUE}Pure mode flag:${NC}" +echo "Pure mode flag:" echo " export IOS_SIMULATOR_PURE=1" echo " or" -echo " export IN_NIX_SHELL=pure" +echo " export DEVBOX_PURE_SHELL=1" echo " This triggers creation of test-specific simulator" echo "" -echo -e "${BLUE}Pure mode detection logic:${NC}" -echo " if [ \"\${IOS_SIMULATOR_PURE:-0}\" = \"1\" ] || [ \"\${IN_NIX_SHELL:-}\" = \"pure\" ]; then" -echo " # Create test simulator with ' Test' suffix" -echo " test_name=\"\${device_name} Test\"" -echo " xcrun simctl create \"\$test_name\" ..." -echo " fi" -echo "" - # Check for existing test simulators test_sims=$(xcrun simctl list devices | grep " Test" | grep -c -E "iPhone|iPad" || echo "0") echo "Test simulators currently present: $test_sims" @@ -145,9 +128,7 @@ fi echo "" # Test 3: Device matching logic -echo "========================================" -echo "TEST 3: Device Matching Logic" -echo "========================================" +log_test "Device Matching Logic" echo "" echo "How simulators are matched to device definitions:" @@ -181,9 +162,7 @@ fi echo "" # Test 4: UDID tracking -echo "========================================" -echo "TEST 4: UDID Tracking" -echo "========================================" +log_test "UDID Tracking" echo "" echo "UDID (Unique Device Identifier) is used because:" @@ -205,22 +184,20 @@ if [ "$booted_count" -gt 0 ]; then udid=$(echo "$first_line" | grep -oE '[0-9A-F-]{36}') device_name=$(echo "$first_line" | sed -E 's/^[[:space:]]*([^(]+).*/\1/' | xargs) - echo -e " ${GREEN}โœ“${NC} Example UDID: $udid" + echo " Example UDID: $udid" echo " Device: $device_name" if xcrun simctl bootstatus "$udid" >/dev/null 2>&1; then - echo -e " ${GREEN}โœ“${NC} Simulator is responsive" + echo " Simulator is responsive" fi else - echo -e " ${YELLOW}โš ${NC} No booted simulators to demonstrate" + echo " No booted simulators to demonstrate" fi echo "" # Test 5: Cleanup behavior -echo "========================================" -echo "TEST 5: Cleanup Behavior" -echo "========================================" +log_test "Cleanup Behavior" echo "" echo "Normal mode cleanup:" @@ -229,24 +206,15 @@ echo " - Simulator kept running for dev convenience" echo " - Can immediately test/debug the app" echo "" -echo "Pure mode cleanup (IN_NIX_SHELL=pure):" +echo "Pure mode cleanup (DEVBOX_PURE_SHELL=1):" echo " - Test simulator is shutdown" echo " - Test simulator is deleted: xcrun simctl delete " echo " - Next run starts completely fresh" echo " - No leftover test state" echo "" -echo "Cleanup logic in test-suite.yaml:" -echo " if [ \"\${IN_NIX_SHELL:-}\" = \"pure\" ]; then" -echo " xcrun simctl shutdown \$test_udid" -echo " xcrun simctl delete \$test_udid" -echo " fi" -echo "" - # Test 6: Runtime handling -echo "========================================" -echo "TEST 6: Runtime Resolution" -echo "========================================" +log_test "Runtime Resolution" echo "" echo "iOS runtimes (OS versions) are resolved from device definitions:" @@ -268,7 +236,7 @@ if [ -n "$available_runtimes" ]; then echo " - iOS $version" done else - echo -e " ${YELLOW}โš ${NC} No iOS runtimes found" + echo " No iOS runtimes found" fi echo "" @@ -280,19 +248,19 @@ echo "========================================" echo "" echo "Key Differences:" echo "" -echo -e "${GREEN}Normal Mode:${NC}" -echo " โœ“ Fast (reuses existing simulator)" -echo " โœ“ Good for development/iteration" -echo " โœ“ Simulator persists between runs" -echo " โœ“ Can inspect/debug app after test" -echo " โœ— May have state from previous runs" +echo "Normal Mode:" +echo " + Fast (reuses existing simulator)" +echo " + Good for development/iteration" +echo " + Simulator persists between runs" +echo " + Can inspect/debug app after test" +echo " - May have state from previous runs" echo "" -echo -e "${BLUE}Pure Mode:${NC}" -echo " โœ“ Deterministic (clean state every time)" -echo " โœ“ Good for CI/CD pipelines" -echo " โœ“ Isolated test runs" -echo " โœ“ Test simulators clearly identified (with ' Test' suffix)" -echo " โœ— Slower (creates fresh simulator)" +echo "Pure Mode:" +echo " + Deterministic (clean state every time)" +echo " + Good for CI/CD pipelines" +echo " + Isolated test runs" +echo " + Test simulators clearly identified (with ' Test' suffix)" +echo " - Slower (creates fresh simulator)" echo "" echo "Usage:" @@ -302,12 +270,7 @@ echo "" echo " # Pure mode (CI/CD workflow)" echo " devbox run --pure test:e2e" echo " # or" -echo " IN_NIX_SHELL=pure devbox run test:e2e" -echo "" - -echo "Environment detection:" -echo " Normal mode: IN_NIX_SHELL=impure or unset" -echo " Pure mode: IN_NIX_SHELL=pure (set automatically by 'devbox run --pure')" +echo " DEVBOX_PURE_SHELL=1 devbox run test:e2e" echo "" -echo -e "${GREEN}All behavior tests passed!${NC}" +echo "All behavior tests passed!" diff --git a/plugins/tests/react-native/test-lib.sh b/plugins/tests/react-native/test-lib.sh index 46de27f..575866b 100755 --- a/plugins/tests/react-native/test-lib.sh +++ b/plugins/tests/react-native/test-lib.sh @@ -5,125 +5,14 @@ set -euo pipefail -# Setup logging - redirect all output to log file -SCRIPT_DIR_NAME="$(basename "$(dirname "$0")")" -SCRIPT_NAME="$(basename "$0" .sh)" -mkdir -p "${TEST_LOGS_DIR:-reports/logs}" -LOG_FILE="${TEST_LOGS_DIR:-reports/logs}/${SCRIPT_DIR_NAME}-${SCRIPT_NAME}.txt" -exec > >(tee "$LOG_FILE") -exec 2>&1 - -# ============================================================================ -# Test Framework -# ============================================================================ - -test_passed=0 -test_failed=0 -test_name="" - -start_test() { - test_name="$1" - echo "" - echo "TEST: $test_name" -} - -assert_equal() { - expected="$1" - actual="$2" - message="${3:-}" - - if [ "$expected" = "$actual" ]; then - echo " โœ“ PASS${message:+: $message}" - test_passed=$((test_passed + 1)) - else - echo " โœ— FAIL${message:+: $message}" - echo " Expected: '$expected'" - echo " Actual: '$actual'" - test_failed=$((test_failed + 1)) - fi -} - -assert_success() { - command_str="$1" - message="${2:-}" - - if eval "$command_str" >/dev/null 2>&1; then - echo " โœ“ PASS${message:+: $message}" - test_passed=$((test_passed + 1)) - else - echo " โœ— FAIL${message:+: $message}" - echo " Command failed: $command_str" - test_failed=$((test_failed + 1)) - fi -} - -assert_failure() { - command_str="$1" - message="${2:-}" - - # Run in subshell to prevent exit from killing test script - if ! (eval "$command_str") >/dev/null 2>&1; then - echo " โœ“ PASS${message:+: $message}" - test_passed=$((test_passed + 1)) - else - echo " โœ— FAIL${message:+: $message}" - echo " Command should have failed: $command_str" - test_failed=$((test_failed + 1)) - fi -} - -assert_contains() { - haystack="$1" - needle="$2" - message="${3:-}" - - if echo "$haystack" | grep -q "$needle"; then - echo " โœ“ PASS${message:+: $message}" - test_passed=$((test_passed + 1)) - else - echo " โœ— FAIL${message:+: $message}" - echo " String '$haystack' does not contain '$needle'" - test_failed=$((test_failed + 1)) - fi -} - -test_summary() { - total=$((test_passed + test_failed)) - echo "" - echo "========================================" - echo "Test Summary" - echo "========================================" - echo "Total: $total" - echo "Passed: $test_passed" - echo "Failed: $test_failed" - echo "" - - # Write results file for summary aggregation - results_dir="${TEST_RESULTS_DIR:-$(cd "$(dirname "$0")/../../../reports/results" 2>/dev/null && pwd || echo "/tmp")}" - mkdir -p "$results_dir" 2>/dev/null || true - cat > "$results_dir/react-native-lib.json" << EOF -{ - "suite": "react-native-lib", - "passed": $test_passed, - "failed": $test_failed, - "total": $total -} -EOF - - if [ "$test_failed" -gt 0 ]; then - echo "RESULT: โœ— FAILED" - exit 1 - else - echo "RESULT: โœ“ ALL PASSED" - exit 0 - fi -} +script_dir="$(cd "$(dirname "$0")" && pwd)" +. "$script_dir/../test-framework.sh" +setup_logging # ============================================================================ # Setup # ============================================================================ -script_dir="$(cd "$(dirname "$0")" && pwd)" lib_path="$script_dir/../../react-native/virtenv/scripts/lib/lib.sh" if [ ! -f "$lib_path" ]; then @@ -132,7 +21,7 @@ if [ ! -f "$lib_path" ]; then fi # Create temporary test environment -test_virtenv="/tmp/rn-plugin-test-$$" +test_virtenv="$(make_temp_dir "rn-plugin")" mkdir -p "$test_virtenv/metro" export REACT_NATIVE_VIRTENV="$test_virtenv" @@ -146,10 +35,6 @@ echo "========================================" echo "Testing: $lib_path" echo "Test Virtenv: $test_virtenv" -# Prepare results directory -results_base="${TEST_RESULTS_DIR:-$(cd "$script_dir/../../.." 2>/dev/null && pwd)/reports/results}" -mkdir -p "$results_base" - # ============================================================================ # Tests: Port Allocation # ============================================================================ @@ -303,4 +188,4 @@ assert_success "[ -L '$test_virtenv/metro/env-ios.sh' ]" "iOS env symlink should # Cleanup test environment rm -rf "$test_virtenv" -test_summary +test_summary "react-native-lib" diff --git a/plugins/tests/test-framework.sh b/plugins/tests/test-framework.sh index 5ac8fb1..5e10a6e 100644 --- a/plugins/tests/test-framework.sh +++ b/plugins/tests/test-framework.sh @@ -1,8 +1,67 @@ #!/usr/bin/env bash +# Shared test framework for plugin unit and integration tests +# +# Source this file from any test script: +# script_dir="$(cd "$(dirname "$0")" && pwd)" +# . "$script_dir/../test-framework.sh" +# setup_logging +# +# # ... tests ... +# +# test_summary "suite-name" + set -euo pipefail -TEST_PASS=0 -TEST_FAIL=0 +# ============================================================================ +# Auto-detection +# ============================================================================ + +FRAMEWORK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="${REPO_ROOT:-$(cd "$FRAMEWORK_DIR/../.." && pwd)}" +export REPO_ROOT + +# ============================================================================ +# Counters +# ============================================================================ + +test_passed=0 +test_failed=0 + +# ============================================================================ +# Logging Setup +# ============================================================================ + +# Redirects stdout/stderr to both terminal and a log file under reports/logs/. +# Call this near the top of your test script, after sourcing the framework. +setup_logging() { + local script_dir_name script_name + script_dir_name="$(basename "$(dirname "$0")")" + script_name="$(basename "$0" .sh)" + mkdir -p "${TEST_LOGS_DIR:-reports/logs}" + LOG_FILE="${TEST_LOGS_DIR:-reports/logs}/${script_dir_name}-${script_name}.txt" + exec > >(tee "$LOG_FILE") + exec 2>&1 +} + +# ============================================================================ +# Test Section Headers +# ============================================================================ + +start_test() { + echo "" + echo "TEST: $1" +} + +log_test() { + echo "" + echo "========================================" + echo "TEST: $1" + echo "========================================" +} + +# ============================================================================ +# Assertions +# ============================================================================ assert_equal() { local expected="$1" @@ -10,13 +69,90 @@ assert_equal() { local message="${3:-}" if [ "$expected" = "$actual" ]; then - TEST_PASS=$((TEST_PASS + 1)) - echo "โœ“ ${message}" + echo " โœ“ PASS${message:+: $message}" + test_passed=$((test_passed + 1)) + else + echo " โœ— FAIL${message:+: $message}" + echo " Expected: '$expected'" + echo " Actual: '$actual'" + test_failed=$((test_failed + 1)) + fi +} + +assert_success() { + local command_str="$1" + local message="${2:-}" + + if eval "$command_str" >/dev/null 2>&1; then + echo " โœ“ PASS${message:+: $message}" + test_passed=$((test_passed + 1)) + else + echo " โœ— FAIL${message:+: $message}" + echo " Command failed: $command_str" + test_failed=$((test_failed + 1)) + fi +} + +assert_failure() { + local command_str="$1" + local message="${2:-}" + + # Run in subshell to prevent exit from killing test script + if ! (eval "$command_str") >/dev/null 2>&1; then + echo " โœ“ PASS${message:+: $message}" + test_passed=$((test_passed + 1)) + else + echo " โœ— FAIL${message:+: $message}" + echo " Command should have failed: $command_str" + test_failed=$((test_failed + 1)) + fi +} + +assert_not_empty() { + local actual="$1" + local message="${2:-}" + + if [ -n "$actual" ]; then + echo " โœ“ PASS${message:+: $message}" + test_passed=$((test_passed + 1)) + else + echo " โœ— FAIL${message:+: $message}" + echo " Value was empty" + test_failed=$((test_failed + 1)) + fi +} + +assert_contains() { + local haystack="$1" + local needle="$2" + local message="${3:-}" + + if echo "$haystack" | grep -q "$needle"; then + echo " โœ“ PASS${message:+: $message}" + test_passed=$((test_passed + 1)) + else + echo " โœ— FAIL${message:+: $message}" + echo " '$haystack' does not contain '$needle'" + test_failed=$((test_failed + 1)) + fi +} + +assert_output() { + local cmd="$1" + local expected="$2" + local description="$3" + + local output + output=$(eval "$cmd" 2>&1 || true) + + if echo "$output" | grep -q "$expected"; then + echo " โœ“ PASS${description:+: $description}" + test_passed=$((test_passed + 1)) else - TEST_FAIL=$((TEST_FAIL + 1)) - echo "โœ— ${message}" - echo " Expected: $expected" - echo " Actual: $actual" + echo " โœ— FAIL${description:+: $description}" + echo " Expected to contain: $expected" + echo " Got: $output" + test_failed=$((test_failed + 1)) fi } @@ -25,11 +161,11 @@ assert_file_exists() { local message="${2:-File exists: $file}" if [ -f "$file" ]; then - TEST_PASS=$((TEST_PASS + 1)) - echo "โœ“ ${message}" + test_passed=$((test_passed + 1)) + echo " โœ“ PASS: ${message}" else - TEST_FAIL=$((TEST_FAIL + 1)) - echo "โœ— ${message}" + test_failed=$((test_failed + 1)) + echo " โœ— FAIL: ${message}" fi } @@ -39,11 +175,11 @@ assert_file_contains() { local message="${3:-File contains pattern: $pattern}" if [ -f "$file" ] && grep -q "$pattern" "$file"; then - TEST_PASS=$((TEST_PASS + 1)) - echo "โœ“ ${message}" + test_passed=$((test_passed + 1)) + echo " โœ“ PASS: ${message}" else - TEST_FAIL=$((TEST_FAIL + 1)) - echo "โœ— ${message}" + test_failed=$((test_failed + 1)) + echo " โœ— FAIL: ${message}" fi } @@ -52,54 +188,214 @@ assert_command_success() { shift if "$@" >/dev/null 2>&1; then - TEST_PASS=$((TEST_PASS + 1)) - echo "โœ“ ${message}" + test_passed=$((test_passed + 1)) + echo " โœ“ PASS: ${message}" else - TEST_FAIL=$((TEST_FAIL + 1)) - echo "โœ— ${message}" - echo " Command failed: $*" + test_failed=$((test_failed + 1)) + echo " โœ— FAIL: ${message}" + echo " Command failed: $*" fi } +# ============================================================================ +# E2E Step Tracking +# ============================================================================ + +# Record a passing E2E step. Call from process-compose YAML processes. +e2e_step_pass() { + local step_name="$1" + mkdir -p reports/steps + echo "pass" > "reports/steps/${step_name}.status" +} + +# Record a failing E2E step with an optional reason. +e2e_step_fail() { + local step_name="$1" + local reason="${2:-Unknown error}" + mkdir -p reports/steps + printf 'fail\n%s\n' "$reason" > "reports/steps/${step_name}.status" +} + +# Read all step status files and report results. Replaces assert_file_exists +# for E2E summaries. Returns 0 if all steps passed, 1 otherwise. +e2e_report_steps() { + local steps_dir="reports/steps" + local any_failure=0 + + if [ ! -d "$steps_dir" ] || [ -z "$(ls "$steps_dir"/*.status 2>/dev/null)" ]; then + echo " No step status files found - pipeline may not have started" + test_failed=$((test_failed + 1)) + return 1 + fi + + for status_file in "$steps_dir"/*.status; do + local step_name + step_name="$(basename "$status_file" .status)" + local status + status="$(head -1 "$status_file")" + if [ "$status" = "pass" ]; then + echo " โœ“ PASS: $step_name" + test_passed=$((test_passed + 1)) + else + local reason + reason="$(tail -n +2 "$status_file")" + echo " โœ— FAIL: $step_name" + [ -n "$reason" ] && echo " Reason: $reason" + test_failed=$((test_failed + 1)) + any_failure=1 + fi + done + + rm -rf "$steps_dir" + return $any_failure +} + +# ============================================================================ +# Fixture Helpers +# ============================================================================ + +fixture_android_devices_dir() { + printf '%s\n' "$REPO_ROOT/examples/android/devbox.d/android/devices" +} + +fixture_ios_devices_dir() { + printf '%s\n' "$REPO_ROOT/examples/ios/devbox.d/ios/devices" +} + +# Creates a project-local temp directory under reports/tmp/. +# Returns the path. Caller is responsible for cleanup. +make_temp_dir() { + local label="${1:-test}" + local temp_dir="$REPO_ROOT/reports/tmp/${label}-$$" + mkdir -p "$temp_dir" + printf '%s\n' "$temp_dir" +} + +# ============================================================================ +# Test Summary +# ============================================================================ + test_summary() { local suite_name="${1:-unknown}" - local total=$((TEST_PASS + TEST_FAIL)) + local total=$((test_passed + test_failed)) echo "" - echo "====================================" - echo "Test Results:" - echo " Passed: $TEST_PASS" - echo " Failed: $TEST_FAIL" - echo "====================================" - - # Write results file for summary aggregation - # Find repo root first - # Use BASH_SOURCE[0] instead of $0 to work correctly when sourced - local repo_root="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && while [ ! -f "devbox.json" ] && [ "$(pwd)" != "/" ]; do cd ..; done; pwd)}" - - # If TEST_RESULTS_DIR is set, use it; otherwise default to repo_root/reports/results - local results_dir="${TEST_RESULTS_DIR:-$repo_root/reports/results}" + echo "========================================" + echo "Test Summary" + echo "========================================" + echo "Total: $total" + echo "Passed: $test_passed" + echo "Failed: $test_failed" + echo "" - # If results_dir is relative, make it absolute by prepending repo_root + # Write per-suite results JSON + local results_dir="${TEST_RESULTS_DIR:-$REPO_ROOT/reports/results}" if [[ ! "$results_dir" = /* ]]; then - results_dir="$repo_root/$results_dir" + results_dir="$REPO_ROOT/$results_dir" fi - mkdir -p "$results_dir" || { - echo "Warning: Could not create results directory: $results_dir" >&2 - return 0 - } - + mkdir -p "$results_dir" 2>/dev/null || true cat > "$results_dir/${suite_name}.json" << EOF { "suite": "${suite_name}", - "passed": ${TEST_PASS}, - "failed": ${TEST_FAIL}, - "total": ${total} + "passed": ${test_passed}, + "failed": ${test_failed}, + "total": ${total}, + "timestamp": "$(date '+%Y-%m-%d %H:%M:%S')" } EOF - if [ "$TEST_FAIL" -gt 0 ]; then + # Regenerate combined summary from all suite JSONs + _regenerate_summary "$results_dir" + + if [ "$test_failed" -gt 0 ]; then + echo "RESULT: โœ— FAILED" exit 1 + else + echo "RESULT: โœ“ ALL PASSED" + exit 0 fi } + +# Regenerate reports/summary.md from all per-suite JSON files. +# Called automatically by test_summary(); can also be called standalone. +_regenerate_summary() { + local results_dir="${1:-${TEST_RESULTS_DIR:-$REPO_ROOT/reports/results}}" + local reports_dir="${REPORTS_DIR:-$REPO_ROOT/reports}" + local logs_dir="${TEST_LOGS_DIR:-$reports_dir/logs}" + + # Resolve relative paths against REPO_ROOT + [[ "$results_dir" = /* ]] || results_dir="$REPO_ROOT/$results_dir" + [[ "$reports_dir" = /* ]] || reports_dir="$REPO_ROOT/$reports_dir" + [[ "$logs_dir" = /* ]] || logs_dir="$REPO_ROOT/$logs_dir" + + local summary_file="$reports_dir/summary.md" + + # Bail if no result files exist yet + local json_files + json_files=$(ls "$results_dir"/*.json 2>/dev/null | sort) || return 0 + [ -n "$json_files" ] || return 0 + + # Aggregate + local _tp=0 _tf=0 _sc=0 _af=0 + local _names=() _passed=() _failed=() _totals=() _times=() + + for rf in $json_files; do + [ -f "$rf" ] || continue + local n p f t ts + n=$(jq -r '.suite // "unknown"' "$rf" 2>/dev/null) + p=$(jq -r '.passed // 0' "$rf" 2>/dev/null) + f=$(jq -r '.failed // 0' "$rf" 2>/dev/null) + t=$(jq -r '.total // 0' "$rf" 2>/dev/null) + ts=$(jq -r '.timestamp // ""' "$rf" 2>/dev/null) + + _names+=("$n"); _passed+=("$p"); _failed+=("$f"); _totals+=("$t"); _times+=("$ts") + _tp=$((_tp + p)); _tf=$((_tf + f)); _sc=$((_sc + 1)) + [ "$f" -gt 0 ] && _af=1 + done + + local _gt=$((_tp + _tf)) + + mkdir -p "$reports_dir" 2>/dev/null || true + { + echo "# Test Suite Summary" + echo "" + echo "**Updated:** $(date '+%Y-%m-%d %H:%M:%S')" + echo "" + + if [ "$_af" -gt 0 ]; then + echo "> **SOME TESTS FAILED**" + else + echo "> **ALL ${_sc} SUITES PASSED** (${_gt} tests)" + fi + echo "" + + echo "| Suite | Passed | Failed | Total | Result | Ran At |" + echo "|-------|-------:|-------:|------:|--------|--------|" + + for i in $(seq 0 $((_sc - 1))); do + local badge="PASS" + [ "${_failed[$i]}" -gt 0 ] && badge="FAIL" + echo "| ${_names[$i]} | ${_passed[$i]} | ${_failed[$i]} | ${_totals[$i]} | ${badge} | ${_times[$i]} |" + done + + local total_badge="PASS" + [ "$_af" -gt 0 ] && total_badge="FAIL" + echo "| **TOTAL** | **${_tp}** | **${_tf}** | **${_gt}** | **${total_badge}** | |" + echo "" + + # Log files + if ls "$logs_dir"/*.txt >/dev/null 2>&1; then + echo "## Log Files" + echo "" + for log in "$logs_dir"/*.txt; do + echo "- \`$log\`" + done + echo "" + fi + + echo "---" + echo "" + echo "_Run \`devbox run test:fast\` to regenerate this summary_" + } > "$summary_file" +} diff --git a/test-results/devbox-mcp-logs b/test-results/devbox-mcp-logs deleted file mode 100644 index cba4d3a..0000000 --- a/test-results/devbox-mcp-logs +++ /dev/null @@ -1,126 +0,0 @@ -{"level":"info","process":"test-server","replica":0,"message":"Testing devbox-mcp server..."} -{"level":"info","process":"test-server","replica":0,"message":"โœ“ Server file exists"} -{"level":"info","process":"test-server","replica":0,"message":"โœ“ package.json exists"} -{"level":"info","process":"test-server","replica":0,"message":"โœ“ package.json has correct name"} -{"level":"info","process":"test-server","replica":0,"message":"โœ“ package.json has MCP SDK dependency"} -{"level":"info","process":"test-server","replica":0,"message":"โœ“ plugin.json exists"} -{"level":"info","process":"test-server","replica":0,"message":"โœ“ plugin.json has correct name"} -{"level":"info","process":"test-server","replica":0,"message":"โœ“ plugin.json includes process-compose"} -{"level":"info","process":"test-server","replica":0,"message":"โœ“ process-compose.yaml exists"} -{"level":"info","process":"test-server","replica":0,"message":"โœ“ process-compose.yaml defines mcp-server process"} -{"level":"info","process":"test-server","replica":0,"message":"Validating Node.js syntax..."} -{"level":"info","process":"test-server","replica":0,"message":"โœ“ Server JavaScript syntax is valid"} -{"level":"info","process":"test-server","replica":0,"message":"Checking required tools are defined..."} -{"level":"info","process":"test-server","replica":0,"message":"โœ“ Tool defined: devbox_run"} -{"level":"info","process":"test-server","replica":0,"message":"โœ“ Tool defined: devbox_list"} -{"level":"info","process":"test-server","replica":0,"message":"โœ“ Tool defined: devbox_add"} -{"level":"info","process":"test-server","replica":0,"message":"โœ“ Tool defined: devbox_info"} -{"level":"info","process":"test-server","replica":0,"message":"โœ“ Tool defined: devbox_search"} -{"level":"info","process":"test-server","replica":0,"message":"โœ“ Tool defined: devbox_docs_search"} -{"level":"info","process":"test-server","replica":0,"message":"โœ“ Tool defined: devbox_docs_list"} -{"level":"info","process":"test-server","replica":0,"message":"โœ“ Tool defined: devbox_docs_read"} -{"level":"info","process":"test-server","replica":0,"message":"โœ“ Tool defined: devbox_init"} -{"level":"info","process":"test-server","replica":0,"message":"โœ“ Tool defined: devbox_shell_env"} -{"level":"info","process":"test-server","replica":0,"message":"โœ“ Tool defined: devbox_sync"} -{"level":"info","process":"test-server","replica":0,"message":"โœ“ Server imports MCP SDK"} -{"level":"info","process":"test-server","replica":0,"message":"โœ“ Server imports StdioServerTransport"} -{"level":"info","process":"test-server","replica":0,"message":"โœ“ Server has error handling"} -{"level":"info","process":"test-server","replica":0} -{"level":"info","process":"test-server","replica":0,"message":"===================================="} -{"level":"info","process":"test-server","replica":0,"message":"Test Results:"} -{"level":"info","process":"test-server","replica":0,"message":" Passed: 24"} -{"level":"info","process":"test-server","replica":0,"message":" Failed: 0"} -{"level":"info","process":"test-server","replica":0,"message":"===================================="} -{"level":"info","process":"test-tools","replica":0,"message":"Testing devbox-mcp tools..."} -{"level":"info","process":"test-tools","replica":0,"message":"Testing server startup (5 second timeout)..."} -{"level":"info","process":"test-tools","replica":0,"message":"โœ“ Server starts successfully"} -{"level":"info","process":"test-tools","replica":0,"message":"Checking tool schemas..."} -{"level":"info","process":"test-tools","replica":0,"message":"โœ“ devbox_run tool defined"} -{"level":"info","process":"test-tools","replica":0,"message":"โœ“ devbox_run has command parameter"} -{"level":"info","process":"test-tools","replica":0,"message":"โœ“ devbox_list tool defined"} -{"level":"info","process":"test-tools","replica":0,"message":"โœ“ devbox_add tool defined"} -{"level":"info","process":"test-tools","replica":0,"message":"โœ“ devbox_add has packages parameter"} -{"level":"info","process":"test-tools","replica":0,"message":"โœ“ devbox_info tool defined"} -{"level":"info","process":"test-tools","replica":0,"message":"โœ“ devbox_search tool defined"} -{"level":"info","process":"test-tools","replica":0,"message":"โœ“ devbox_docs_search tool defined"} -{"level":"info","process":"test-tools","replica":0,"message":"โœ“ devbox_docs_search has maxResults parameter"} -{"level":"info","process":"test-tools","replica":0,"message":"โœ“ devbox_docs_list tool defined"} -{"level":"info","process":"test-tools","replica":0,"message":"โœ“ devbox_docs_read tool defined"} -{"level":"info","process":"test-tools","replica":0,"message":"โœ“ devbox_docs_read has filePath parameter"} -{"level":"info","process":"test-tools","replica":0,"message":"Checking helper functions..."} -{"level":"info","process":"test-tools","replica":0,"message":"โœ“ runDevbox helper function defined"} -{"level":"info","process":"test-tools","replica":0,"message":"โœ“ ensureDocsRepo helper function defined"} -{"level":"info","process":"test-tools","replica":0,"message":"โœ“ searchDocs helper function defined"} -{"level":"info","process":"test-tools","replica":0,"message":"โœ“ listDocs helper function defined"} -{"level":"info","process":"test-tools","replica":0,"message":"โœ“ readDoc helper function defined"} -{"level":"info","process":"test-tools","replica":0,"message":"Checking tool handlers..."} -{"level":"info","process":"test-tools","replica":0,"message":"โœ“ Handler exists for devbox_run"} -{"level":"info","process":"test-tools","replica":0,"message":"โœ“ Handler exists for devbox_list"} -{"level":"info","process":"test-tools","replica":0,"message":"โœ“ Handler exists for devbox_add"} -{"level":"info","process":"test-tools","replica":0,"message":"โœ“ Handler exists for devbox_info"} -{"level":"info","process":"test-tools","replica":0,"message":"โœ“ Handler exists for devbox_search"} -{"level":"info","process":"test-tools","replica":0,"message":"โœ“ Handler exists for devbox_docs_search"} -{"level":"info","process":"test-tools","replica":0,"message":"โœ“ Handler exists for devbox_docs_list"} -{"level":"info","process":"test-tools","replica":0,"message":"โœ“ Handler exists for devbox_docs_read"} -{"level":"info","process":"test-tools","replica":0,"message":"โœ“ Handler exists for devbox_init"} -{"level":"info","process":"test-tools","replica":0,"message":"โœ“ Handler exists for devbox_shell_env"} -{"level":"info","process":"test-tools","replica":0,"message":"โœ“ Handler exists for devbox_sync"} -{"level":"info","process":"test-tools","replica":0} -{"level":"info","process":"test-tools","replica":0,"message":"===================================="} -{"level":"info","process":"test-tools","replica":0,"message":"Test Results:"} -{"level":"info","process":"test-tools","replica":0,"message":" Passed: 29"} -{"level":"info","process":"test-tools","replica":0,"message":" Failed: 0"} -{"level":"info","process":"test-tools","replica":0,"message":"===================================="} -{"level":"info","process":"test-mcp-tools","replica":0,"message":"Testing MCP tools functionality..."} -{"level":"info","process":"test-mcp-tools","replica":0} -{"level":"info","process":"test-mcp-tools","replica":0,"message":"Test directory: /var/folders/4k/82wjl0fd5zvgtwh3hbcrh3rm0000gn/T/devbox-mcp-test-1771436721137"} -{"level":"info","process":"test-mcp-tools","replica":0} -{"level":"info","process":"test-mcp-tools","replica":0} -{"level":"info","process":"test-mcp-tools","replica":0,"message":"Test 1: devbox init"} -{"level":"info","process":"test-mcp-tools","replica":0,"message":"โœ“ devbox_init - Created devbox.json"} -{"level":"info","process":"test-mcp-tools","replica":0} -{"level":"info","process":"test-mcp-tools","replica":0,"message":"Test 2: devbox add"} -{"level":"info","process":"test-mcp-tools","replica":0,"message":"โœ“ devbox_add - Added hello package"} -{"level":"info","process":"test-mcp-tools","replica":0} -{"level":"info","process":"test-mcp-tools","replica":0,"message":"Test 3: devbox list"} -{"level":"info","process":"test-mcp-tools","replica":0,"message":"โœ“ devbox_list - Listed packages including hello"} -{"level":"info","process":"test-mcp-tools","replica":0} -{"level":"info","process":"test-mcp-tools","replica":0,"message":"Test 4: devbox info"} -{"level":"info","process":"test-mcp-tools","replica":0,"message":"โœ“ devbox_info - Retrieved package info"} -{"level":"info","process":"test-mcp-tools","replica":0} -{"level":"info","process":"test-mcp-tools","replica":0,"message":"Test 5: devbox search"} -{"level":"info","process":"test-mcp-tools","replica":0,"message":"โœ“ devbox_search - Searched for packages"} -{"level":"info","process":"test-mcp-tools","replica":0} -{"level":"info","process":"test-mcp-tools","replica":0,"message":"Test 6: devbox run"} -{"level":"info","process":"test-mcp-tools","replica":0,"message":"โœ“ devbox_run - Executed hello command"} -{"level":"info","process":"test-mcp-tools","replica":0} -{"level":"info","process":"test-mcp-tools","replica":0,"message":"Test 7: devbox shell env"} -{"level":"info","process":"test-mcp-tools","replica":0,"message":"โœ“ devbox_shell_env - Retrieved environment variables"} -{"level":"info","process":"test-mcp-tools","replica":0} -{"level":"info","process":"test-mcp-tools","replica":0,"message":"Test 8: docs search (cloning repo, may take 30s)"} -{"level":"info","process":"test-mcp-tools","replica":0,"message":"โœ“ devbox_docs_search - Searched documentation"} -{"level":"info","process":"test-mcp-tools","replica":0} -{"level":"info","process":"test-mcp-tools","replica":0,"message":"Test 9: docs list"} -{"level":"info","process":"test-mcp-tools","replica":0,"message":"โœ“ devbox_docs_list - Listed 174 documentation files"} -{"level":"info","process":"test-mcp-tools","replica":0} -{"level":"info","process":"test-mcp-tools","replica":0,"message":"Test 10: docs read"} -{"level":"info","process":"test-mcp-tools","replica":0,"message":"โœ“ devbox_docs_read - Read .vale/styles/write-good/README.md"} -{"level":"info","process":"test-mcp-tools","replica":0} -{"level":"info","process":"test-mcp-tools","replica":0,"message":"Cleaning up..."} -{"level":"info","process":"test-mcp-tools","replica":0} -{"level":"info","process":"test-mcp-tools","replica":0,"message":"===================================="} -{"level":"info","process":"test-mcp-tools","replica":0,"message":"Test Results:"} -{"level":"info","process":"test-mcp-tools","replica":0,"message":" Passed: 10"} -{"level":"info","process":"test-mcp-tools","replica":0,"message":" Failed: 0"} -{"level":"info","process":"test-mcp-tools","replica":0,"message":"===================================="} -{"level":"info","process":"test-mcp-tools","replica":0} -{"level":"info","process":"summary","replica":0} -{"level":"info","process":"summary","replica":0,"message":"===================================="} -{"level":"info","process":"summary","replica":0,"message":"Devbox MCP Test Suite Summary"} -{"level":"info","process":"summary","replica":0,"message":"===================================="} -{"level":"info","process":"summary","replica":0} -{"level":"info","process":"summary","replica":0,"message":"Test Logs:"} -{"level":"info","process":"summary","replica":0,"message":" test-results/devbox-mcp-logs"} -{"level":"info","process":"summary","replica":0} -{"level":"info","process":"summary","replica":0,"message":"All tests completed!"} -{"level":"info","process":"summary","replica":0,"message":"===================================="} diff --git a/tests/README.md b/tests/README.md index 1fb6707..753f697 100644 --- a/tests/README.md +++ b/tests/README.md @@ -173,7 +173,7 @@ Runs all static analysis in parallel: ### 3. Unit Test Suite (`devbox run test:unit`) -**Configuration**: `tests/process-compose-unit-tests.yaml` +**Configuration**: `tests/unit-tests.yaml` Runs plugin unit tests after linting: @@ -192,7 +192,7 @@ Runs plugin unit tests after linting: ### 4. E2E Test Suite (`devbox run test:e2e`) -**Configuration**: `tests/process-compose-e2e.yaml` +**Configuration**: `tests/e2e.yaml` Runs end-to-end tests with orchestrated dependencies (one platform at a time to avoid resource conflicts): diff --git a/tests/process-compose-e2e.yaml b/tests/e2e.yaml similarity index 84% rename from tests/process-compose-e2e.yaml rename to tests/e2e.yaml index 9d5ccdf..b4aeecf 100644 --- a/tests/process-compose-e2e.yaml +++ b/tests/e2e.yaml @@ -2,6 +2,7 @@ version: "0.5" log_location: "${REPORTS_DIR:-reports}/e2e-logs" log_level: info +is_strict: true environment: - "TEST_PURE=1" @@ -33,17 +34,14 @@ processes: availability: restart: "no" - # Summary + # Summary - only runs when all E2E tests succeed summary: command: | echo "" echo "========================================" - echo " E2E TESTS COMPLETE" + echo " E2E TESTS PASSED" echo "========================================" echo "" - echo "Results:" - echo " Check logs above for individual test results" - echo "" echo "Logs: ${REPORTS_DIR:-reports}/e2e-logs" echo "" @@ -54,13 +52,13 @@ processes: sleep 30 echo "Exiting..." fi - exit 0 depends_on: e2e-android: - condition: process_completed + condition: process_completed_successfully e2e-ios: - condition: process_completed + condition: process_completed_successfully e2e-react-native: - condition: process_completed + condition: process_completed_successfully availability: restart: "no" + exit_on_end: true diff --git a/tests/fixtures/android/devices/test-max.json b/tests/fixtures/android/devices/test-max.json deleted file mode 100644 index 05a0a08..0000000 --- a/tests/fixtures/android/devices/test-max.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "test_pixel_api36", - "api": 36, - "device": "pixel", - "tag": "google_apis" -} diff --git a/tests/fixtures/android/devices/test-min.json b/tests/fixtures/android/devices/test-min.json deleted file mode 100644 index 2bde2f9..0000000 --- a/tests/fixtures/android/devices/test-min.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "test_pixel_api21", - "api": 21, - "device": "pixel", - "tag": "google_apis" -} diff --git a/tests/fixtures/ios/devices/test-max.json b/tests/fixtures/ios/devices/test-max.json deleted file mode 100644 index b9b7ce8..0000000 --- a/tests/fixtures/ios/devices/test-max.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "test_iphone16_ios18", - "runtime": "18.0" -} diff --git a/tests/fixtures/ios/devices/test-min.json b/tests/fixtures/ios/devices/test-min.json deleted file mode 100644 index f26c57a..0000000 --- a/tests/fixtures/ios/devices/test-min.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "test_iphone15_ios17", - "runtime": "17.5" -} diff --git a/tests/integration/android/test-device-mgmt.sh b/tests/integration/android/test-device-mgmt.sh index 6450edf..f3c3c2d 100644 --- a/tests/integration/android/test-device-mgmt.sh +++ b/tests/integration/android/test-device-mgmt.sh @@ -20,12 +20,12 @@ REPO_ROOT="$SCRIPT_DIR/../../.." . "$REPO_ROOT/plugins/tests/test-framework.sh" # Setup test environment -TEST_ROOT="/tmp/android-integration-test-$$" +TEST_ROOT="$(make_temp_dir "android-integration")" mkdir -p "$TEST_ROOT/devbox.d/android/devices" mkdir -p "$TEST_ROOT/devbox.d/android/scripts" -# Copy fixtures -cp "$SCRIPT_DIR/../../fixtures/android/devices/"*.json "$TEST_ROOT/devbox.d/android/devices/" +# Copy device fixtures from example project +cp "$REPO_ROOT/examples/android/devbox.d/android/devices/"*.json "$TEST_ROOT/devbox.d/android/devices/" # Copy plugin scripts cp -r "$REPO_ROOT/plugins/android/virtenv/scripts/"* "$TEST_ROOT/devbox.d/android/scripts/" @@ -43,10 +43,10 @@ cd "$TEST_ROOT" # Test 1: Device list command echo "Test: Device listing..." if sh "$ANDROID_SCRIPTS_DIR/user/devices.sh" list >/dev/null 2>&1; then - TEST_PASS=$((TEST_PASS + 1)) + test_passed=$((test_passed + 1)) echo "โœ“ Device list command succeeds" else - TEST_FAIL=$((TEST_FAIL + 1)) + test_failed=$((test_failed + 1)) echo "โœ— Device list command failed" fi @@ -56,7 +56,7 @@ if sh "$ANDROID_SCRIPTS_DIR/user/devices.sh" eval >/dev/null 2>&1; then assert_file_exists "$ANDROID_DEVICES_DIR/devices.lock" "Lock file created after eval" else echo "โœ— Device eval command failed" - TEST_FAIL=$((TEST_FAIL + 1)) + test_failed=$((test_failed + 1)) fi # Test 3: Lock file structure @@ -64,14 +64,14 @@ echo "Test: Lock file structure..." if [ -f "$ANDROID_DEVICES_DIR/devices.lock" ]; then # Lock file should be valid JSON with devices array if jq -e '.devices' "$ANDROID_DEVICES_DIR/devices.lock" >/dev/null 2>&1; then - TEST_PASS=$((TEST_PASS + 1)) + test_passed=$((test_passed + 1)) echo "โœ“ Lock file has valid structure" else - TEST_FAIL=$((TEST_FAIL + 1)) + test_failed=$((test_failed + 1)) echo "โœ— Lock file has invalid format" fi else - TEST_FAIL=$((TEST_FAIL + 1)) + test_failed=$((test_failed + 1)) echo "โœ— Lock file not found" fi @@ -80,10 +80,10 @@ echo "Test: Device count validation..." device_count=$(jq '.devices | length' "$ANDROID_DEVICES_DIR/devices.lock") expected_count=$(ls -1 "$ANDROID_DEVICES_DIR"/*.json | wc -l | tr -d ' ') if [ "$device_count" = "$expected_count" ]; then - TEST_PASS=$((TEST_PASS + 1)) + test_passed=$((test_passed + 1)) echo "โœ“ All devices included in lock file ($device_count devices)" else - TEST_FAIL=$((TEST_FAIL + 1)) + test_failed=$((test_failed + 1)) echo "โœ— Device count mismatch (expected $expected_count, got $device_count)" fi diff --git a/tests/integration/android/test-validation.sh b/tests/integration/android/test-validation.sh index 1236253..11c335d 100644 --- a/tests/integration/android/test-validation.sh +++ b/tests/integration/android/test-validation.sh @@ -20,12 +20,12 @@ REPO_ROOT="$SCRIPT_DIR/../../.." . "$REPO_ROOT/plugins/tests/test-framework.sh" # Setup test environment -TEST_ROOT="/tmp/android-validation-test-$$" +TEST_ROOT="$(make_temp_dir "android-validation")" mkdir -p "$TEST_ROOT/devbox.d/android/devices" mkdir -p "$TEST_ROOT/devbox.d/android/scripts" -# Copy fixtures -cp "$SCRIPT_DIR/../../fixtures/android/devices/"*.json "$TEST_ROOT/devbox.d/android/devices/" +# Copy device fixtures from example project +cp "$REPO_ROOT/examples/android/devbox.d/android/devices/"*.json "$TEST_ROOT/devbox.d/android/devices/" # Copy plugin scripts cp -r "$REPO_ROOT/plugins/android/virtenv/scripts/"* "$TEST_ROOT/devbox.d/android/scripts/" @@ -37,7 +37,7 @@ export ANDROID_DEVICES_DIR="$TEST_ROOT/devbox.d/android/devices" export ANDROID_SCRIPTS_DIR="$TEST_ROOT/devbox.d/android/scripts" export ANDROID_DEVICES="" export ANDROID_SDK_ROOT="/tmp/fake-sdk" -export ANDROID_DEFAULT_DEVICE="test_pixel_api36" +export ANDROID_DEFAULT_DEVICE="medium_phone_api36" cd "$TEST_ROOT" @@ -45,14 +45,14 @@ cd "$TEST_ROOT" echo "Test: Lock file generation..." if sh "$ANDROID_SCRIPTS_DIR/user/devices.sh" eval >/dev/null 2>&1; then if [ -f "$ANDROID_DEVICES_DIR/devices.lock" ]; then - TEST_PASS=$((TEST_PASS + 1)) + test_passed=$((test_passed + 1)) echo "โœ“ Lock file generated successfully" else - TEST_FAIL=$((TEST_FAIL + 1)) + test_failed=$((test_failed + 1)) echo "โœ— Lock file not created" fi else - TEST_FAIL=$((TEST_FAIL + 1)) + test_failed=$((test_failed + 1)) echo "โœ— Device eval command failed" fi @@ -60,14 +60,14 @@ fi echo "Test: Lock file content validation..." if [ -f "$ANDROID_DEVICES_DIR/devices.lock" ]; then if [ -s "$ANDROID_DEVICES_DIR/devices.lock" ]; then - TEST_PASS=$((TEST_PASS + 1)) + test_passed=$((test_passed + 1)) echo "โœ“ Lock file has valid content" else - TEST_FAIL=$((TEST_FAIL + 1)) + test_failed=$((test_failed + 1)) echo "โœ— Lock file is empty" fi else - TEST_FAIL=$((TEST_FAIL + 1)) + test_failed=$((test_failed + 1)) echo "โœ— Lock file not found" fi @@ -77,29 +77,29 @@ if [ -f "$ANDROID_DEVICES_DIR/devices.lock" ]; then if jq -e '.checksum' "$ANDROID_DEVICES_DIR/devices.lock" >/dev/null 2>&1; then checksum=$(jq -r '.checksum' "$ANDROID_DEVICES_DIR/devices.lock") if [ -n "$checksum" ] && [ "$checksum" != "null" ]; then - TEST_PASS=$((TEST_PASS + 1)) + test_passed=$((test_passed + 1)) echo "โœ“ Lock file has valid checksum" else - TEST_FAIL=$((TEST_FAIL + 1)) + test_failed=$((test_failed + 1)) echo "โœ— Lock file checksum is invalid" fi else - TEST_FAIL=$((TEST_FAIL + 1)) + test_failed=$((test_failed + 1)) echo "โœ— Lock file missing checksum field" fi else - TEST_FAIL=$((TEST_FAIL + 1)) + test_failed=$((test_failed + 1)) echo "โœ— Lock file not found" fi # Test 4: Device list shows fixtures echo "Test: Device list validation..." device_list=$(sh "$ANDROID_SCRIPTS_DIR/user/devices.sh" list 2>/dev/null || echo "") -if echo "$device_list" | grep -q "test_pixel"; then - TEST_PASS=$((TEST_PASS + 1)) +if echo "$device_list" | grep -q "pixel_api21"; then + test_passed=$((test_passed + 1)) echo "โœ“ Device list shows test devices" else - TEST_FAIL=$((TEST_FAIL + 1)) + test_failed=$((test_failed + 1)) echo "โœ— Device list doesn't show expected devices" fi diff --git a/tests/integration/ios/test-cache.sh b/tests/integration/ios/test-cache.sh index d70ab3c..5ba6421 100644 --- a/tests/integration/ios/test-cache.sh +++ b/tests/integration/ios/test-cache.sh @@ -26,13 +26,13 @@ REPO_ROOT="$SCRIPT_DIR/../../.." . "$REPO_ROOT/plugins/tests/test-framework.sh" # Setup test environment -TEST_ROOT="/tmp/ios-cache-test-$$" +TEST_ROOT="$(make_temp_dir "ios-cache")" mkdir -p "$TEST_ROOT/devbox.d/ios/devices" mkdir -p "$TEST_ROOT/devbox.d/ios/scripts" mkdir -p "$TEST_ROOT/.devbox/virtenv/ios" -# Copy fixtures -cp "$SCRIPT_DIR/../../fixtures/ios/devices/"*.json "$TEST_ROOT/devbox.d/ios/devices/" +# Copy device fixtures from example project +cp "$REPO_ROOT/examples/ios/devbox.d/ios/devices/"*.json "$TEST_ROOT/devbox.d/ios/devices/" # Copy plugin scripts (layered structure) cp -r "$REPO_ROOT/plugins/ios/virtenv/scripts/"* "$TEST_ROOT/devbox.d/ios/scripts/" @@ -50,14 +50,14 @@ cd "$TEST_ROOT" echo "Test: Lock file generation..." if sh "$IOS_SCRIPTS_DIR/user/devices.sh" eval >/dev/null 2>&1; then if [ -f "$IOS_DEVICES_DIR/devices.lock" ]; then - TEST_PASS=$((TEST_PASS + 1)) + test_passed=$((test_passed + 1)) echo "โœ“ Lock file generated successfully" else - TEST_FAIL=$((TEST_FAIL + 1)) + test_failed=$((test_failed + 1)) echo "โœ— Lock file not created" fi else - TEST_FAIL=$((TEST_FAIL + 1)) + test_failed=$((test_failed + 1)) echo "โœ— Device eval command failed" fi @@ -65,25 +65,25 @@ fi echo "Test: Lock file content validation..." if [ -f "$IOS_DEVICES_DIR/devices.lock" ]; then if [ -s "$IOS_DEVICES_DIR/devices.lock" ]; then - TEST_PASS=$((TEST_PASS + 1)) + test_passed=$((test_passed + 1)) echo "โœ“ Lock file has valid content" else - TEST_FAIL=$((TEST_FAIL + 1)) + test_failed=$((test_failed + 1)) echo "โœ— Lock file is empty" fi else - TEST_FAIL=$((TEST_FAIL + 1)) + test_failed=$((test_failed + 1)) echo "โœ— Lock file not found" fi # Test 3: Xcode developer directory resolution echo "Test: Xcode developer directory..." if xcrun --show-sdk-path >/dev/null 2>&1; then - TEST_PASS=$((TEST_PASS + 1)) + test_passed=$((test_passed + 1)) echo "โœ“ Xcode command line tools available" else echo "โš  Xcode tools not available (skipping)" - TEST_PASS=$((TEST_PASS + 1)) # Don't fail if Xcode isn't installed + test_passed=$((test_passed + 1)) # Don't fail if Xcode isn't installed fi # Test 4: Lock file has checksum @@ -92,29 +92,29 @@ if [ -f "$IOS_DEVICES_DIR/devices.lock" ]; then if jq -e '.checksum' "$IOS_DEVICES_DIR/devices.lock" >/dev/null 2>&1; then checksum=$(jq -r '.checksum' "$IOS_DEVICES_DIR/devices.lock") if [ -n "$checksum" ] && [ "$checksum" != "null" ]; then - TEST_PASS=$((TEST_PASS + 1)) + test_passed=$((test_passed + 1)) echo "โœ“ Lock file has valid checksum" else - TEST_FAIL=$((TEST_FAIL + 1)) + test_failed=$((test_failed + 1)) echo "โœ— Lock file checksum is invalid" fi else - TEST_FAIL=$((TEST_FAIL + 1)) + test_failed=$((test_failed + 1)) echo "โœ— Lock file missing checksum field" fi else - TEST_FAIL=$((TEST_FAIL + 1)) + test_failed=$((test_failed + 1)) echo "โœ— Lock file not found" fi # Test 5: Device list shows fixtures echo "Test: Device list validation..." device_list=$(sh "$IOS_SCRIPTS_DIR/user/devices.sh" list 2>/dev/null || echo "") -if echo "$device_list" | grep -q "test_iphone"; then - TEST_PASS=$((TEST_PASS + 1)) +if echo "$device_list" | grep -q "iPhone"; then + test_passed=$((test_passed + 1)) echo "โœ“ Device list shows test devices" else - TEST_FAIL=$((TEST_FAIL + 1)) + test_failed=$((test_failed + 1)) echo "โœ— Device list doesn't show expected devices" fi diff --git a/tests/integration/ios/test-device-mgmt.sh b/tests/integration/ios/test-device-mgmt.sh index dc7a974..526af76 100644 --- a/tests/integration/ios/test-device-mgmt.sh +++ b/tests/integration/ios/test-device-mgmt.sh @@ -20,12 +20,12 @@ REPO_ROOT="$SCRIPT_DIR/../../.." . "$REPO_ROOT/plugins/tests/test-framework.sh" # Setup test environment -TEST_ROOT="/tmp/ios-integration-test-$$" +TEST_ROOT="$(make_temp_dir "ios-integration")" mkdir -p "$TEST_ROOT/devbox.d/ios/devices" mkdir -p "$TEST_ROOT/devbox.d/ios/scripts" -# Copy fixtures -cp "$SCRIPT_DIR/../../fixtures/ios/devices/"*.json "$TEST_ROOT/devbox.d/ios/devices/" +# Copy device fixtures from example project +cp "$REPO_ROOT/examples/ios/devbox.d/ios/devices/"*.json "$TEST_ROOT/devbox.d/ios/devices/" # Copy plugin scripts (layered structure) cp -r "$REPO_ROOT/plugins/ios/virtenv/scripts/"* "$TEST_ROOT/devbox.d/ios/scripts/" @@ -42,10 +42,10 @@ cd "$TEST_ROOT" # Test 1: Device list command echo "Test: Device listing..." if sh "$IOS_SCRIPTS_DIR/user/devices.sh" list >/dev/null 2>&1; then - TEST_PASS=$((TEST_PASS + 1)) + test_passed=$((test_passed + 1)) echo "โœ“ Device list command succeeds" else - TEST_FAIL=$((TEST_FAIL + 1)) + test_failed=$((test_failed + 1)) echo "โœ— Device list command failed" fi @@ -55,7 +55,7 @@ if sh "$IOS_SCRIPTS_DIR/user/devices.sh" eval >/dev/null 2>&1; then assert_file_exists "$IOS_DEVICES_DIR/devices.lock" "Lock file created after eval" else echo "โœ— Device eval command failed" - TEST_FAIL=$((TEST_FAIL + 1)) + test_failed=$((test_failed + 1)) fi # Test 3: Lock file structure @@ -63,14 +63,14 @@ echo "Test: Lock file structure..." if [ -f "$IOS_DEVICES_DIR/devices.lock" ]; then # Lock file should be valid JSON with devices array if jq -e '.devices' "$IOS_DEVICES_DIR/devices.lock" >/dev/null 2>&1; then - TEST_PASS=$((TEST_PASS + 1)) + test_passed=$((test_passed + 1)) echo "โœ“ Lock file has valid structure" else - TEST_FAIL=$((TEST_FAIL + 1)) + test_failed=$((test_failed + 1)) echo "โœ— Lock file has invalid format" fi else - TEST_FAIL=$((TEST_FAIL + 1)) + test_failed=$((test_failed + 1)) echo "โœ— Lock file not found" fi @@ -79,10 +79,10 @@ echo "Test: Device count validation..." device_count=$(jq '.devices | length' "$IOS_DEVICES_DIR/devices.lock") expected_count=$(ls -1 "$IOS_DEVICES_DIR"/*.json | wc -l | tr -d ' ') if [ "$device_count" = "$expected_count" ]; then - TEST_PASS=$((TEST_PASS + 1)) + test_passed=$((test_passed + 1)) echo "โœ“ All devices included in lock file ($device_count devices)" else - TEST_FAIL=$((TEST_FAIL + 1)) + test_failed=$((test_failed + 1)) echo "โœ— Device count mismatch (expected $expected_count, got $device_count)" fi diff --git a/tests/test-results/android-repo-e2e-logs b/tests/test-results/android-repo-e2e-logs deleted file mode 100644 index ab226d4..0000000 --- a/tests/test-results/android-repo-e2e-logs +++ /dev/null @@ -1,34 +0,0 @@ -{"level":"error","process":"init-project","replica":0,"message":"bash: line 1: cd: examples/android: No such file or directory"} -{"level":"info","process":"init-project","replica":0,"message":"Initializing Android example project..."} -{"level":"info","process":"init-project","replica":0,"message":"โœ“ Project ready"} -{"level":"info","process":"setup-avd","replica":0,"message":"Setting up AVD..."} -{"level":"error","process":"setup-avd","replica":0,"message":"bash: line 1: cd: examples/android: No such file or directory"} -{"level":"error","process":"build-app","replica":0,"message":"bash: line 1: cd: examples/android: No such file or directory"} -{"level":"info","process":"build-app","replica":0,"message":"Building Android app..."} -{"level":"error","process":"build-app","replica":0,"message":"Info: Running script \"gradle\" on /Users/abueide/code/devbox-plugins"} -{"level":"error","process":"setup-avd","replica":0,"message":"Info: Running script \"android.sh\" on /Users/abueide/code/devbox-plugins"} -{"level":"error","process":"build-app","replica":0,"message":"/Users/abueide/code/devbox-plugins/.devbox/gen/scripts/.cmd.sh: line 7: gradle: command not found"} -{"level":"error","process":"setup-avd","replica":0,"message":"/Users/abueide/code/devbox-plugins/.devbox/gen/scripts/.cmd.sh: line 7: android.sh: command not found"} -{"level":"error","process":"build-app","replica":0,"message":"Error: error running script \"gradle\" in Devbox: exit status 127"} -{"level":"error","process":"build-app","replica":0} -{"level":"error","process":"setup-avd","replica":0,"message":"Error: error running script \"android.sh\" in Devbox: exit status 127"} -{"level":"error","process":"setup-avd","replica":0} -{"level":"info","process":"cleanup","replica":0,"message":"Cleaning up..."} -{"level":"error","process":"cleanup","replica":0,"message":"bash: line 1: cd: examples/android: No such file or directory"} -{"level":"error","process":"cleanup","replica":0,"message":"bash: line 3: .devbox/virtenv/android/scripts/env.sh: No such file or directory"} -{"level":"error","process":"cleanup","replica":0,"message":"bash: line 6: android_stop_emulator: command not found"} -{"level":"info","process":"cleanup","replica":0,"message":"โœ“ Cleanup complete"} -{"level":"info","process":"summary","replica":0} -{"level":"info","process":"summary","replica":0,"message":"========================================"} -{"level":"info","process":"summary","replica":0,"message":" Android E2E Test Complete"} -{"level":"info","process":"summary","replica":0,"message":"========================================"} -{"level":"info","process":"summary","replica":0} -{"level":"info","process":"summary","replica":0,"message":"Results:"} -{"level":"info","process":"summary","replica":0,"message":" โœ“ AVD setup"} -{"level":"info","process":"summary","replica":0,"message":" โœ“ App build"} -{"level":"info","process":"summary","replica":0,"message":" โœ“ Emulator started"} -{"level":"info","process":"summary","replica":0,"message":" โœ“ App deployed"} -{"level":"info","process":"summary","replica":0,"message":" โœ“ App verified"} -{"level":"info","process":"summary","replica":0} -{"level":"info","process":"summary","replica":0,"message":"Logs: test-results/android-repo-e2e-logs"} -{"level":"info","process":"summary","replica":0} diff --git a/tests/test-results/react-native-repo-e2e-logs b/tests/test-results/react-native-repo-e2e-logs deleted file mode 100644 index 1630c5c..0000000 --- a/tests/test-results/react-native-repo-e2e-logs +++ /dev/null @@ -1,307 +0,0 @@ -{"level":"info","process":"install-deps","replica":0,"message":"Installing Node dependencies..."} -{"level":"info","process":"install-deps","replica":0} -{"level":"info","process":"install-deps","replica":0,"message":"up to date, audited 847 packages in 1s"} -{"level":"info","process":"install-deps","replica":0} -{"level":"info","process":"install-deps","replica":0,"message":"164 packages are looking for funding"} -{"level":"info","process":"install-deps","replica":0,"message":" run `npm fund` for details"} -{"level":"info","process":"install-deps","replica":0} -{"level":"info","process":"install-deps","replica":0,"message":"7 high severity vulnerabilities"} -{"level":"info","process":"install-deps","replica":0} -{"level":"info","process":"install-deps","replica":0,"message":"To address all issues (including breaking changes), run:"} -{"level":"info","process":"install-deps","replica":0,"message":" npm audit fix --force"} -{"level":"info","process":"install-deps","replica":0} -{"level":"info","process":"install-deps","replica":0,"message":"Run `npm audit` for details."} -{"level":"info","process":"ios-build","replica":0,"message":"Building iOS app..."} -{"level":"info","process":"metro-bundler","replica":0,"message":"Starting Metro bundler..."} -{"level":"info","process":"android-build","replica":0,"message":"Building Android app..."} -{"level":"info","process":"build-web","replica":0,"message":"Building web bundle..."} -{"level":"error","process":"ios-build","replica":0,"message":"Info: Running script \"build:ios\" on /Users/abueide/code/devbox-plugins/examples/react-native"} -{"level":"error","process":"android-build","replica":0,"message":"Info: Running script \"build:android\" on /Users/abueide/code/devbox-plugins/examples/react-native"} -{"level":"error","process":"build-web","replica":0,"message":"Info: Running script \"build-web\" on /Users/abueide/code/devbox-plugins/examples/react-native"} -{"level":"error","process":"ios-build","replica":0,"message":"๐Ÿ” Evaluating Android SDK from Nix flake..."} -{"level":"error","process":"ios-build","replica":0,"message":" This may take a few minutes on first run"} -{"level":"error","process":"android-build","replica":0,"message":"๐Ÿ” Evaluating Android SDK from Nix flake..."} -{"level":"error","process":"android-build","replica":0,"message":" This may take a few minutes on first run"} -{"level":"error","process":"build-web","replica":0,"message":"๐Ÿ” Evaluating Android SDK from Nix flake..."} -{"level":"error","process":"build-web","replica":0,"message":" This may take a few minutes on first run"} -{"level":"info","process":"metro-bundler","replica":0} -{"level":"info","process":"metro-bundler","replica":0,"message":"Welcome to React Native v0.83"} -{"level":"info","process":"metro-bundler","replica":0,"message":"Starting dev server on http://localhost:8081"} -{"level":"info","process":"metro-bundler","replica":0} -{"level":"info","process":"metro-bundler","replica":0} -{"level":"info","process":"metro-bundler","replica":0,"message":" โ–’โ–’โ–“โ–“โ–“โ–“โ–’โ–’"} -{"level":"info","process":"metro-bundler","replica":0,"message":" โ–’โ–“โ–“โ–“โ–’โ–’โ–‘โ–‘โ–’โ–’โ–“โ–“โ–“โ–’"} -{"level":"info","process":"metro-bundler","replica":0,"message":" โ–’โ–“โ–“โ–“โ–“โ–‘โ–‘โ–‘โ–’โ–’โ–’โ–’โ–‘โ–‘โ–‘โ–“โ–“โ–“โ–“โ–’"} -{"level":"info","process":"metro-bundler","replica":0,"message":" โ–“โ–“โ–’โ–’โ–’โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–’โ–’โ–’โ–“โ–“"} -{"level":"info","process":"metro-bundler","replica":0,"message":" โ–“โ–“โ–‘โ–‘โ–‘โ–‘โ–‘โ–’โ–“โ–“โ–“โ–“โ–“โ–“โ–’โ–‘โ–‘โ–‘โ–‘โ–‘โ–“โ–“"} -{"level":"info","process":"metro-bundler","replica":0,"message":" โ–“โ–“โ–‘โ–‘โ–“โ–“โ–’โ–‘โ–‘โ–‘โ–’โ–’โ–‘โ–‘โ–‘โ–’โ–“โ–’โ–‘โ–‘โ–“โ–“"} -{"level":"info","process":"metro-bundler","replica":0,"message":" โ–“โ–“โ–‘โ–‘โ–“โ–“โ–“โ–“โ–“โ–’โ–’โ–’โ–’โ–“โ–“โ–“โ–“โ–’โ–‘โ–‘โ–“โ–“"} -{"level":"info","process":"metro-bundler","replica":0,"message":" โ–“โ–“โ–‘โ–‘โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–’โ–‘โ–‘โ–“โ–“"} -{"level":"info","process":"metro-bundler","replica":0,"message":" โ–“โ–“โ–’โ–‘โ–‘โ–’โ–’โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–’โ–‘โ–‘โ–‘โ–’โ–“โ–“"} -{"level":"info","process":"metro-bundler","replica":0,"message":" โ–’โ–“โ–“โ–“โ–’โ–‘โ–‘โ–‘โ–’โ–“โ–“โ–’โ–‘โ–‘โ–‘โ–’โ–“โ–“โ–“โ–’"} -{"level":"info","process":"metro-bundler","replica":0,"message":" โ–’โ–“โ–“โ–“โ–’โ–‘โ–‘โ–‘โ–‘โ–’โ–“โ–“โ–“โ–’"} -{"level":"info","process":"metro-bundler","replica":0,"message":" โ–’โ–’โ–“โ–“โ–“โ–“โ–’โ–’"} -{"level":"info","process":"metro-bundler","replica":0} -{"level":"info","process":"metro-bundler","replica":0} -{"level":"info","process":"metro-bundler","replica":0,"message":" Welcome to Metro v0.83.3"} -{"level":"info","process":"metro-bundler","replica":0,"message":" Fast - Scalable - Integrated"} -{"level":"info","process":"metro-bundler","replica":0} -{"level":"info","process":"metro-bundler","replica":0} -{"level":"error","process":"build-web","replica":0,"message":"Info: Running script \"build-node\" on /Users/abueide/code/devbox-plugins/examples/react-native"} -{"level":"error","process":"android-build","replica":0,"message":"Info: Running script \"build:node\" on /Users/abueide/code/devbox-plugins/examples/react-native"} -{"level":"error","process":"build-web","replica":0,"message":"/bin/sh: /Users/abueide/code/devbox-plugins/examples/react-native/.devbox/gen/scripts/.cmd.sh: No such file or directory"} -{"level":"error","process":"build-web","replica":0,"message":"Error: error running script \"build-node\" in Devbox: exit status 127"} -{"level":"error","process":"build-web","replica":0} -{"level":"error","process":"ios-build","replica":0,"message":"Info: Running script \"build:node\" on /Users/abueide/code/devbox-plugins/examples/react-native"} -{"level":"error","process":"build-web","replica":0,"message":"Error: error running script \"build-web\" in Devbox: exit status 127"} -{"level":"error","process":"build-web","replica":0} -{"level":"info","process":"build-web","replica":0,"message":"โš  Web build skipped (optional)"} -{"level":"info","process":"metro-bundler","replica":0,"message":" INFO Dev server ready. Press Ctrl+C to exit."} -{"level":"info","process":"metro-bundler","replica":0,"message":" INFO Interactive mode is not supported in this environment"} -{"level":"info","process":"android-build","replica":0} -{"level":"info","process":"android-build","replica":0,"message":"up to date, audited 847 packages in 1s"} -{"level":"info","process":"android-build","replica":0} -{"level":"info","process":"android-build","replica":0,"message":"164 packages are looking for funding"} -{"level":"info","process":"android-build","replica":0,"message":" run `npm fund` for details"} -{"level":"info","process":"ios-build","replica":0} -{"level":"info","process":"ios-build","replica":0,"message":"up to date, audited 847 packages in 1s"} -{"level":"info","process":"ios-build","replica":0} -{"level":"info","process":"ios-build","replica":0,"message":"164 packages are looking for funding"} -{"level":"info","process":"ios-build","replica":0,"message":" run `npm fund` for details"} -{"level":"info","process":"ios-build","replica":0} -{"level":"info","process":"ios-build","replica":0,"message":"7 high severity vulnerabilities"} -{"level":"info","process":"ios-build","replica":0} -{"level":"info","process":"ios-build","replica":0,"message":"To address all issues (including breaking changes), run:"} -{"level":"info","process":"ios-build","replica":0,"message":" npm audit fix --force"} -{"level":"info","process":"ios-build","replica":0} -{"level":"info","process":"ios-build","replica":0,"message":"Run `npm audit` for details."} -{"level":"info","process":"android-build","replica":0} -{"level":"info","process":"android-build","replica":0,"message":"7 high severity vulnerabilities"} -{"level":"info","process":"android-build","replica":0} -{"level":"info","process":"android-build","replica":0,"message":"To address all issues (including breaking changes), run:"} -{"level":"info","process":"android-build","replica":0,"message":" npm audit fix --force"} -{"level":"info","process":"android-build","replica":0} -{"level":"info","process":"android-build","replica":0,"message":"Run `npm audit` for details."} -{"level":"info","process":"android-build","replica":0,"message":"> Task :gradle-plugin:settings-plugin:checkKotlinGradlePluginConfigurationErrors SKIPPED"} -{"level":"info","process":"android-build","replica":0,"message":"> Task :gradle-plugin:shared:checkKotlinGradlePluginConfigurationErrors SKIPPED"} -{"level":"info","process":"android-build","replica":0,"message":"> Task :gradle-plugin:shared:compileKotlin UP-TO-DATE"} -{"level":"info","process":"android-build","replica":0,"message":"> Task :gradle-plugin:shared:compileJava NO-SOURCE"} -{"level":"info","process":"android-build","replica":0,"message":"> Task :gradle-plugin:shared:processResources NO-SOURCE"} -{"level":"info","process":"android-build","replica":0,"message":"> Task :gradle-plugin:shared:classes UP-TO-DATE"} -{"level":"info","process":"android-build","replica":0,"message":"> Task :gradle-plugin:shared:jar UP-TO-DATE"} -{"level":"info","process":"android-build","replica":0,"message":"> Task :gradle-plugin:settings-plugin:compileKotlin UP-TO-DATE"} -{"level":"info","process":"android-build","replica":0,"message":"> Task :gradle-plugin:settings-plugin:compileJava NO-SOURCE"} -{"level":"info","process":"android-build","replica":0,"message":"> Task :gradle-plugin:settings-plugin:pluginDescriptors UP-TO-DATE"} -{"level":"info","process":"android-build","replica":0,"message":"> Task :gradle-plugin:settings-plugin:processResources UP-TO-DATE"} -{"level":"info","process":"android-build","replica":0,"message":"> Task :gradle-plugin:settings-plugin:classes UP-TO-DATE"} -{"level":"info","process":"android-build","replica":0,"message":"> Task :gradle-plugin:settings-plugin:jar UP-TO-DATE"} -{"level":"info","process":"android-build","replica":0,"message":"> Task :gradle-plugin:react-native-gradle-plugin:checkKotlinGradlePluginConfigurationErrors SKIPPED"} -{"level":"info","process":"android-build","replica":0,"message":"> Task :gradle-plugin:react-native-gradle-plugin:pluginDescriptors"} -{"level":"info","process":"android-build","replica":0,"message":"> Task :gradle-plugin:react-native-gradle-plugin:processResources"} -{"level":"info","process":"ios-build","replica":0,"message":"Command line invocation:"} -{"level":"info","process":"ios-build","replica":0,"message":" /Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild -project ReactNativeExample.xcodeproj -scheme ReactNativeExample -configuration Debug -destination \"generic/platform=iOS Simulator\" -derivedDataPath ../.devbox/virtenv/ios/DerivedData build"} -{"level":"info","process":"ios-build","replica":0} -{"level":"info","process":"ios-build","replica":0,"message":"ComputePackagePrebuildTargetDependencyGraph"} -{"level":"info","process":"ios-build","replica":0} -{"level":"info","process":"ios-build","replica":0,"message":"Prepare packages"} -{"level":"info","process":"ios-build","replica":0} -{"level":"info","process":"ios-build","replica":0,"message":"CreateBuildRequest"} -{"level":"info","process":"ios-build","replica":0} -{"level":"info","process":"ios-build","replica":0,"message":"SendProjectDescription"} -{"level":"info","process":"ios-build","replica":0} -{"level":"info","process":"ios-build","replica":0,"message":"CreateBuildOperation"} -{"level":"info","process":"ios-build","replica":0} -{"level":"info","process":"ios-build","replica":0,"message":"ComputeTargetDependencyGraph"} -{"level":"info","process":"ios-build","replica":0,"message":"note: Building targets in dependency order"} -{"level":"info","process":"ios-build","replica":0,"message":"note: Target dependency graph (1 target)"} -{"level":"info","process":"ios-build","replica":0,"message":" Target 'ReactNativeExample' in project 'ReactNativeExample' (no dependencies)"} -{"level":"info","process":"ios-build","replica":0} -{"level":"info","process":"ios-build","replica":0,"message":"GatherProvisioningInputs"} -{"level":"info","process":"ios-build","replica":0} -{"level":"info","process":"ios-build","replica":0,"message":"CreateBuildDescription"} -{"level":"info","process":"ios-build","replica":0} -{"level":"info","process":"ios-build","replica":0,"message":"ExecuteExternalTool /usr/bin/clang -v -E -dM -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator26.2.sdk -x c -c /dev/null"} -{"level":"info","process":"ios-build","replica":0} -{"level":"info","process":"ios-build","replica":0,"message":"ExecuteExternalTool /Applications/Xcode.app/Contents/Developer/usr/bin/ibtool --version --output-format xml1"} -{"level":"info","process":"ios-build","replica":0} -{"level":"info","process":"ios-build","replica":0,"message":"ExecuteExternalTool /usr/bin/clang -v -E -dM -arch arm64 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator26.2.sdk -x c -c /dev/null"} -{"level":"info","process":"ios-build","replica":0} -{"level":"info","process":"ios-build","replica":0,"message":"ExecuteExternalTool /Applications/Xcode.app/Contents/Developer/usr/bin/actool --print-asset-tag-combinations --output-format xml1 /Users/abueide/code/devbox-plugins/examples/react-native/ios/ReactNativeExample/Images.xcassets"} -{"level":"info","process":"ios-build","replica":0} -{"level":"info","process":"ios-build","replica":0,"message":"ExecuteExternalTool /Applications/Xcode.app/Contents/Developer/usr/bin/actool --version --output-format xml1"} -{"level":"info","process":"ios-build","replica":0} -{"level":"info","process":"ios-build","replica":0,"message":"ExecuteExternalTool /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swiftc --version"} -{"level":"info","process":"ios-build","replica":0} -{"level":"info","process":"ios-build","replica":0,"message":"ExecuteExternalTool /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ld -version_details"} -{"level":"info","process":"ios-build","replica":0} -{"level":"info","process":"ios-build","replica":0,"message":"ExecuteExternalTool /usr/bin/clang -v -E -dM -arch x86_64 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator26.2.sdk -x c -c /dev/null"} -{"level":"info","process":"ios-build","replica":0} -{"level":"info","process":"ios-build","replica":0,"message":"Build description signature: ffd83db717a8d47df2733fa592f20346"} -{"level":"info","process":"ios-build","replica":0,"message":"Build description path: /Users/abueide/code/devbox-plugins/examples/react-native/.devbox/virtenv/ios/DerivedData/Build/Intermediates.noindex/XCBuildData/ffd83db717a8d47df2733fa592f20346.xcbuilddata"} -{"level":"info","process":"ios-build","replica":0,"message":"/Users/abueide/code/devbox-plugins/examples/react-native/ios/ReactNativeExample.xcodeproj:1:1: error: Unable to open base configuration reference file '/Users/abueide/code/devbox-plugins/examples/react-native/ios/Pods/Target Support Files/Pods-ReactNativeExample/Pods-ReactNativeExample.debug.xcconfig'. (in target 'ReactNativeExample' from project 'ReactNativeExample')"} -{"level":"info","process":"ios-build","replica":0,"message":"warning: Unable to read contents of XCFileList '/Target Support Files/Pods-ReactNativeExample/Pods-ReactNativeExample-frameworks-Debug-output-files.xcfilelist' (in target 'ReactNativeExample' from project 'ReactNativeExample')"} -{"level":"info","process":"ios-build","replica":0,"message":"warning: Unable to read contents of XCFileList '/Target Support Files/Pods-ReactNativeExample/Pods-ReactNativeExample-resources-Debug-output-files.xcfilelist' (in target 'ReactNativeExample' from project 'ReactNativeExample')"} -{"level":"info","process":"ios-build","replica":0,"message":"warning: Run script build phase 'Bundle React Native code and images' will be run during every build because it does not specify any outputs. To address this issue, either add output dependencies to the script phase, or configure it to run in every build by unchecking \"Based on dependency analysis\" in the script phase. (in target 'ReactNativeExample' from project 'ReactNativeExample')"} -{"level":"info","process":"ios-build","replica":0,"message":"error: Unable to load contents of file list: '/Target Support Files/Pods-ReactNativeExample/Pods-ReactNativeExample-resources-Debug-input-files.xcfilelist' (in target 'ReactNativeExample' from project 'ReactNativeExample')"} -{"level":"info","process":"ios-build","replica":0,"message":"error: Unable to load contents of file list: '/Target Support Files/Pods-ReactNativeExample/Pods-ReactNativeExample-frameworks-Debug-input-files.xcfilelist' (in target 'ReactNativeExample' from project 'ReactNativeExample')"} -{"level":"info","process":"ios-build","replica":0,"message":"error: Unable to load contents of file list: '/Target Support Files/Pods-ReactNativeExample/Pods-ReactNativeExample-resources-Debug-output-files.xcfilelist' (in target 'ReactNativeExample' from project 'ReactNativeExample')"} -{"level":"info","process":"ios-build","replica":0,"message":"error: Unable to load contents of file list: '/Target Support Files/Pods-ReactNativeExample/Pods-ReactNativeExample-frameworks-Debug-output-files.xcfilelist' (in target 'ReactNativeExample' from project 'ReactNativeExample')"} -{"level":"info","process":"ios-build","replica":0,"message":"warning: Run script build phase '[CP] Copy Pods Resources' will be run during every build because it does not specify any outputs. To address this issue, either add output dependencies to the script phase, or configure it to run in every build by unchecking \"Based on dependency analysis\" in the script phase. (in target 'ReactNativeExample' from project 'ReactNativeExample')"} -{"level":"info","process":"ios-build","replica":0,"message":"warning: Run script build phase '[CP] Embed Pods Frameworks' will be run during every build because it does not specify any outputs. To address this issue, either add output dependencies to the script phase, or configure it to run in every build by unchecking \"Based on dependency analysis\" in the script phase. (in target 'ReactNativeExample' from project 'ReactNativeExample')"} -{"level":"info","process":"ios-build","replica":0,"message":"note: Explicit modules is enabled but could not resolve libclang.dylib, continuing with explicit modules disabled. (in target 'ReactNativeExample' from project 'ReactNativeExample')"} -{"level":"info","process":"ios-build","replica":0,"message":"note: Candidate '/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/libclang.dylib' skipped because it did not match the configured compiler (in target 'ReactNativeExample' from project 'ReactNativeExample')"} -{"level":"info","process":"ios-build","replica":0,"message":"note: Explicit modules is enabled but could not resolve libclang.dylib, continuing with explicit modules disabled. (in target 'ReactNativeExample' from project 'ReactNativeExample')"} -{"level":"info","process":"ios-build","replica":0,"message":"note: Candidate '/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/libclang.dylib' skipped because it did not match the configured compiler (in target 'ReactNativeExample' from project 'ReactNativeExample')"} -{"level":"error","process":"ios-build","replica":0,"message":"** BUILD FAILED **"} -{"level":"error","process":"ios-build","replica":0} -{"level":"error","process":"ios-build","replica":0} -{"level":"error","process":"ios-build","replica":0,"message":"The following build commands failed:"} -{"level":"error","process":"ios-build","replica":0,"message":"\tBuilding project ReactNativeExample with scheme ReactNativeExample and configuration Debug"} -{"level":"error","process":"ios-build","replica":0,"message":"(1 failure)"} -{"level":"error","process":"ios-build","replica":0,"message":"Error: error running script \"build:ios\" in Devbox: exit status 65"} -{"level":"error","process":"ios-build","replica":0} -{"level":"info","process":"android-setup-avd","replica":0,"message":"Setting up Android AVD..."} -{"level":"error","process":"android-setup-avd","replica":0,"message":"Info: Running script \"android.sh\" on /Users/abueide/code/devbox-plugins/examples/react-native"} -{"level":"error","process":"android-setup-avd","replica":0,"message":"๐Ÿ” Evaluating Android SDK from Nix flake..."} -{"level":"error","process":"android-setup-avd","replica":0,"message":" This may take a few minutes on first run"} -{"level":"info","process":"android-build","replica":0,"message":"> Task :gradle-plugin:react-native-gradle-plugin:compileKotlin"} -{"level":"info","process":"android-build","replica":0,"message":"> Task :gradle-plugin:react-native-gradle-plugin:compileJava NO-SOURCE"} -{"level":"info","process":"android-build","replica":0,"message":"> Task :gradle-plugin:react-native-gradle-plugin:classes"} -{"level":"info","process":"android-build","replica":0,"message":"> Task :gradle-plugin:react-native-gradle-plugin:jar"} -{"level":"info","process":"android-build","replica":0} -{"level":"info","process":"android-build","replica":0,"message":"> Configure project :"} -{"level":"error","process":"android-build","replica":0,"message":"********************************************************************************"} -{"level":"error","process":"android-build","replica":0} -{"level":"error","process":"android-build","replica":0,"message":"WARNING: Setting `newArchEnabled=false` in your `gradle.properties` file is not"} -{"level":"error","process":"android-build","replica":0,"message":"supported anymore since React Native 0.82."} -{"level":"error","process":"android-build","replica":0} -{"level":"error","process":"android-build","replica":0,"message":"You can remove the line from your `gradle.properties` file."} -{"level":"error","process":"android-build","replica":0} -{"level":"error","process":"android-build","replica":0,"message":"The application will run with the New Architecture enabled by default."} -{"level":"error","process":"android-build","replica":0} -{"level":"error","process":"android-build","replica":0,"message":"********************************************************************************"} -{"level":"error","process":"android-build","replica":0} -{"level":"info","process":"android-setup-avd","replica":0,"message":"Syncing AVDs with device definitions..."} -{"level":"info","process":"android-setup-avd","replica":0,"message":"================================================"} -{"level":"info","process":"android-cleanup","replica":0,"message":"Cleaning up Android..."} -{"level":"error","process":"android-cleanup","replica":0,"message":"Info: Running script \"stop:emu\" on /Users/abueide/code/devbox-plugins/examples/react-native"} -{"level":"info","process":"android-build","replica":0,"message":"Observed package id 'ndk;29.0.14206865' in inconsistent location '/nix/store/525b0rx2baqy7fwq7kzp7g736br1pzvf-androidsdk/libexec/android-sdk/ndk-bundle' (Expected '/nix/store/525b0rx2baqy7fwq7kzp7g736br1pzvf-androidsdk/libexec/android-sdk/ndk/29.0.14206865')"} -{"level":"info","process":"android-build","replica":0,"message":"Observed package id 'ndk;29.0.14206865' in inconsistent location '/nix/store/525b0rx2baqy7fwq7kzp7g736br1pzvf-androidsdk/libexec/android-sdk/ndk-bundle' (Expected '/nix/store/525b0rx2baqy7fwq7kzp7g736br1pzvf-androidsdk/libexec/android-sdk/ndk/29.0.14206865')"} -{"level":"info","process":"android-build","replica":0,"message":"Observed package id 'ndk;29.0.14206865' in inconsistent location '/nix/store/525b0rx2baqy7fwq7kzp7g736br1pzvf-androidsdk/libexec/android-sdk/ndk-bundle' (Expected '/nix/store/525b0rx2baqy7fwq7kzp7g736br1pzvf-androidsdk/libexec/android-sdk/ndk/29.0.14206865')"} -{"level":"info","process":"android-build","replica":0,"message":"Observed package id 'ndk;29.0.14206865' in inconsistent location '/nix/store/525b0rx2baqy7fwq7kzp7g736br1pzvf-androidsdk/libexec/android-sdk/ndk-bundle' (Expected '/nix/store/525b0rx2baqy7fwq7kzp7g736br1pzvf-androidsdk/libexec/android-sdk/ndk/29.0.14206865')"} -{"level":"info","process":"android-build","replica":0,"message":"Observed package id 'ndk;29.0.14206865' in inconsistent location '/nix/store/525b0rx2baqy7fwq7kzp7g736br1pzvf-androidsdk/libexec/android-sdk/ndk-bundle' (Expected '/nix/store/525b0rx2baqy7fwq7kzp7g736br1pzvf-androidsdk/libexec/android-sdk/ndk/29.0.14206865')"} -{"level":"error","process":"android-cleanup","replica":0,"message":"๐Ÿ” Evaluating Android SDK from Nix flake..."} -{"level":"error","process":"android-cleanup","replica":0,"message":" This may take a few minutes on first run"} -{"level":"info","process":"android-build","replica":0,"message":"Checking the license for package Android SDK Platform 35 in /nix/store/525b0rx2baqy7fwq7kzp7g736br1pzvf-androidsdk/libexec/android-sdk/licenses"} -{"level":"info","process":"android-build","replica":0,"message":"License for package Android SDK Platform 35 accepted."} -{"level":"info","process":"android-build","replica":0,"message":"Preparing \"Install Android SDK Platform 35 (revision 2)\"."} -{"level":"info","process":"android-build","replica":0,"message":"Warning: Failed to read or create install properties file."} -{"level":"info","process":"android-build","replica":0} -{"level":"info","process":"android-build","replica":0,"message":"[Incubating] Problems report is available at: file:///Users/abueide/code/devbox-plugins/examples/react-native/android/build/reports/problems/problems-report.html"} -{"level":"error","process":"android-build","replica":0} -{"level":"error","process":"android-build","replica":0,"message":"FAILURE: Build failed with an exception."} -{"level":"error","process":"android-build","replica":0} -{"level":"error","process":"android-build","replica":0,"message":"* What went wrong:"} -{"level":"error","process":"android-build","replica":0,"message":"Could not determine the dependencies of task ':app:compileDebugJavaWithJavac'."} -{"level":"error","process":"android-build","replica":0,"message":"> Failed to install the following SDK components:"} -{"level":"error","process":"android-build","replica":0,"message":" platforms;android-35 Android SDK Platform 35"} -{"level":"error","process":"android-build","replica":0,"message":" The SDK directory is not writable (/nix/store/525b0rx2baqy7fwq7kzp7g736br1pzvf-androidsdk/libexec/android-sdk)"} -{"level":"error","process":"android-build","replica":0} -{"level":"error","process":"android-build","replica":0} -{"level":"error","process":"android-build","replica":0,"message":"* Try:"} -{"level":"info","process":"android-build","replica":0} -{"level":"info","process":"android-build","replica":0,"message":"Deprecated Gradle features were used in this build, making it incompatible with Gradle 9.0."} -{"level":"info","process":"android-build","replica":0} -{"level":"info","process":"android-build","replica":0,"message":"You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins."} -{"level":"error","process":"android-build","replica":0,"message":"> Run with --stacktrace option to get the stack trace."} -{"level":"error","process":"android-build","replica":0,"message":"> Run with --info or --debug option to get more log output."} -{"level":"error","process":"android-build","replica":0,"message":"> Run with --scan to get full insights."} -{"level":"error","process":"android-build","replica":0,"message":"> Get more help at https://help.gradle.org."} -{"level":"error","process":"android-build","replica":0} -{"level":"error","process":"android-build","replica":0,"message":"BUILD FAILED in 13s"} -{"level":"info","process":"android-build","replica":0} -{"level":"info","process":"android-build","replica":0,"message":"For more on this, please refer to https://docs.gradle.org/8.14.4/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation."} -{"level":"info","process":"android-build","replica":0,"message":"10 actionable tasks: 4 executed, 6 up-to-date"} -{"level":"error","process":"android-build","replica":0,"message":"Error: error running script \"build:android\" in Devbox: exit status 1"} -{"level":"error","process":"android-build","replica":0} -{"level":"info","process":"android-cleanup","replica":0,"message":"Stopping Android emulators..."} -{"level":"info","process":"android-cleanup","replica":0,"message":"Stopping emulators: emulator-5554 "} -{"level":"info","process":"android-cleanup","replica":0,"message":"โœ“ Android emulators stopped"} -{"level":"error","process":"android-cleanup","replica":0,"message":"bash: line 4: adb: command not found"} -{"level":"info","process":"android-cleanup","replica":0,"message":"โœ“ Android cleanup complete"} -{"level":"info","process":"ios-verify-sim","replica":0,"message":"Verifying iOS simulator..."} -{"level":"error","process":"ios-verify-sim","replica":0,"message":"Info: Running script \"ios.sh\" on /Users/abueide/code/devbox-plugins/examples/react-native"} -{"level":"error","process":"ios-verify-sim","replica":0,"message":"๐Ÿ” Evaluating Android SDK from Nix flake..."} -{"level":"error","process":"ios-verify-sim","replica":0,"message":" This may take a few minutes on first run"} -{"level":"info","process":"ios-verify-sim","replica":0,"message":"Syncing simulators with device definitions..."} -{"level":"info","process":"ios-verify-sim","replica":0,"message":"================================================"} -{"level":"error","process":"ios-verify-sim","replica":0,"message":"jq: error (at :31): Cannot index string with string \"name\""} -{"level":"info","process":"ios-verify-sim","replica":0,"message":" ๐Ÿ”„ Recreating device: iPhone 17 (iOS 26.2) (iOS โ†’ 26.2)"} -{"level":"error","process":"ios-verify-sim","replica":0,"message":"jq: error (at :29): Cannot index string with string \"name\""} -{"level":"info","process":"ios-verify-sim","replica":0,"message":" ๐Ÿ”„ Recreating device: iPhone 13 (iOS 26.2) (iOS โ†’ 15.4)"} -{"level":"info","process":"ios-verify-sim","replica":0,"message":"================================================"} -{"level":"info","process":"ios-verify-sim","replica":0,"message":"Sync complete:"} -{"level":"info","process":"ios-verify-sim","replica":0,"message":" โœ“ Matched: 0"} -{"level":"info","process":"ios-verify-sim","replica":0,"message":" โš  Skipped: 2"} -{"level":"info","process":"ios-simulator","replica":0,"message":"Starting iOS simulator..."} -{"level":"error","process":"ios-simulator","replica":0,"message":"Info: Running script \"start:sim\" on /Users/abueide/code/devbox-plugins/examples/react-native"} -{"level":"error","process":"ios-simulator","replica":0,"message":"๐Ÿ” Evaluating Android SDK from Nix flake..."} -{"level":"error","process":"ios-simulator","replica":0,"message":" This may take a few minutes on first run"} -{"level":"error","process":"ios-simulator","replica":0,"message":"/Users/abueide/code/devbox-plugins/examples/react-native/.devbox/gen/scripts/start:sim.sh: line 7: ios_start: command not found"} -{"level":"error","process":"ios-simulator","replica":0,"message":"Error: error running script \"start:sim\" in Devbox: exit status 127"} -{"level":"error","process":"ios-simulator","replica":0} -{"level":"info","process":"ios-cleanup","replica":0,"message":"Cleaning up iOS..."} -{"level":"error","process":"ios-cleanup","replica":0,"message":"Info: Running script \"stop:sim\" on /Users/abueide/code/devbox-plugins/examples/react-native"} -{"level":"error","process":"ios-cleanup","replica":0,"message":"๐Ÿ” Evaluating Android SDK from Nix flake..."} -{"level":"error","process":"ios-cleanup","replica":0,"message":" This may take a few minutes on first run"} -{"level":"error","process":"ios-cleanup","replica":0,"message":"/Users/abueide/code/devbox-plugins/examples/react-native/.devbox/gen/scripts/stop:sim.sh: line 7: ios_stop: command not found"} -{"level":"error","process":"ios-cleanup","replica":0,"message":"Error: error running script \"stop:sim\" in Devbox: exit status 127"} -{"level":"error","process":"ios-cleanup","replica":0} -{"level":"info","process":"ios-cleanup","replica":0,"message":"โœ“ iOS cleanup complete"} -{"level":"info","process":"stop-metro","replica":0,"message":"Stopping Metro bundler..."} -{"level":"info","process":"stop-metro","replica":0,"message":"โœ“ Metro stopped"} -{"level":"info","process":"summary","replica":0} -{"level":"info","process":"summary","replica":0,"message":"========================================"} -{"level":"info","process":"summary","replica":0,"message":" React Native E2E Test Complete"} -{"level":"info","process":"summary","replica":0,"message":"========================================"} -{"level":"info","process":"summary","replica":0} -{"level":"info","process":"summary","replica":0,"message":"Results:"} -{"level":"info","process":"summary","replica":0,"message":" โœ“ Node dependencies installed"} -{"level":"info","process":"summary","replica":0,"message":" โœ“ Metro bundler started"} -{"level":"info","process":"summary","replica":0,"message":" โœ“ Android workflow complete"} -{"level":"info","process":"summary","replica":0,"message":" - AVD setup"} -{"level":"info","process":"summary","replica":0,"message":" - App build"} -{"level":"info","process":"summary","replica":0,"message":" - Emulator started"} -{"level":"info","process":"summary","replica":0,"message":" - App deployed"} -{"level":"info","process":"summary","replica":0,"message":" - App verified"} -{"level":"info","process":"summary","replica":0,"message":" โœ“ iOS workflow complete"} -{"level":"info","process":"summary","replica":0,"message":" - Simulator verified"} -{"level":"info","process":"summary","replica":0,"message":" - App build"} -{"level":"info","process":"summary","replica":0,"message":" - Simulator started"} -{"level":"info","process":"summary","replica":0,"message":" - App deployed"} -{"level":"info","process":"summary","replica":0,"message":" - App verified"} -{"level":"info","process":"summary","replica":0} -{"level":"info","process":"summary","replica":0,"message":"Logs: test-results/react-native-repo-e2e-logs"} -{"level":"info","process":"summary","replica":0} -{"level":"info","process":"metro-bundler","replica":0,"message":"\n"} -{"level":"info","process":"metro-bundler","replica":0,"message":"Starting Metro bundler..."} -{"level":"info","process":"metro-bundler","replica":0} -{"level":"info","process":"metro-bundler","replica":0,"message":"Welcome to React Native v0.83"} -{"level":"info","process":"metro-bundler","replica":0,"message":"Starting dev server on http://localhost:8081"} -{"level":"info","process":"metro-bundler","replica":0} -{"level":"info","process":"metro-bundler","replica":0} -{"level":"info","process":"metro-bundler","replica":0,"message":" โ–’โ–’โ–“โ–“โ–“โ–“โ–’โ–’"} -{"level":"info","process":"metro-bundler","replica":0,"message":" โ–’โ–“โ–“โ–“โ–’โ–’โ–‘โ–‘โ–’โ–’โ–“โ–“โ–“โ–’"} -{"level":"info","process":"metro-bundler","replica":0,"message":" โ–’โ–“โ–“โ–“โ–“โ–‘โ–‘โ–‘โ–’โ–’โ–’โ–’โ–‘โ–‘โ–‘โ–“โ–“โ–“โ–“โ–’"} -{"level":"info","process":"metro-bundler","replica":0,"message":" โ–“โ–“โ–’โ–’โ–’โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–’โ–’โ–’โ–“โ–“"} -{"level":"info","process":"metro-bundler","replica":0,"message":" โ–“โ–“โ–‘โ–‘โ–‘โ–‘โ–‘โ–’โ–“โ–“โ–“โ–“โ–“โ–“โ–’โ–‘โ–‘โ–‘โ–‘โ–‘โ–“โ–“"} -{"level":"info","process":"metro-bundler","replica":0,"message":" โ–“โ–“โ–‘โ–‘โ–“โ–“โ–’โ–‘โ–‘โ–‘โ–’โ–’โ–‘โ–‘โ–‘โ–’โ–“โ–’โ–‘โ–‘โ–“โ–“"} -{"level":"info","process":"metro-bundler","replica":0,"message":" โ–“โ–“โ–‘โ–‘โ–“โ–“โ–“โ–“โ–“โ–’โ–’โ–’โ–’โ–“โ–“โ–“โ–“โ–’โ–‘โ–‘โ–“โ–“"} -{"level":"info","process":"metro-bundler","replica":0,"message":" โ–“โ–“โ–‘โ–‘โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–’โ–‘โ–‘โ–“โ–“"} -{"level":"info","process":"metro-bundler","replica":0,"message":" โ–“โ–“โ–’โ–‘โ–‘โ–’โ–’โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–’โ–‘โ–‘โ–‘โ–’โ–“โ–“"} -{"level":"info","process":"metro-bundler","replica":0,"message":" โ–’โ–“โ–“โ–“โ–’โ–‘โ–‘โ–‘โ–’โ–“โ–“โ–’โ–‘โ–‘โ–‘โ–’โ–“โ–“โ–“โ–’"} -{"level":"info","process":"metro-bundler","replica":0,"message":" โ–’โ–“โ–“โ–“โ–’โ–‘โ–‘โ–‘โ–‘โ–’โ–“โ–“โ–“โ–’"} -{"level":"info","process":"metro-bundler","replica":0,"message":" โ–’โ–’โ–“โ–“โ–“โ–“โ–’โ–’"} -{"level":"info","process":"metro-bundler","replica":0} -{"level":"info","process":"metro-bundler","replica":0} -{"level":"info","process":"metro-bundler","repl \ No newline at end of file diff --git a/tests/test-summary.sh b/tests/test-summary.sh index 2af47b3..22367cb 100755 --- a/tests/test-summary.sh +++ b/tests/test-summary.sh @@ -1,421 +1,154 @@ #!/usr/bin/env bash -# Test Suite Summary Generator -# Aggregates test results from all test suites and displays summary - +# Test Suite Summary - ASCII terminal output + markdown report +# Aggregates reports/results/*.json written by test_summary() set -euo pipefail -# Setup logging to file - redirect all output through tee -mkdir -p "${TEST_LOGS_DIR:-reports/logs}" -LOG_FILE="${TEST_LOGS_DIR:-reports/logs}/summary.txt" +# Configuration +REPORTS_DIR="${REPORTS_DIR:-reports}" +TEST_RESULTS_DIR="${TEST_RESULTS_DIR:-$REPORTS_DIR/results}" +TEST_LOGS_DIR="${TEST_LOGS_DIR:-$REPORTS_DIR/logs}" -# Use exec to redirect all output to both stdout and log file +# Setup logging to file +mkdir -p "$TEST_LOGS_DIR" +LOG_FILE="$TEST_LOGS_DIR/summary.txt" exec > >(tee "$LOG_FILE") exec 2>&1 +# Source framework for _regenerate_summary +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +export REPO_ROOT +. "$REPO_ROOT/plugins/tests/test-framework.sh" + +# Regenerate reports/summary.md from all per-suite JSONs +_regenerate_summary "$TEST_RESULTS_DIR" + # ANSI color codes RED='\033[0;31m' GREEN='\033[0;32m' BOLD='\033[1m' -NC='\033[0m' # No Color - -# Configuration (can be overridden via env vars) -REPORTS_DIR="${REPORTS_DIR:-reports}" -TEST_RESULTS_DIR="${TEST_RESULTS_DIR:-$REPORTS_DIR/results}" - -# Results tracking -total_passed=0 -total_failed=0 -suite_count=0 - -echo "" -echo "========================================" -echo " TEST SUITE SUMMARY" -echo "========================================" -echo "" - -# Parse lint results -if [ -d "$REPORTS_DIR/devbox-lint-logs" ]; then - echo -e "${BOLD}Linting & Validation:${NC}" - lint_passed=0 - lint_failed=0 - - for process in lint-android-scripts lint-ios-scripts lint-react-native-scripts validate-pr-checks-workflow validate-e2e-full-workflow; do - log_file="$REPORTS_DIR/devbox-lint-logs/$process/out.log" - if [ -f "$log_file" ]; then - if grep -q "โœ“\|PASS\|valid" "$log_file" 2>/dev/null; then - lint_passed=$((lint_passed + 1)) - elif grep -q "โœ—\|FAIL\|error" "$log_file" 2>/dev/null; then - lint_failed=$((lint_failed + 1)) - fi - fi - done - - if [ $lint_failed -eq 0 ]; then - echo -e " ${GREEN}โœ“${NC} Shellcheck & Workflows: ${lint_passed} checks passed" - else - echo -e " ${RED}โœ—${NC} Shellcheck & Workflows: ${lint_passed} passed, ${lint_failed} failed" - fi - - total_passed=$((total_passed + lint_passed)) - total_failed=$((total_failed + lint_failed)) - suite_count=$((suite_count + 1)) -fi - -# Parse Android plugin unit tests -echo "" -echo -e "${BOLD}Android Plugin Tests:${NC}" -android_passed=0 -android_failed=0 - -# Check for JSON result files -if [ -f "$TEST_RESULTS_DIR/android-lib.json" ]; then - passed=$(jq -r '.passed' $TEST_RESULTS_DIR/android-lib.json) - failed=$(jq -r '.failed' $TEST_RESULTS_DIR/android-lib.json) - android_passed=$((android_passed + passed)) - android_failed=$((android_failed + failed)) - - if [ "$failed" -eq 0 ]; then - echo -e " ${GREEN}โœ“${NC} lib.sh: ${passed} tests passed" - else - echo -e " ${RED}โœ—${NC} lib.sh: ${passed} passed, ${failed} failed" - fi -else - echo -e " ${NC}โš  lib.sh: no results found${NC}" -fi - -if [ -f "$TEST_RESULTS_DIR/android-devices.json" ]; then - passed=$(jq -r '.passed' $TEST_RESULTS_DIR/android-devices.json) - failed=$(jq -r '.failed' $TEST_RESULTS_DIR/android-devices.json) - android_passed=$((android_passed + passed)) - android_failed=$((android_failed + failed)) - - if [ "$failed" -eq 0 ]; then - echo -e " ${GREEN}โœ“${NC} devices.sh: ${passed} tests passed" - else - echo -e " ${RED}โœ—${NC} devices.sh: ${passed} passed, ${failed} failed" - fi -else - echo -e " ${NC}โš  devices.sh: no results found${NC}" -fi - -total_passed=$((total_passed + android_passed)) -total_failed=$((total_failed + android_failed)) -if [ $android_passed -gt 0 ] || [ $android_failed -gt 0 ]; then - suite_count=$((suite_count + 1)) -fi +DIM='\033[2m' +NC='\033[0m' + +# ============================================================================ +# Collect results for ASCII output +# ============================================================================ + +_total_passed=0 +_total_failed=0 +_suite_count=0 +_any_failure=0 + +_suite_names=() +_suite_passed=() +_suite_failed=() +_suite_totals=() +_suite_times=() + +for result_file in $(ls "$TEST_RESULTS_DIR"/*.json 2>/dev/null | sort); do + [ -f "$result_file" ] || continue + name=$(jq -r '.suite // "unknown"' "$result_file" 2>/dev/null) + p=$(jq -r '.passed // 0' "$result_file" 2>/dev/null) + f=$(jq -r '.failed // 0' "$result_file" 2>/dev/null) + t=$(jq -r '.total // 0' "$result_file" 2>/dev/null) + ts=$(jq -r '.timestamp // ""' "$result_file" 2>/dev/null) + + _suite_names+=("$name") + _suite_passed+=("$p") + _suite_failed+=("$f") + _suite_totals+=("$t") + _suite_times+=("$ts") + + _total_passed=$((_total_passed + p)) + _total_failed=$((_total_failed + f)) + _suite_count=$((_suite_count + 1)) + if [ "$f" -gt 0 ]; then _any_failure=1; fi +done + +_grand_total=$((_total_passed + _total_failed)) + +# Column width from longest suite name +col=10 +for name in "${_suite_names[@]}"; do + [ ${#name} -gt "$col" ] && col=${#name} +done + +# ============================================================================ +# ASCII box table +# ============================================================================ + +divider=$(printf '%0.sโ”€' $(seq 1 $((col + 56)))) -# Parse iOS plugin unit tests echo "" -echo -e "${BOLD}iOS Plugin Tests:${NC}" -ios_passed=0 -ios_failed=0 - -# Check for JSON result files -if [ -f "$TEST_RESULTS_DIR/ios-lib.json" ]; then - passed=$(jq -r '.passed' $TEST_RESULTS_DIR/ios-lib.json) - failed=$(jq -r '.failed' $TEST_RESULTS_DIR/ios-lib.json) - ios_passed=$((ios_passed + passed)) - ios_failed=$((ios_failed + failed)) - - if [ "$failed" -eq 0 ]; then - echo -e " ${GREEN}โœ“${NC} lib.sh: ${passed} tests passed" - else - echo -e " ${RED}โœ—${NC} lib.sh: ${passed} passed, ${failed} failed" - fi -else - echo -e " ${NC}โš  lib.sh: no results found${NC}" -fi - -if [ -f "$TEST_RESULTS_DIR/ios-devices.json" ]; then - passed=$(jq -r '.passed' $TEST_RESULTS_DIR/ios-devices.json) - failed=$(jq -r '.failed' $TEST_RESULTS_DIR/ios-devices.json) - ios_passed=$((ios_passed + passed)) - ios_failed=$((ios_failed + failed)) - - if [ "$failed" -eq 0 ]; then - echo -e " ${GREEN}โœ“${NC} devices.sh: ${passed} tests passed" +echo "โ”Œ${divider}โ”" +printf "โ”‚%*sโ”‚\n" $((col + 56)) "" +if [ "$_any_failure" -gt 0 ]; then + label="SOME TESTS FAILED" + pad_total=$((col + 56 - ${#label})) + pad_left=$((pad_total / 2)) + pad_right=$((pad_total - pad_left)) + printf "โ”‚$(printf '%*s' "$pad_left" "")${RED}${BOLD}%s${NC}$(printf '%*s' "$pad_right" "")โ”‚\n" "$label" +else + label="ALL ${_suite_count} SUITES PASSED (${_grand_total} tests)" + pad_total=$((col + 56 - ${#label})) + pad_left=$((pad_total / 2)) + pad_right=$((pad_total - pad_left)) + printf "โ”‚$(printf '%*s' "$pad_left" "")${GREEN}${BOLD}%s${NC}$(printf '%*s' "$pad_right" "")โ”‚\n" "$label" +fi +printf "โ”‚%*sโ”‚\n" $((col + 56)) "" +echo "โ”œ${divider}โ”ค" + +# Table header +printf "โ”‚ ${BOLD}%-${col}s โ”‚ %7s โ”‚ %7s โ”‚ %7s โ”‚ %-6s โ”‚ %-19s${NC} โ”‚\n" "Suite" "Passed" "Failed" "Total" "Result" "Ran At" +echo "โ”œ${divider}โ”ค" + +# Suite rows +for i in $(seq 0 $((_suite_count - 1))); do + if [ "${_suite_failed[$i]}" -gt 0 ]; then + status="${RED}FAIL${NC} " else - echo -e " ${RED}โœ—${NC} devices.sh: ${passed} passed, ${failed} failed" + status="${GREEN}PASS${NC} " fi -else - echo -e " ${NC}โš  devices.sh: no results found${NC}" -fi + printf "โ”‚ %-${col}s โ”‚ %7d โ”‚ %7d โ”‚ %7d โ”‚ %bโ”‚ %-19s โ”‚\n" \ + "${_suite_names[$i]}" "${_suite_passed[$i]}" "${_suite_failed[$i]}" "${_suite_totals[$i]}" "$status" "${_suite_times[$i]}" +done -total_passed=$((total_passed + ios_passed)) -total_failed=$((total_failed + ios_failed)) -if [ $ios_passed -gt 0 ] || [ $ios_failed -gt 0 ]; then - suite_count=$((suite_count + 1)) +if [ "$_suite_count" -eq 0 ]; then + printf "โ”‚ ${DIM}%-$((col + 54))s${NC} โ”‚\n" "No test results found in $TEST_RESULTS_DIR/" fi -# Parse React Native plugin unit tests -echo "" -echo -e "${BOLD}React Native Plugin Tests:${NC}" -rn_passed=0 -rn_failed=0 - -if [ -f "$TEST_RESULTS_DIR/react-native-lib.json" ]; then - passed=$(jq -r '.passed' $TEST_RESULTS_DIR/react-native-lib.json) - failed=$(jq -r '.failed' $TEST_RESULTS_DIR/react-native-lib.json) - rn_passed=$((rn_passed + passed)) - rn_failed=$((rn_failed + failed)) - - if [ "$failed" -eq 0 ]; then - echo -e " ${GREEN}โœ“${NC} lib.sh: ${passed} tests passed" - else - echo -e " ${RED}โœ—${NC} lib.sh: ${passed} passed, ${failed} failed" - fi +# Totals row +echo "โ”œ${divider}โ”ค" +if [ "$_any_failure" -gt 0 ]; then + result_label="${RED}${BOLD}FAIL${NC} " else - echo -e " ${NC}โš  lib.sh: no results found${NC}" + result_label="${GREEN}${BOLD}PASS${NC} " fi - -total_passed=$((total_passed + rn_passed)) -total_failed=$((total_failed + rn_failed)) -if [ $rn_passed -gt 0 ] || [ $rn_failed -gt 0 ]; then - suite_count=$((suite_count + 1)) -fi - -# Parse integration tests +printf "โ”‚ ${BOLD}%-${col}s${NC} โ”‚ ${BOLD}%7d${NC} โ”‚ ${BOLD}%7d${NC} โ”‚ ${BOLD}%7d${NC} โ”‚ %bโ”‚ %-19s โ”‚\n" \ + "TOTAL" "$_total_passed" "$_total_failed" "$_grand_total" "$result_label" "" +echo "โ””${divider}โ”˜" echo "" -echo -e "${BOLD}Integration Tests:${NC}" -integration_passed=0 -integration_failed=0 - -# Android integration tests -if [ -f "$TEST_RESULTS_DIR/android-integration-device-mgmt.json" ]; then - passed=$(jq -r '.passed' $TEST_RESULTS_DIR/android-integration-device-mgmt.json) - failed=$(jq -r '.failed' $TEST_RESULTS_DIR/android-integration-device-mgmt.json) - integration_passed=$((integration_passed + passed)) - integration_failed=$((integration_failed + failed)) - - if [ "$failed" -eq 0 ]; then - echo -e " ${GREEN}โœ“${NC} android device mgmt: ${passed} tests passed" - else - echo -e " ${RED}โœ—${NC} android device mgmt: ${passed} passed, ${failed} failed" - fi -else - echo -e " ${NC}โš  android device mgmt: no results found${NC}" -fi - -if [ -f "$TEST_RESULTS_DIR/android-integration-validation.json" ]; then - passed=$(jq -r '.passed' $TEST_RESULTS_DIR/android-integration-validation.json) - failed=$(jq -r '.failed' $TEST_RESULTS_DIR/android-integration-validation.json) - integration_passed=$((integration_passed + passed)) - integration_failed=$((integration_failed + failed)) - - if [ "$failed" -eq 0 ]; then - echo -e " ${GREEN}โœ“${NC} android validation: ${passed} tests passed" - else - echo -e " ${RED}โœ—${NC} android validation: ${passed} passed, ${failed} failed" - fi -else - echo -e " ${NC}โš  android validation: no results found${NC}" -fi - -# iOS integration tests -if [ -f "$TEST_RESULTS_DIR/ios-integration-device-mgmt.json" ]; then - passed=$(jq -r '.passed' $TEST_RESULTS_DIR/ios-integration-device-mgmt.json) - failed=$(jq -r '.failed' $TEST_RESULTS_DIR/ios-integration-device-mgmt.json) - integration_passed=$((integration_passed + passed)) - integration_failed=$((integration_failed + failed)) - - if [ "$failed" -eq 0 ]; then - echo -e " ${GREEN}โœ“${NC} ios device mgmt: ${passed} tests passed" - else - echo -e " ${RED}โœ—${NC} ios device mgmt: ${passed} passed, ${failed} failed" - fi -else - echo -e " ${NC}โš  ios device mgmt: no results found${NC}" -fi - -if [ -f "$TEST_RESULTS_DIR/ios-integration-cache.json" ]; then - passed=$(jq -r '.passed' $TEST_RESULTS_DIR/ios-integration-cache.json) - failed=$(jq -r '.failed' $TEST_RESULTS_DIR/ios-integration-cache.json) - integration_passed=$((integration_passed + passed)) - integration_failed=$((integration_failed + failed)) - - if [ "$failed" -eq 0 ]; then - echo -e " ${GREEN}โœ“${NC} ios cache: ${passed} tests passed" - else - echo -e " ${RED}โœ—${NC} ios cache: ${passed} passed, ${failed} failed" - fi -else - echo -e " ${NC}โš  ios cache: no results found${NC}" -fi -total_passed=$((total_passed + integration_passed)) -total_failed=$((total_failed + integration_failed)) -if [ $integration_passed -gt 0 ] || [ $integration_failed -gt 0 ]; then - suite_count=$((suite_count + 1)) -fi - -# Parse devbox-mcp tests -if [ -d "$REPORTS_DIR/devbox-mcp-logs" ]; then +# Log file listing +if ls "$TEST_LOGS_DIR"/*.txt >/dev/null 2>&1; then + echo -e "${DIM}Log files:${NC}" + for log in "$TEST_LOGS_DIR"/*.txt; do + echo -e " ${DIM}$log${NC}" + done echo "" - echo -e "${BOLD}Devbox MCP Tests:${NC}" - mcp_passed=0 - mcp_failed=0 - - log_file="$REPORTS_DIR/devbox-mcp-logs/test-mcp-tools/out.log" - if [ -f "$log_file" ]; then - log_content=$(cat "$log_file") - if echo "$log_content" | grep -q "Passed:"; then - passed=$(echo "$log_content" | grep "Passed:" | awk '{print $2}') - failed=$(echo "$log_content" | grep "Failed:" | awk '{print $2}') - mcp_passed=$passed - mcp_failed=$failed - - if [ "$failed" -eq 0 ]; then - echo -e " ${GREEN}โœ“${NC} MCP Tools: ${passed} tests passed" - else - echo -e " ${RED}โœ—${NC} MCP Tools: ${passed} passed, ${failed} failed" - fi - fi - fi - - total_passed=$((total_passed + mcp_passed)) - total_failed=$((total_failed + mcp_failed)) - suite_count=$((suite_count + 1)) fi -# Final summary -echo "" -echo "========================================" -if [ $total_failed -eq 0 ]; then - echo -e "${GREEN}${BOLD} ALL TESTS PASSED โœ“${NC}" -else - echo -e "${RED}${BOLD} SOME TESTS FAILED โœ—${NC}" -fi -echo "========================================" -echo "" -echo "Results:" -echo " Test Suites: ${suite_count}" -echo -e " ${GREEN}Passed: ${total_passed}${NC}" -if [ $total_failed -gt 0 ]; then - echo -e " ${RED}Failed: ${total_failed}${NC}" -else - echo " Failed: 0" -fi -echo "" -echo "Test Log Files:" -echo " $REPORTS_DIR/logs/android-test-lib.txt" -echo " $REPORTS_DIR/logs/android-test-devices.txt" -echo " $REPORTS_DIR/logs/android-test-device-mgmt.txt" -echo " $REPORTS_DIR/logs/android-test-validation.txt" -echo " $REPORTS_DIR/logs/ios-test-lib.txt" -echo " $REPORTS_DIR/logs/ios-test-devices.txt" -echo " $REPORTS_DIR/logs/ios-test-device-mgmt.txt" -echo " $REPORTS_DIR/logs/ios-test-cache.txt" -echo " $REPORTS_DIR/logs/react-native-test-lib.txt" -echo " $REPORTS_DIR/logs/summary.txt" -echo "" -echo "Lint Logs:" -echo " $REPORTS_DIR/devbox-lint-logs/" +echo -e "${DIM}Result files: $TEST_RESULTS_DIR/*.json${NC}" +echo -e "${DIM}Markdown report: $REPORTS_DIR/summary.md${NC}" echo "" -echo "Result Files:" -echo " $REPORTS_DIR/results/*.json" -echo "" - -# Write markdown summary -summary_file="$REPORTS_DIR/summary.md" -mkdir -p "$REPORTS_DIR" - -cat > "$summary_file" << MDEOF -# Test Suite Summary - -**Generated:** $(date '+%Y-%m-%d %H:%M:%S') - -## Overall Results - -$(if [ $total_failed -eq 0 ]; then echo "โœ… **ALL TESTS PASSED**"; else echo "โŒ **SOME TESTS FAILED**"; fi) - -- **Test Suites:** ${suite_count} -- **Total Passed:** ${total_passed} -- **Total Failed:** ${total_failed} - ---- -## Test Breakdown - -### Linting & Validation -$(if [ -d "$REPORTS_DIR/devbox-lint-logs" ]; then - echo "- Android scripts: โœ…" - echo "- iOS scripts: โœ…" - echo "- React Native scripts: โœ…" - echo "- Workflow validation: โœ…" -else - echo "No lint results found" -fi) - -### Android Plugin Tests -$(if [ $android_passed -gt 0 ] || [ $android_failed -gt 0 ]; then - echo "- lib.sh: $(if [ $android_failed -eq 0 ]; then echo "โœ…"; else echo "โŒ"; fi) (Passed: $android_passed, Failed: $android_failed)" - echo "- devices.sh: โœ…" -else - echo "No Android test results found" -fi) - -### iOS Plugin Tests -$(if [ $ios_passed -gt 0 ] || [ $ios_failed -gt 0 ]; then - echo "- lib.sh: $(if [ $ios_failed -eq 0 ]; then echo "โœ…"; else echo "โŒ"; fi) (Passed: $ios_passed, Failed: $ios_failed)" -else - echo "No iOS test results found" -fi) - -### React Native Plugin Tests -$(if [ $rn_passed -gt 0 ] || [ $rn_failed -gt 0 ]; then - echo "- lib.sh: $(if [ $rn_failed -eq 0 ]; then echo "โœ…"; else echo "โŒ"; fi) (Passed: $rn_passed, Failed: $rn_failed)" -else - echo "No React Native test results found" -fi) - -### Integration Tests -$(if [ $integration_passed -gt 0 ] || [ $integration_failed -gt 0 ]; then - echo "- Android device mgmt: $(if [ $integration_failed -eq 0 ]; then echo "โœ…"; else echo "โš ๏ธ"; fi)" - echo "- Android validation: โœ…" - echo "- iOS device mgmt: โœ…" - echo "- iOS cache: โœ…" - echo "" - echo "Total: Passed: $integration_passed, Failed: $integration_failed" -else - echo "No integration test results found" -fi) - -### Devbox MCP Tests -$(if [ -d "$REPORTS_DIR/devbox-mcp-logs" ]; then - if [ -f "$REPORTS_DIR/devbox-mcp-logs/test-mcp-tools/out.log" ]; then - log=$(cat "$REPORTS_DIR/devbox-mcp-logs/test-mcp-tools/out.log") - if echo "$log" | grep -q "Passed:"; then - passed=$(echo "$log" | grep "Passed:" | awk '{print $2}') - failed=$(echo "$log" | grep "Failed:" | awk '{print $2}') - echo "- MCP Tools: $(if [ "$failed" -eq 0 ]; then echo "โœ…"; else echo "โŒ"; fi) (Passed: $passed, Failed: $failed)" - else - echo "- MCP Tools: โœ…" - fi - else - echo "- MCP Tools: โœ…" - fi -else - echo "No devbox-mcp test results found" -fi) - ---- - -## Log Files - -Detailed logs available in: -- \`$REPORTS_DIR/devbox-lint-logs/\` -- \`$REPORTS_DIR/devbox-unit-tests-logs/\` -- \`$REPORTS_DIR/devbox-mcp-logs/\` - ---- - -_Run \`devbox run test:fast\` to regenerate this summary_ -MDEOF - -echo "Summary written to: $summary_file" -echo "" +# TUI mode: sleep so the user can read the summary before process-compose exits +if [ "${TEST_TUI:-false}" = "true" ] || [ "${TEST_TUI:-0}" = "1" ]; then + echo -e "${DIM}TUI mode: waiting 30s before exit (Ctrl+C to skip)...${NC}" + sleep 30 || true +fi # Exit with failure if any tests failed -if [ $total_failed -gt 0 ]; then +if [ "$_any_failure" -gt 0 ]; then exit 1 fi diff --git a/tests/process-compose-unit-tests.yaml b/tests/unit-tests.yaml similarity index 61% rename from tests/process-compose-unit-tests.yaml rename to tests/unit-tests.yaml index cc2cc5b..f9fd3f5 100644 --- a/tests/process-compose-unit-tests.yaml +++ b/tests/unit-tests.yaml @@ -2,6 +2,7 @@ version: "0.5" log_location: "${REPORTS_DIR:-reports}/devbox-unit-tests-logs" log_level: info +is_strict: true processes: # Android plugin unit tests @@ -15,6 +16,16 @@ processes: availability: restart: "no" + test-android-apk-detection: + command: "cd examples/android && devbox run bash ../../plugins/tests/android/test-apk-detection.sh" + availability: + restart: "no" + + test-android-apk-resolution: + command: "bash plugins/tests/android/test-apk-resolution.sh" + availability: + restart: "no" + # Android integration tests test-android-device-mgmt: command: "bash tests/integration/android/test-device-mgmt.sh" @@ -37,6 +48,11 @@ processes: availability: restart: "no" + test-ios-app-resolution: + command: "bash plugins/tests/ios/test-app-resolution.sh" + availability: + restart: "no" + # iOS integration tests test-ios-device-mgmt: command: "bash tests/integration/ios/test-device-mgmt.sh" @@ -54,7 +70,13 @@ processes: availability: restart: "no" - # Summary - runs after all tests complete + # devbox-mcp plugin tests + test-devbox-mcp: + command: "process-compose -f plugins/devbox-mcp/tests/test-suite.yaml --no-server --tui=false" + availability: + restart: "no" + + # Summary - only runs when all tests pass summary: command: | echo "" @@ -65,20 +87,26 @@ processes: echo "Android Tests:" echo " - lib.sh" echo " - devices.sh" + echo " - apk-detection" + echo " - apk-resolution" echo " - device-mgmt (integration)" echo " - validation (integration)" echo "" echo "iOS Tests:" echo " - lib.sh" echo " - devices.sh" + echo " - app-resolution" echo " - device-mgmt (integration)" echo " - cache (integration)" echo "" echo "React Native Tests:" echo " - lib.sh" echo "" + echo "devbox-mcp Tests:" + echo " - All tests" + echo "" echo "========================================" - echo " ALL UNIT TESTS COMPLETE" + echo " ALL UNIT TESTS PASSED" echo "========================================" echo "" @@ -89,25 +117,33 @@ processes: sleep 30 echo "Exiting..." fi - exit 0 depends_on: test-android-lib: - condition: process_completed + condition: process_completed_successfully test-android-devices: - condition: process_completed + condition: process_completed_successfully + test-android-apk-detection: + condition: process_completed_successfully + test-android-apk-resolution: + condition: process_completed_successfully test-android-device-mgmt: - condition: process_completed + condition: process_completed_successfully test-android-validation: - condition: process_completed + condition: process_completed_successfully test-ios-lib: - condition: process_completed + condition: process_completed_successfully test-ios-devices: - condition: process_completed + condition: process_completed_successfully + test-ios-app-resolution: + condition: process_completed_successfully test-ios-device-mgmt: - condition: process_completed + condition: process_completed_successfully test-ios-cache: - condition: process_completed + condition: process_completed_successfully test-rn-lib: - condition: process_completed + condition: process_completed_successfully + test-devbox-mcp: + condition: process_completed_successfully availability: restart: "no" + exit_on_end: true diff --git a/treefmt.toml b/treefmt.toml index 447b81c..2ab75af 100644 --- a/treefmt.toml +++ b/treefmt.toml @@ -28,6 +28,7 @@ excludes = [ "node_modules/*", "examples/*/node_modules/*", "test-results/*", + "tests/test-results/*", "reports/*", ] diff --git a/wiki/README.md b/wiki/README.md index 87a95be..92adfe4 100644 --- a/wiki/README.md +++ b/wiki/README.md @@ -15,7 +15,7 @@ Welcome to the devbox-plugins documentation. This wiki provides comprehensive gu Step-by-step tutorials and practical workflows for using the plugins: -- [Quick Start](guides/quick-start.md) - Get started in 5 minutes +- [Quick Start](guides/quick-start.md) - Set up your first project - [Android Guide](guides/android-guide.md) - Complete Android workflow - [iOS Guide](guides/ios-guide.md) - Complete iOS workflow - [React Native Guide](guides/react-native-guide.md) - Complete React Native workflow diff --git a/wiki/guides/android-guide.md b/wiki/guides/android-guide.md index 237510a..a246c38 100644 --- a/wiki/guides/android-guide.md +++ b/wiki/guides/android-guide.md @@ -6,11 +6,12 @@ A complete guide to Android development using the Devbox Android plugin. This gu The Android plugin enables reproducible Android development without touching global system state. It provides: -- **Project-local Android environment**: All Android user data (AVDs, emulator configs, adb keys) is stored in `.devbox/virtenv/android`, never in `~/.android` +- **Project-local Android environment**: All Android user data (AVDs, emulator configs, adb keys) is stored in `.devbox/virtenv/`, never in `~/.android` - **Reproducible SDK management**: Android SDK composed via Nix flake, ensuring consistent tooling across machines - **Device management**: JSON-based device definitions with CLI commands for creating, updating, and managing AVDs -- **Development scripts**: Runtime scripts for building, running, and debugging Android apps -- **Testing infrastructure**: E2E test suites with process-compose for automated testing +- **Emulator control**: Scripts for starting, stopping, and resetting emulators + +The plugin does **not** provide build or deploy commands. Every project has different build tooling (Gradle, Bazel, custom scripts), so you define those in your own `devbox.json`. See [Adding Build Scripts](#adding-build-and-deploy-scripts) for patterns. Pure shells with `devbox run --pure` guarantee isolated, reproducible execution without side effects. @@ -19,8 +20,8 @@ Pure shells with `devbox run --pure` guarantee isolated, reproducible execution ### Prerequisites - [Devbox](https://www.jetify.com/devbox/docs/installing_devbox/) installed -- Java Development Kit (JDK) - typically JDK 17 or later -- Gradle (for building Android projects) + +Devbox handles downloading JDK, Gradle, and all other tools โ€” you don't need to install them separately. ### Adding the Plugin to Your Project @@ -32,13 +33,12 @@ Create or modify your `devbox.json` to include the Android plugin: "packages": { "jdk17": "latest", "gradle": "latest" - }, - "env": { - "ANDROID_APP_APK": "app/build/outputs/apk/debug/app-debug.apk" } } ``` +The `include` line adds the plugin. The `packages` section adds your build tooling. APK paths are auto-detected at runtime when you deploy. + ### Initial Setup Initialize the Devbox environment: @@ -49,8 +49,8 @@ devbox shell This command: 1. Downloads and installs the Android SDK via Nix -2. Creates device definitions in `devbox.d/android/devices/` -3. Sets up runtime scripts in `.devbox/virtenv/android/scripts/` +2. Creates device definitions in your devbox.d directory +3. Sets up runtime scripts 4. Configures environment variables for Android development Verify the installation: @@ -63,14 +63,16 @@ This displays resolved SDK information including SDK root, build tools version, ## Device Management -Device definitions are JSON files stored in `devbox.d/android/devices/`. Each device specifies an Android Virtual Device (AVD) configuration. +Device definitions are JSON files that describe AVD configurations. Each plugin install creates a `devices/` directory inside your `devbox.d/` folder with default device files. ### Default Devices The plugin includes two default devices: -- `min.json` - Minimum supported Android version (API 21) -- `max.json` - Latest Android version (currently API 36) +- `min.json` - Minimum supported Android version (API 21, named `pixel_api21`) +- `max.json` - Latest Android version (API 36, named `medium_phone_api36`) + +These files live in your `devbox.d/` directory, which is the devbox plugin configuration folder. The plugin creates a subdirectory there with a `devices/` folder containing them (e.g., `devbox.d//devices/min.json`). The filenames (`min`, `max`) are short nicknames you use in commands. The `name` field inside each JSON file is the full AVD name that appears in device listings. ### Listing Devices @@ -91,14 +93,14 @@ devbox run android.sh devices create pixel_api28 \ --api 28 \ --device pixel \ --tag google_apis \ - --preferred_abi x86_64 + --abi x86_64 ``` Parameters: - `--api` (required): Android API level - `--device` (required): AVD device profile (e.g., `pixel`, `pixel_xl`, `Nexus 5`) - `--tag`: System image tag (`google_apis`, `google_apis_playstore`, `play_store`, `aosp_atd`, `google_atd`) -- `--preferred_abi`: Preferred ABI architecture (`x86_64`, `arm64-v8a`, `x86`) +- `--abi`: Preferred ABI architecture (`x86_64`, `arm64-v8a`, `x86`) ### Viewing Device Details @@ -153,7 +155,7 @@ After creating, updating, or deleting devices, regenerate the lock file: devbox run android.sh devices eval ``` -The lock file (`devbox.d/android/devices.lock`) optimizes CI builds by limiting which SDK versions are downloaded. Commit this file to version control. +The lock file (in your devices directory) optimizes CI builds by limiting which SDK versions are downloaded. Commit this file to version control. ### Syncing AVDs @@ -167,29 +169,6 @@ This creates or updates AVDs to match your JSON device definitions. Run this aft ## Development Workflow -### Building Your App - -Build the Android app using Gradle: - -```bash -# Standard build with info logging -devbox run build - -# Build with full debug output -gradle assembleDebug --debug - -# Clean build artifacts -gradle clean -``` - -Or use Gradle directly: - -```bash -devbox run gradle assembleDebug --info -``` - -The plugin sources the Android environment automatically, setting `ANDROID_SDK_ROOT` and adding SDK tools to PATH. - ### Starting an Emulator Start an Android emulator for testing: @@ -198,7 +177,7 @@ Start an Android emulator for testing: # Start default device devbox run start:emu -# Start specific device +# Start specific device by nickname devbox run start:emu pixel_api28 # Start with clean state (wipe data) @@ -217,50 +196,85 @@ Set the default device in `devbox.json`: } ``` -### Running Your App +### Stopping the Emulator -Build, install, and launch your app on the emulator: +Stop all running emulators: ```bash -# Build and run on default device -devbox run start:app +devbox run stop:emu +``` -# Build and run on specific device -devbox run start:app pixel_api28 +### Resetting Emulator State -# Install pre-built APK without building -devbox run start:app path/to/app.apk +Reset AVD state (clears all data and app installations): + +```bash +devbox run android.sh emulator reset ``` -The `run` command: -1. Builds the app (if no APK path provided) -2. Waits for emulator to be fully booted -3. Installs the APK via adb -4. Launches the app +This is useful after Nix package updates or when you need a clean slate. -### Stopping the Emulator +### Adding Build and Deploy Scripts -Stop all running emulators: +The plugin provides emulator and device management. Build and deploy commands are project-specific, so you define them in your `devbox.json`. Here's a typical setup: -```bash -devbox run stop:emu +```json +{ + "include": ["github:segment-integrations/devbox-plugins?dir=plugins/android"], + "packages": { + "jdk17": "latest", + "gradle": "latest" + }, + "shell": { + "scripts": { + "build:android": [ + "gradle assembleDebug" + ], + "build:release": [ + "gradle assembleRelease" + ], + "start:app": [ + "android.sh run ${1:-}" + ] + } + } +} ``` -Or stop the emulator for a specific device: +The `${1:-}` syntax passes an optional argument through to the command โ€” it means "use the first argument if provided, otherwise use nothing." This lets you run `devbox run start:app` (default device) or `devbox run start:app min` (specific device). + +With these scripts defined, you can: ```bash -devbox run android.sh emulator stop +# Build the APK +devbox run build:android + +# Build, install, and launch on the emulator +devbox run start:app + +# Run on a specific device +devbox run start:app min ``` -### Resetting Emulator State +**How APK auto-detection works:** The `android.sh run` command waits for the emulator to boot, then auto-detects the APK using this precedence chain: -Reset AVD state (clears all data and app installations): +1. `ANDROID_APP_APK` env var โ€” if set, resolves the path/glob relative to project root +2. Recursive search of the project directory for `.apk` files, skipping `.gradle/`, `build/intermediates/`, `node_modules/`, and `.devbox/` +3. Recursive search of the current working directory (if different from project root) -```bash -devbox run android.sh emulator reset +The app's package name and launch activity are extracted from the APK automatically. + +In most projects, step 2 finds the right APK with no configuration. If auto-detection picks the wrong APK (e.g., multiple build variants), set `ANDROID_APP_APK` explicitly: + +```json +{ + "env": { + "ANDROID_APP_APK": "app/build/outputs/apk/debug/app-debug.apk" + } +} ``` -This is useful after Nix package updates or when you need a clean slate. +See the [Android example project](../../examples/android/) for a complete working setup. The example project uses a local plugin path for development. If you use it as a template, change the `include` to the GitHub URL shown above. ### Complete Development Workflow Example @@ -273,7 +287,7 @@ devbox shell # 2. Start emulator devbox run start:emu max -# 3. Build and run app +# 3. Build and run app (using your custom scripts) devbox run build devbox run start:app max @@ -285,33 +299,30 @@ devbox run start:app max devbox run stop:emu ``` -For a streamlined workflow, use the combined command: - -```bash -# Build, install, and launch in one command -devbox run start:app -``` - -This starts the emulator, builds the app, and deploys it automatically. - ## Testing ### Running E2E Tests -The plugin includes E2E test infrastructure using process-compose. Example projects include test suites that: - -1. Build the app -2. Sync AVDs with device definitions -3. Start the emulator -4. Deploy and launch the app -5. Verify the app is running -6. Clean up (stop app and emulator in pure mode) +The [Android example project](../../examples/android/) includes E2E test infrastructure using process-compose. You can use it as a template for your own project. -Run the complete E2E test: +Copy the example test suite: ```bash -cd examples/android -devbox run test:e2e +cp -r examples/android/tests/ your-project/tests/ +``` + +Add a test script to your `devbox.json`: + +```json +{ + "shell": { + "scripts": { + "test:e2e": [ + "process-compose -f tests/test-suite.yaml --no-server --tui=${TEST_TUI:-false}" + ] + } + } +} ``` ### Deterministic Testing @@ -337,32 +348,6 @@ TEST_TUI=true devbox run test:e2e The TUI shows process status, logs, and resource usage during test execution. -### Adding Tests to Your Project - -Copy the example test suite: - -```bash -cp -r examples/android/tests/ your-project/tests/ -``` - -Configure for your app in `devbox.json`: - -```json -{ - "env": { - "ANDROID_APP_APK": "app/build/outputs/apk/debug/app-debug.apk", - "ANDROID_APP_ID": "com.mycompany.myapp" - }, - "shell": { - "scripts": { - "test:e2e": [ - "process-compose -f tests/test-suite.yaml --no-server --tui=${TEST_TUI:-false}" - ] - } - } -} -``` - ### Test Configuration Customize test behavior with environment variables: @@ -388,8 +373,6 @@ Configure the plugin by setting environment variables in `devbox.json`: { "env": { "ANDROID_DEFAULT_DEVICE": "max", - "ANDROID_APP_APK": "app/build/outputs/apk/debug/app-debug.apk", - "ANDROID_APP_ID": "com.example.myapp", "ANDROID_BUILD_TOOLS_VERSION": "36.1.0", "ANDROID_COMPILE_SDK": "36", "ANDROID_TARGET_SDK": "36" @@ -399,8 +382,7 @@ Configure the plugin by setting environment variables in `devbox.json`: Key variables: - `ANDROID_DEFAULT_DEVICE` - Default device when none specified -- `ANDROID_APP_APK` - Path or glob pattern for APK -- `ANDROID_APP_ID` - Android package name +- `ANDROID_APP_APK` - Path or glob pattern for APK (empty = auto-detect) - `ANDROID_BUILD_TOOLS_VERSION` - Build tools version - `ANDROID_COMPILE_SDK` - Compile SDK version - `ANDROID_TARGET_SDK` - Target SDK version @@ -439,12 +421,6 @@ Set `ANDROID_SDK_ROOT` to your local SDK path. Limit which devices are evaluated to reduce initialization time: -```bash -devbox run android.sh devices select min max -``` - -Or set in `devbox.json`: - ```json { "env": { @@ -482,17 +458,6 @@ Display all configuration settings: devbox run android.sh config show ``` -Update configuration by editing your `devbox.json` env section: - -```json -{ - "env": { - "ANDROID_DEFAULT_DEVICE": "pixel_api28", - "ANDROID_DEVICES": "min,max" - } -} -``` - Run `devbox shell` after changing `devbox.json` to apply the new values. To reset to defaults, remove the overrides from your `devbox.json`. ## Troubleshooting @@ -515,7 +480,7 @@ Run `devbox shell` after changing `devbox.json` to apply the new values. To rese 3. Reset emulator state: ```bash - devbox run android.sh emulator reset-device max + devbox run android.sh emulator reset ``` 4. Increase boot timeout: @@ -529,10 +494,9 @@ Run `devbox shell` after changing `devbox.json` to apply the new values. To rese **Solutions**: -1. Verify APK path is correct: +1. Verify the APK exists (check your build output directory): ```bash - echo $ANDROID_APP_APK - ls -l $ANDROID_APP_APK + find . -name '*.apk' -not -path '*/.gradle/*' -not -path '*/intermediates/*' ``` 2. Check if app is already installed: @@ -556,9 +520,10 @@ Run `devbox shell` after changing `devbox.json` to apply the new values. To rese **Solutions**: -1. Source the Android environment: +1. Re-enter devbox shell to reload the environment: ```bash - . ${ANDROID_RUNTIME_DIR}/scripts/init/setup.sh + exit + devbox shell ``` 2. Verify SDK installation: @@ -566,11 +531,6 @@ Run `devbox shell` after changing `devbox.json` to apply the new values. To rese devbox run android.sh info ``` -3. Regenerate the environment: - ```bash - devbox run devbox_sync - ``` - ### Lock File Checksum Mismatch **Symptom**: Warning about lock file checksum not matching device definitions. @@ -582,7 +542,7 @@ Regenerate the lock file: devbox run android.sh devices eval ``` -Commit the updated `devices.lock` file. +Commit the updated lock file. ### Multiple Emulators Conflict @@ -601,11 +561,6 @@ Commit the updated `devices.lock` file. EMU_PORT=5556 devbox run start:emu device2 ``` -3. Use device serials explicitly: - ```bash - ANDROID_SERIAL=emulator-5554 devbox run start:app - ``` - ### Build Fails with SDK Version Errors **Symptom**: Gradle build fails with "SDK Build Tools version X not found". @@ -645,9 +600,6 @@ ANDROID_DEBUG=1 devbox shell # Global debug DEBUG=1 devbox shell - -# Debug during tests -ANDROID_DEBUG=1 devbox run test:e2e ``` Debug logs show: @@ -677,9 +629,13 @@ Test your app across multiple Android versions: 3. Test on each device: ```bash - devbox run start:app api21 - devbox run start:app api28 - devbox run start:app api36 + devbox run start:emu api21 + # run your tests... + devbox run stop:emu + + devbox run start:emu api36 + # run your tests... + devbox run stop:emu ``` ### CI/CD Integration @@ -744,11 +700,11 @@ devbox run android.sh devices create minimal --api 34 --device pixel --tag defau - **[Device Management Guide](device-management.md)**: Deep dive into device definitions and management - **[Testing Guide](testing.md)**: Comprehensive testing strategies and best practices - **[Troubleshooting Guide](troubleshooting.md)**: Extended troubleshooting scenarios -- **[Quick Start](quick-start.md)**: Get up and running in 5 minutes +- **[Quick Start](quick-start.md)**: Get up and running quickly ### Example Projects -- **[Android Example](../../examples/android/)**: Minimal Android app demonstrating plugin usage +- **[Android Example](../../examples/android/)**: Complete Android app with build scripts, deploy commands, and E2E test suites - **[React Native Example](../../examples/react-native/)**: Cross-platform app using both Android and iOS plugins ### Community and Support diff --git a/wiki/guides/cheatsheets/android.md b/wiki/guides/cheatsheets/android.md index 5ed9ca5..c2be800 100644 --- a/wiki/guides/cheatsheets/android.md +++ b/wiki/guides/cheatsheets/android.md @@ -55,15 +55,16 @@ devbox run stop:emu ## Build and Deploy -```bash -# Build app -devbox run build +Build and deploy scripts are project-specific. Define them in your `devbox.json`. -# Build, install, and launch app -devbox run start:app +```bash +# Plugin-provided: run app on emulator (builds, installs, launches) +devbox run android.sh run +devbox run android.sh run pixel_api30 -# Build and run on specific device -devbox run start:app pixel_api30 +# User-defined scripts (add to your devbox.json shell.scripts): +# "build": ["gradle assembleDebug --info"] +# "start:app": ["android.sh run ${1:-}"] # ${1:-} passes optional device arg ``` ## Configuration @@ -130,12 +131,12 @@ adb emu kill ## Testing -```bash -# Run fast tests -devbox run test:fast +Test scripts are project-specific. Define them in your `devbox.json`. -# Run E2E tests -devbox run test:e2e +```bash +# User-defined scripts (add to your devbox.json shell.scripts): +# "test": ["gradle test"] +# "test:e2e": ["process-compose -f tests/test-suite.yaml --no-server"] # Run with headless emulator EMU_HEADLESS=1 devbox run test:e2e @@ -145,14 +146,14 @@ EMU_HEADLESS=1 devbox run test:e2e ``` devbox.d/android/ -โ”œโ”€โ”€ devices/ # Device definitions -โ”‚ โ”œโ”€โ”€ min.json -โ”‚ โ”œโ”€โ”€ max.json -โ”‚ โ””โ”€โ”€ devices.lock # Generated lock file -โ””โ”€โ”€ flake.nix # Nix SDK configuration - -.devbox/virtenv/android/ # Runtime directory (auto-regenerated) -โ””โ”€โ”€ scripts/ # Plugin scripts +โ””โ”€โ”€ devices/ # Device definitions + โ”œโ”€โ”€ min.json + โ”œโ”€โ”€ max.json + โ””โ”€โ”€ devices.lock # Generated lock file + +.devbox/virtenv/android/ # Runtime directory (auto-regenerated, never edit) +โ”œโ”€โ”€ scripts/ # Plugin scripts +โ””โ”€โ”€ flake.nix # Nix SDK configuration (auto-generated) reports/ โ”œโ”€โ”€ logs/ # Test logs diff --git a/wiki/guides/cheatsheets/ios.md b/wiki/guides/cheatsheets/ios.md index 9881c35..f730c30 100644 --- a/wiki/guides/cheatsheets/ios.md +++ b/wiki/guides/cheatsheets/ios.md @@ -56,14 +56,25 @@ devbox run stop:sim ## Build and Deploy ```bash -# Build app -devbox run build +# Build (define build:ios in devbox.json using xcodebuild) +devbox run build:ios -# Build, install, and launch app -devbox run start:ios +# Run app (starts simulator, builds, installs, launches) +ios.sh run -# Build and run on specific device -devbox run start:ios iphone15 +# Run on specific device +ios.sh run max + +# Run pre-built app +ios.sh run /path/to/MyApp.app +``` + +Build scripts in `devbox.json`: + +```bash +# "build:ios": ["ios.sh xcodebuild -scheme MyApp -configuration Debug -destination 'generic/platform=iOS Simulator' build"] +# "build:release": ["ios.sh xcodebuild -scheme MyApp -configuration Release build"] +# "start:app": ["ios.sh run ${1:-}"] ``` ## Configuration @@ -95,9 +106,10 @@ IOS_DEFAULT_RUNTIME="" # Default runtime (empty = latest) IOS_DEVELOPER_DIR="" # Xcode path (empty = auto-detect) IOS_DOWNLOAD_RUNTIME="1" # Auto-download runtimes (1=yes, 0=no) IOS_SKIP_SETUP="0" # Skip setup during init (1=yes, 0=no) -IOS_APP_PROJECT="ios.xcodeproj" # Xcode project path -IOS_APP_SCHEME="ios" # Build scheme -IOS_APP_BUNDLE_ID="com.example.ios" # App bundle ID +IOS_APP_ARTIFACT="" # .app path/glob (empty = auto-detect) +IOS_APP_SCHEME="" # Xcode scheme (empty = auto-detect) +IOS_BUILD_CONFIG="Debug" # Build configuration (Debug/Release) +IOS_DERIVED_DATA_PATH="" # DerivedData path (default: .devbox/virtenv/ios/DerivedData) ``` ## Device Definition Format @@ -134,12 +146,12 @@ launchctl kickstart -k gui/$UID/com.apple.CoreSimulatorService ## Testing -```bash -# Run fast tests -devbox run test:fast +Test scripts are project-specific. Define them in your `devbox.json`. -# Run E2E tests -devbox run test:e2e +```bash +# User-defined scripts (add to your devbox.json shell.scripts): +# "test": ["xcodebuild ... test"] +# "test:e2e": ["process-compose -f tests/test-suite.yaml --no-server"] ``` ## Files and Directories @@ -151,9 +163,9 @@ devbox.d/ios/ โ”œโ”€โ”€ max.json โ””โ”€โ”€ devices.lock # Generated lock file -.devbox/virtenv/ios/ # Runtime directory (auto-regenerated) +.devbox/virtenv/ios/ # Runtime directory (auto-regenerated, never edit) โ”œโ”€โ”€ scripts/ # Plugin scripts -โ””โ”€โ”€ DerivedData/ # Build output +โ””โ”€โ”€ DerivedData/ # Build output (if configured) reports/ โ”œโ”€โ”€ logs/ # Test logs @@ -163,10 +175,13 @@ reports/ ## Common Xcode Commands ```bash -# Build project +# Build project (xcodebuild works natively, Nix vars stripped at init) xcodebuild -project ios.xcodeproj -scheme ios -configuration Debug \ -destination 'generic/platform=iOS Simulator' build +# Or use ios.sh xcodebuild wrapper if Nix vars cause issues +ios.sh xcodebuild -project ios.xcodeproj -scheme ios build + # Install app to simulator xcrun simctl install booted path/to/app.app diff --git a/wiki/guides/cheatsheets/react-native.md b/wiki/guides/cheatsheets/react-native.md index e5b3f20..e5399f0 100644 --- a/wiki/guides/cheatsheets/react-native.md +++ b/wiki/guides/cheatsheets/react-native.md @@ -41,62 +41,61 @@ devbox run ios.sh devices eval ## Running Apps ```bash -# Android -devbox run start:emu # Start emulator -devbox run start:android # Build and run app +# Plugin-provided +devbox run start:emu # Start Android emulator +devbox run stop:emu # Stop Android emulator +devbox run start:sim # Start iOS simulator +devbox run stop:sim # Stop iOS simulator -# iOS -devbox run start:sim # Start simulator -devbox run start:ios # Build and run app - -# Web -devbox run start:web # Start web dev server +# User-defined scripts (add to your devbox.json shell.scripts): +# "start:android": ["process-compose -f tests/dev-android.yaml"] +# "start:ios": ["process-compose -f tests/dev-ios.yaml"] +# "start:web": ["process-compose -f tests/dev-web.yaml"] ``` ## Metro Bundler ```bash -# Start Metro manually -devbox run start:metro +# Plugin-provided +devbox run rn:metro:port android # Get Metro port for test suite +devbox run rn:metro:clean android # Clean Metro for test suite + +# User-defined scripts (add to your devbox.json shell.scripts): +# "start:metro": ["metro.sh start ${1:-default}"] +# "stop:metro": ["metro.sh stop ${1:-default}"] # Metro with custom port RN_METRO_PORT=8091 devbox run start:metro - -# Clean Metro cache -devbox run clean-metro - -# Get Metro port for test suite -devbox run rn:metro:port android - -# Clean Metro for test suite -devbox run rn:metro:clean android ``` ## Building -```bash -# Build all platforms -devbox run build +Build scripts are project-specific. Define them in your `devbox.json`. -# Build specific platform -devbox run build:android -devbox run build:ios -devbox run build:web +```bash +# User-defined scripts (add to your devbox.json shell.scripts): +# "build": ["devbox run build:android", "devbox run build:ios", "devbox run build:web"] +# "build:android": ["cd android && gradle assembleDebug"] +# "build:ios": ["cd ios && pod install && xcodebuild ..."] +# "build:web": ["npx react-native-web build"] ``` ## Testing +Test scripts are project-specific. Define them in your `devbox.json`. + ```bash -# Run all tests -devbox run test:fast +# Plugin-provided +devbox run test:metro # Test Metro bundler setup -# Platform-specific E2E tests -devbox run test:e2e:android -devbox run test:e2e:ios -devbox run test:e2e:web +# User-defined scripts (add to your devbox.json shell.scripts): +# "test": ["npm test"] +# "test:e2e:android": ["process-compose -f tests/test-suite-android-e2e.yaml --no-server --tui=${TEST_TUI:-false}"] +# "test:e2e:ios": ["process-compose -f tests/test-suite-ios-e2e.yaml --no-server --tui=${TEST_TUI:-false}"] -# With TUI (terminal UI) -TEST_TUI=true devbox run test:e2e:android +# Skip unused platform for faster startup +devbox run --pure -e IOS_SKIP_SETUP=1 test:e2e:android +devbox run --pure -e ANDROID_SKIP_SETUP=1 test:e2e:ios ``` ## Configuration @@ -110,12 +109,10 @@ devbox run ios.sh config show ## Diagnostics ```bash -# Health checks -devbox run doctor -devbox run verify:setup - -# Check Metro -devbox run test:metro +# Plugin-provided +devbox run doctor # Health check +devbox run verify:setup # Quick verification +devbox run test:metro # Test Metro setup ``` ## Common Environment Variables @@ -154,8 +151,8 @@ RN_METRO_PORT_END="8199" # Enable debug logging ANDROID_DEBUG=1 IOS_DEBUG=1 devbox shell -# Reset Metro -devbox run clean-metro +# Reset Metro cache +devbox run rn:metro:clean android rm -rf node_modules/.cache npm start -- --reset-cache @@ -178,30 +175,27 @@ rm -rf .devbox/virtenv/ios/DerivedData ## Development Workflow +The commands below assume you have defined `build:android`, `build:ios`, `start:android`, and `start:ios` in your `devbox.json`. + ```bash # Full Android workflow -devbox run start:emu +devbox run start:emu # Plugin-provided npm install -devbox run build:android -devbox run start:android +devbox run build:android # User-defined +devbox run start:android # User-defined # Full iOS workflow -devbox run start:sim +devbox run start:sim # Plugin-provided npm install cd ios && pod install && cd .. -devbox run build:ios -devbox run start:ios - -# Development with hot reload -devbox run start:metro & -devbox run start:android # Android -devbox run start:ios # iOS +devbox run build:ios # User-defined +devbox run start:ios # User-defined ``` ## Process Isolation ```bash -# Run multiple test suites in parallel +# Run multiple test suites in parallel (user-defined test scripts) TEST_SUITE_NAME=android devbox run --pure test:e2e:android & TEST_SUITE_NAME=ios devbox run --pure test:e2e:ios & @@ -222,7 +216,7 @@ devbox.d/ โ”œโ”€โ”€ max.json โ””โ”€โ”€ devices.lock -.devbox/virtenv/ +.devbox/virtenv/ # Runtime directory (auto-regenerated, never edit) โ”œโ”€โ”€ android/ # Android runtime โ”œโ”€โ”€ ios/ # iOS runtime โ””โ”€โ”€ metro/ # Metro state diff --git a/wiki/guides/device-management.md b/wiki/guides/device-management.md index 28d6d06..c91ace1 100644 --- a/wiki/guides/device-management.md +++ b/wiki/guides/device-management.md @@ -17,7 +17,7 @@ Device management in Devbox plugins follows a declarative approach. You define d ### Android Device Definitions -Android device definitions specify AVD (Android Virtual Device) configurations. Each file in `devbox.d/android/devices/*.json` defines one device. +Android device definitions specify AVD (Android Virtual Device) configurations. Each JSON file in your devices directory defines one device. **Schema:** ```json @@ -74,7 +74,7 @@ Android device definitions specify AVD (Android Virtual Device) configurations. ### iOS Device Definitions -iOS device definitions specify simulator configurations. Each file in `devbox.d/ios/devices/*.json` defines one simulator. +iOS device definitions specify simulator configurations. Each JSON file in your devices directory defines one simulator. **Schema:** ```json @@ -127,16 +127,14 @@ Use `min.json` and `max.json` as standard device names for testing minimum and m - Standardized across projects **Example directory structure:** -``` -devbox.d/android/devices/ -โ”œโ”€โ”€ min.json # API 21 - minimum supported -โ”œโ”€โ”€ max.json # API 36 - latest available -โ””โ”€โ”€ pixel_api28.json # Custom device for specific testing -devbox.d/ios/devices/ -โ”œโ”€โ”€ min.json # iOS 15.4 - minimum supported -โ”œโ”€โ”€ max.json # iOS 26.2 - latest available -โ””โ”€โ”€ ipad_pro.json # Custom device for tablet testing +The exact path of the devices directory depends on how the plugin is included in your project (local path vs GitHub reference). Inside your `devbox.d` folder, you will have a platform directory containing a `devices/` subdirectory: + +``` +/ +โ”œโ”€โ”€ min.json # Minimum supported version (e.g., API 21 or iOS 15.4) +โ”œโ”€โ”€ max.json # Latest available version (e.g., API 36 or iOS 26.2) +โ””โ”€โ”€ custom.json # Custom device for specific testing ``` **CI usage:** @@ -368,16 +366,16 @@ Lock files optimize CI builds by limiting which SDK versions and system images a Without lock files: - Nix evaluates all possible device configurations - Downloads system images for all API levels -- Slow CI builds (can add 10+ minutes) +- Slower CI builds With lock files: - Only evaluates specified devices - Downloads only required system images -- Fast CI builds (evaluates in seconds) +- Faster CI builds ### Android Lock File -**Location:** `devbox.d/android/devices/devices.lock` +**Location:** `devices.lock` inside your Android devices directory **Format:** ```json @@ -396,19 +394,17 @@ With lock files: "tag": "google_apis" } ], - "checksum": "2f3ab0e3cefd3e9909185c0717dc9d63038da1e81625eb6fce585e3af446bfef", - "generated_at": "2026-02-12T06:54:22Z" + "checksum": "2f3ab0e3cefd3e9909185c0717dc9d63038da1e81625eb6fce585e3af446bfef" } ``` **Fields:** - `devices` - Array of device configurations to evaluate - `checksum` - SHA-256 hash of all device definition files -- `generated_at` - ISO 8601 timestamp ### iOS Lock File -**Location:** `devbox.d/ios/devices/devices.lock` +**Location:** `devices.lock` inside your iOS devices directory **Format:** ```json @@ -423,8 +419,7 @@ With lock files: "runtime": "15.4" } ], - "checksum": "dd575d31a5adf2f471655389df301215f6ef7130ca284d433929b08b68e42890", - "generated_at": "2026-02-12T06:55:59Z" + "checksum": "dd575d31a5adf2f471655389df301215f6ef7130ca284d433929b08b68e42890" } ``` @@ -582,7 +577,8 @@ set -euo pipefail for device in min max; do echo "Testing on $device" devbox run android.sh emulator start "$device" - devbox run test:android + # Run your project-specific test command here + # e.g., devbox run ./gradlew connectedAndroidTest devbox run android.sh emulator stop done ``` @@ -595,7 +591,8 @@ set -euo pipefail for device in min max; do echo "Testing on $device" devbox run ios.sh simulator start "$device" - devbox run test:ios + # Run your project-specific test command here + # e.g., devbox run xcodebuild test -scheme MyApp devbox run ios.sh simulator stop done ``` @@ -612,7 +609,7 @@ processes: test-android-min: command: | devbox run android.sh emulator start min - devbox run test:android + # Run your project-specific test command here depends_on: setup: condition: process_completed @@ -620,7 +617,7 @@ processes: test-android-max: command: | devbox run android.sh emulator start max - devbox run test:android + # Run your project-specific test command here depends_on: setup: condition: process_completed @@ -628,7 +625,7 @@ processes: test-ios-min: command: | devbox run ios.sh simulator start min - devbox run test:ios + # Run your project-specific test command here depends_on: setup: condition: process_completed @@ -636,7 +633,7 @@ processes: test-ios-max: command: | devbox run ios.sh simulator start max - devbox run test:ios + # Run your project-specific test command here depends_on: setup: condition: process_completed @@ -667,27 +664,24 @@ Override device selection for individual test runs. **Android:** ```bash -# Use default device -devbox run test:android +# Start emulator with default device +devbox run android.sh emulator start -# Override for specific device -ANDROID_DEVICE_NAME=pixel_api28 devbox run test:android - -# Legacy override variable -TARGET_DEVICE=pixel_api28 devbox run test:android +# Start emulator with specific device +devbox run android.sh emulator start pixel_api28 ``` **iOS:** ```bash -# Use default device -devbox run test:ios - -# Override for specific device (via command argument) -devbox run test:ios iphone15 +# Start simulator with default device +devbox run ios.sh simulator start -# Or set device in test suite configuration +# Start simulator with specific device +devbox run ios.sh simulator start iphone15 ``` +Build, test, and deploy commands are project-specific and must be defined by you in your `devbox.json` scripts section. + ### Matrix Testing Test all combinations of devices and configurations. @@ -705,7 +699,8 @@ for device in "${ANDROID_DEVICES[@]}"; do for config in "${CONFIGS[@]}"; do echo "Testing Android $device with $config" devbox run android.sh emulator start "$device" - devbox run test:android:$config + # Run your project-specific test command here, e.g.: + # devbox run ./gradlew connectedAndroidTest -PbuildType="$config" devbox run android.sh emulator stop done done @@ -714,7 +709,8 @@ for device in "${IOS_DEVICES[@]}"; do for config in "${CONFIGS[@]}"; do echo "Testing iOS $device with $config" devbox run ios.sh simulator start "$device" - devbox run test:ios:$config + # Run your project-specific test command here, e.g.: + # devbox run xcodebuild test -scheme MyApp -configuration "$config" devbox run ios.sh simulator stop done done @@ -744,15 +740,15 @@ Specify a default device used when no device is explicitly provided. } ``` -**Usage:** +**Usage with plugin-provided commands:** ```bash -# Uses default device -devbox run start:app -devbox run start:ios +# Start emulator/simulator with default device +devbox run android.sh emulator start +devbox run ios.sh simulator start -# Override with specific device -devbox run start:app min -devbox run start:ios min +# Start emulator/simulator with specific device +devbox run android.sh emulator start min +devbox run ios.sh simulator start min ``` ### CI Device Selection @@ -783,7 +779,7 @@ jobs: - uses: actions/checkout@v3 - run: devbox install - run: devbox run android.sh emulator start ${{ matrix.device }} - - run: devbox run test:android + - run: devbox run your-android-test-script # Define in devbox.json scripts - run: devbox run android.sh emulator stop test-ios: @@ -795,7 +791,7 @@ jobs: - uses: actions/checkout@v3 - run: devbox install - run: devbox run ios.sh simulator start ${{ matrix.device }} - - run: devbox run test:ios + - run: devbox run your-ios-test-script # Define in devbox.json scripts - run: devbox run ios.sh simulator stop ``` @@ -877,8 +873,8 @@ Use clear, descriptive names for devices. Commit device definitions and lock files. **What to commit:** -- `devbox.d/*/devices/*.json` - Device definitions -- `devbox.d/*/devices/devices.lock` - Lock files +- `*.json` files in your devices directories - Device definitions +- `devices.lock` files in your devices directories - Lock files **What to ignore:** - `.devbox/virtenv/` - Generated runtime files @@ -914,8 +910,8 @@ devbox run android.sh devices create pixel_api35 \ # Regenerate lock file devbox run android.sh devices eval -# Commit both -git add devbox.d/android/devices/ +# Commit both the device definition and updated lock file +git add / git commit -m "feat(android): add pixel_api35 device" ``` @@ -937,8 +933,8 @@ devbox run android.sh devices update max --api 36 # Regenerate lock devbox run android.sh devices eval -# Test compatibility -devbox run test:android max +# Test compatibility by starting the emulator with the updated device +devbox run android.sh emulator start max ``` ### Performance Optimization @@ -980,7 +976,7 @@ Execution continues. Regenerate lock file when convenient. **Missing device definition (error):** ``` -[ERROR] Device 'pixel_api30' not found in devbox.d/android/devices/ +[ERROR] Device 'pixel_api30' not found in devices directory Available devices: min, max ``` Execution stops. Create device or use an available device name. @@ -1011,18 +1007,19 @@ For React Native or hybrid apps, manage both Android and iOS devices. ``` **Directory structure:** + +The exact layout of your `devbox.d` directory depends on how the plugin is included (local path vs GitHub reference). Each platform will have its own devices directory containing your device definitions and lock file: + ``` -devbox.d/ -โ”œโ”€โ”€ android/ -โ”‚ โ””โ”€โ”€ devices/ -โ”‚ โ”œโ”€โ”€ min.json -โ”‚ โ”œโ”€โ”€ max.json -โ”‚ โ””โ”€โ”€ devices.lock -โ””โ”€โ”€ ios/ - โ””โ”€โ”€ devices/ - โ”œโ”€โ”€ min.json - โ”œโ”€โ”€ max.json - โ””โ”€โ”€ devices.lock +/ +โ”œโ”€โ”€ min.json +โ”œโ”€โ”€ max.json +โ””โ”€โ”€ devices.lock + +/ +โ”œโ”€โ”€ min.json +โ”œโ”€โ”€ max.json +โ””โ”€โ”€ devices.lock ``` **Testing workflow:** @@ -1030,14 +1027,14 @@ devbox.d/ # Android testing devbox run android.sh devices list devbox run android.sh devices eval -devbox run start:app min -devbox run start:app max +devbox run android.sh emulator start min +devbox run android.sh emulator start max # iOS testing devbox run ios.sh devices list devbox run ios.sh devices eval -devbox run start:ios min -devbox run start:ios max +devbox run ios.sh simulator start min +devbox run ios.sh simulator start max ``` ## Troubleshooting @@ -1051,8 +1048,8 @@ devbox run start:ios max # List available devices devbox run {platform}.sh devices list -# Check file exists -ls devbox.d/{platform}/devices/ +# Check file exists in your devices directory +ls / # Ensure filename matches (case-sensitive) # device-name.json should match exactly @@ -1068,9 +1065,8 @@ ls devbox.d/{platform}/devices/ devbox run android.sh devices eval devbox run ios.sh devices eval -# Commit updated lock file -git add devbox.d/*/devices/devices.lock -git commit -m "chore: update device lock files" +# Commit updated lock files from your devices directories +git commit -am "chore: update device lock files" ``` ### Android System Image Not Found diff --git a/wiki/guides/ios-guide.md b/wiki/guides/ios-guide.md index 506b6ac..cf8ce2c 100644 --- a/wiki/guides/ios-guide.md +++ b/wiki/guides/ios-guide.md @@ -6,11 +6,13 @@ A complete guide to iOS development using the Devbox iOS plugin. This guide cove The iOS plugin enables reproducible iOS development by automatically discovering Xcode and managing iOS simulators project-locally. It provides: -- **Project-local simulator management**: Device definitions stored in `devbox.d/ios/devices/`, isolated from system-wide simulator configuration +- **Project-local simulator management**: Device definitions stored in your devbox.d directory, isolated from system-wide simulator configuration - **Automatic Xcode discovery**: Multi-strategy detection with caching for fast shell initialization - **Device management**: JSON-based device definitions with CLI commands for creating, updating, and managing simulators -- **Development scripts**: Runtime scripts for building, running, and debugging iOS apps -- **Testing infrastructure**: E2E test suites with process-compose for automated testing +- **Simulator control**: Scripts for starting and stopping simulators +- **App deployment**: `ios.sh run` auto-detects your `.app` bundle, extracts the bundle ID, and deploys to the simulator + +The plugin auto-detects your Xcode project, `.app` path, and bundle ID at runtime. You define a `build:ios` script in your `devbox.json` to handle the actual Xcode build, then `ios.sh run` handles everything else. See [Adding Build Scripts](#adding-build-and-deploy-scripts) for patterns. Pure shells with `devbox run --pure` create test-specific simulators and clean up after execution, ensuring isolated, reproducible testing. @@ -18,25 +20,23 @@ Pure shells with `devbox run --pure` create test-specific simulators and clean u ### Prerequisites -- macOS (iOS development requires macOS) -- [Xcode](https://apps.apple.com/app/xcode/id497799835) installed from the App Store, or Xcode Command Line Tools +- macOS with [Xcode](https://apps.apple.com/app/xcode/id497799835) installed - [Devbox](https://www.jetify.com/devbox/docs/installing_devbox/) installed +Devbox handles downloading other tools โ€” you don't need to install them separately. + ### Adding the Plugin to Your Project Create or modify your `devbox.json` to include the iOS plugin: ```json { - "include": ["github:segment-integrations/devbox-plugins?dir=plugins/ios"], - "env": { - "IOS_APP_PROJECT": "MyApp.xcodeproj", - "IOS_APP_SCHEME": "MyApp", - "IOS_APP_BUNDLE_ID": "com.example.myapp" - } + "include": ["github:segment-integrations/devbox-plugins?dir=plugins/ios"] } ``` +Build output is stored in `.devbox/virtenv/ios/DerivedData` by default (configurable via `IOS_DERIVED_DATA_PATH`). + ### Initial Setup Initialize the Devbox environment: @@ -47,8 +47,8 @@ devbox shell This command: 1. Discovers Xcode installation automatically -2. Creates device definitions in `devbox.d/ios/devices/` -3. Sets up runtime scripts in `.devbox/virtenv/ios/scripts/` +2. Creates device definitions in your devbox.d directory +3. Sets up runtime scripts 4. Configures environment variables for iOS development 5. Caches Xcode path for fast subsequent initialization @@ -70,14 +70,16 @@ This checks for Xcode installation, command-line tools, xcrun and simctl availab ## Device Management -Device definitions are JSON files stored in `devbox.d/ios/devices/`. Each device specifies an iOS simulator configuration. +Device definitions are JSON files that describe simulator configurations. Each plugin install creates a `devices/` directory inside your `devbox.d/` folder with default device files. ### Default Devices The plugin includes two default devices: -- `min.json` - Minimum supported iOS version (iOS 15.4) -- `max.json` - Latest iOS version (currently iOS 26.2) +- `min.json` - Minimum supported iOS version (iOS 15.4, named `iPhone 13`) +- `max.json` - Latest iOS version (iOS 26.2, named `iPhone 17`) + +These files live in your `devbox.d/` directory, which is the devbox plugin configuration folder. The plugin creates a subdirectory there with a `devices/` folder containing them (e.g., `devbox.d//devices/min.json`). The filenames (`min`, `max`) are short nicknames you use in commands. The `name` field inside each JSON file is the simulator display name that appears in device listings. ### Listing Devices @@ -87,7 +89,7 @@ View all available device definitions: devbox run ios.sh devices list ``` -Output shows device names, display names, and iOS runtime versions. +Output shows device names and iOS runtime versions. ### Viewing Available Device Types and Runtimes @@ -177,7 +179,7 @@ After creating, updating, or deleting devices, regenerate the lock file: devbox run ios.sh devices eval ``` -The lock file (`devbox.d/ios/devices/devices.lock`) tracks which devices should be created and includes checksums for validation. Commit this file to version control. +The lock file (in your devices directory) tracks which devices should be created and includes checksums for validation. Commit this file to version control. ### Syncing Simulators @@ -197,24 +199,6 @@ Run this after modifying device files or pulling changes. ## Development Workflow -### Building Your App - -Build the iOS app using Xcode: - -```bash -# Standard build -devbox run build - -# Or build with xcodebuild directly -xcodebuild -project ${IOS_APP_PROJECT} -scheme ${IOS_APP_SCHEME} -configuration Debug \ - -destination 'generic/platform=iOS Simulator' -derivedDataPath DerivedData build -``` - -The plugin automatically configures the build environment, setting `DEVELOPER_DIR` and adding Xcode tools to PATH. Builds are configured for: -- Destination: iOS Simulator -- Configuration: Debug -- Output: `IOS_APP_DERIVED_DATA` (default: `.devbox/virtenv/ios/DerivedData`) - ### Starting a Simulator Start an iOS simulator for testing: @@ -223,7 +207,7 @@ Start an iOS simulator for testing: # Start default device devbox run start:sim -# Start specific device +# Start specific device by nickname devbox run start:sim iphone15 ``` @@ -239,42 +223,70 @@ Set the default device in `devbox.json`: } ``` -### Running Your App +### Stopping the Simulator -Build, install, and launch your app on the simulator: +Stop all running simulators: ```bash -# Build and run on default device -devbox run start:ios +devbox run stop:sim +``` + +### Adding Build and Deploy Scripts + +The plugin provides simulator and device management. Build and deploy commands are specific to your Xcode project, so you define them in your `devbox.json`. Here's a typical setup: -# Build and run on specific device -devbox run start:ios iphone15 +```json +{ + "include": ["github:segment-integrations/devbox-plugins?dir=plugins/ios"], + "shell": { + "scripts": { + "build:ios": [ + "ios.sh xcodebuild -scheme MyApp -configuration Debug -destination 'generic/platform=iOS Simulator' build" + ], + "build:release": [ + "ios.sh xcodebuild -scheme MyApp -configuration Release build" + ], + "start:app": [ + "ios.sh run ${1:-}" + ] + } + } +} ``` -The `start-ios` command: -1. Builds the app (runs `build-ios`) -2. Installs the app bundle matched by `IOS_APP_ARTIFACT` -3. Launches the app on the simulator +The `ios.sh run` command handles the full deployment pipeline: starts the simulator, runs your `build:ios` script, auto-detects the `.app` bundle, extracts the bundle ID from `Info.plist`, installs, and launches. The `${1:-}` syntax passes an optional device nickname through. + +**How app auto-detection works:** After building, `ios.sh run` finds your `.app` bundle using this precedence chain: -The app artifact path is auto-detected based on your Xcode configuration. You can override it: +1. `IOS_APP_ARTIFACT` env var โ€” if set, resolves the path/glob relative to project root +2. `xcodebuild -showBuildSettings` โ€” queries your Xcode project for BUILT_PRODUCTS_DIR + FULL_PRODUCT_NAME (auto-detected from project) +3. Recursive search of the project directory for `.app` bundles, skipping `Pods/`, `.build/`, `node_modules/`, `.devbox/`, and similar directories +4. Recursive search of the current working directory (if different from project root) + +In most projects, step 2 or 3 finds the right `.app` automatically with no configuration needed. If auto-detection doesn't work (e.g., multiple `.app` bundles, non-standard project layout), set `IOS_APP_ARTIFACT` explicitly: ```json { "env": { - "IOS_APP_ARTIFACT": ".devbox/virtenv/ios/DerivedData/Build/Products/Debug-iphonesimulator/MyApp.app" + "IOS_APP_ARTIFACT": "DerivedData/Build/Products/Debug-iphonesimulator/MyApp.app" } } ``` -### Stopping the Simulator - -Stop all running simulators: +With the scripts defined above, you can: ```bash -devbox run stop:sim +# Build the app +devbox run build:ios + +# Start simulator, install, and launch +devbox run start:app + +# Run on a specific device +devbox run start:app min ``` -This shuts down all iOS simulators. +See the [iOS example project](../../examples/ios/) for a complete working setup. The example project uses a local plugin path for development. If you use it as a template, change the `include` to the GitHub URL shown above. ### Complete Development Workflow Example @@ -284,49 +296,41 @@ Typical development session: # 1. Enter devbox shell devbox shell -# 2. Start simulator -devbox run start:sim max - -# 3. Build and run app -devbox run build -devbox run start:ios max +# 2. Build and deploy (starts simulator, builds, installs, and launches) +devbox run start:app -# 4. Make code changes, rebuild, and redeploy -devbox run build -devbox run start:ios max +# 3. Make code changes, rebuild, and redeploy +devbox run start:app # 5. Stop simulator when done devbox run stop:sim ``` -For a streamlined workflow, use the combined command that handles everything: - -```bash -# Build, install, and launch in one command -devbox run start:ios -``` - ## Testing ### Running E2E Tests -The plugin includes E2E test infrastructure using process-compose. Example projects include test suites that: - -1. Build the app -2. Sync simulators with device definitions -3. Start the simulator -4. Deploy and launch the app -5. Verify the app is running -6. Clean up (stop app and simulator in pure mode) +The [iOS example project](../../examples/ios/) includes E2E test infrastructure using process-compose. You can use it as a template for your own project. -Run the complete E2E test: +Copy the example test suite: ```bash -cd examples/ios -devbox run test:e2e +cp -r examples/ios/tests/ your-project/tests/ ``` -Duration: 3-5 minutes (faster with warm build cache) +Add a test script to your `devbox.json`: + +```json +{ + "shell": { + "scripts": { + "test:e2e": [ + "process-compose -f tests/test-suite.yaml --no-server --tui=${TEST_TUI:-false}" + ] + } + } +} +``` ### Normal Mode vs Pure Mode @@ -348,14 +352,9 @@ devbox run test:e2e ```bash devbox run --pure test:e2e - -# Or in CI: -IN_NIX_SHELL=pure devbox run test:e2e ``` -The `IN_NIX_SHELL` environment variable is automatically set by devbox: -- `IN_NIX_SHELL=impure` - Normal mode -- `IN_NIX_SHELL=pure` - Pure mode (set by `--pure` flag) +The `DEVBOX_PURE_SHELL` environment variable is automatically set by devbox when using the `--pure` flag. Scripts auto-detect this to determine whether to create fresh, isolated emulators/simulators. ### Interactive Monitoring @@ -369,23 +368,11 @@ The TUI shows process status, logs, and resource usage during test execution. ### Adding Tests to Your Project -Copy the example test suite: - -```bash -cp -r examples/ios/tests/ your-project/tests/ -``` - Configure for your app in `devbox.json`: ```json { "include": ["github:segment-integrations/devbox-plugins?dir=plugins/ios"], - "env": { - "IOS_APP_PROJECT": "YourApp.xcodeproj", - "IOS_APP_SCHEME": "YourApp", - "IOS_APP_BUNDLE_ID": "com.yourcompany.yourapp", - "IOS_APP_ARTIFACT": ".devbox/virtenv/ios/DerivedData/Build/Products/Debug-iphonesimulator/YourApp.app" - }, "shell": { "scripts": { "test:e2e": [ @@ -430,12 +417,9 @@ Environment variables: | Variable | Description | Default | |----------|-------------|---------| -| `IOS_APP_PROJECT` | Path to .xcodeproj or .xcworkspace | Required | -| `IOS_APP_SCHEME` | Xcode build scheme | Required | -| `IOS_APP_BUNDLE_ID` | App bundle identifier | Required | -| `IOS_APP_ARTIFACT` | Path to built .app bundle | Auto-detected | +| `IOS_APP_ARTIFACT` | Path or glob for .app bundle | Auto-detect | | `IOS_DEFAULT_DEVICE` | Default simulator device | `max` | -| `IOS_DOWNLOAD_RUNTIME` | Auto-download missing runtimes (0/1) | `0` | +| `IOS_DOWNLOAD_RUNTIME` | Auto-download missing runtimes (0/1) | `1` | | `TEST_TUI` | Show process-compose TUI (true/false) | `false` | | `BOOT_TIMEOUT` | Simulator boot timeout (seconds) | `120` | | `TEST_TIMEOUT` | Overall test timeout (seconds) | `300` | @@ -451,11 +435,7 @@ Configure the plugin by setting environment variables in `devbox.json`: "env": { "IOS_DEFAULT_DEVICE": "max", "IOS_DEVICES": "min,max", - "IOS_APP_PROJECT": "ios.xcodeproj", - "IOS_APP_SCHEME": "ios", - "IOS_APP_BUNDLE_ID": "com.example.ios", - "IOS_APP_ARTIFACT": ".devbox/virtenv/ios/DerivedData/Build/Products/Debug-iphonesimulator/ios.app", - "IOS_DOWNLOAD_RUNTIME": "0" + "IOS_DOWNLOAD_RUNTIME": "1" } } ``` @@ -463,18 +443,18 @@ Configure the plugin by setting environment variables in `devbox.json`: Key variables: - `IOS_DEFAULT_DEVICE` - Default device when none specified - `IOS_DEVICES` - Comma-separated device names to evaluate (empty = all) -- `IOS_APP_PROJECT` - Path to .xcodeproj or .xcworkspace -- `IOS_APP_SCHEME` - Xcode build scheme -- `IOS_APP_BUNDLE_ID` - App bundle identifier -- `IOS_APP_ARTIFACT` - Path to built .app bundle (auto-detected if not set) -- `IOS_DOWNLOAD_RUNTIME` - Auto-download missing iOS runtimes (0/1) +- `IOS_APP_ARTIFACT` - Path or glob for .app bundle (empty = auto-detect via xcodebuild + search) +- `IOS_APP_SCHEME` - Xcode scheme override (empty = auto-detect from project name) +- `IOS_BUILD_CONFIG` - Build configuration: Debug or Release (default: Debug) +- `IOS_DERIVED_DATA_PATH` - DerivedData directory (default: .devbox/virtenv/ios/DerivedData) +- `IOS_DOWNLOAD_RUNTIME` - Auto-download missing iOS runtimes (0/1, default: 1) ### Xcode Configuration The plugin automatically discovers Xcode using multiple fallback strategies: 1. Check `IOS_DEVELOPER_DIR` environment variable -2. Check cache file (`.devbox/virtenv/ios/.xcode_dev_dir.cache`, 1-hour TTL) +2. Check cache file (1-hour TTL) 3. Find latest Xcode in `/Applications/Xcode*.app` by version number 4. Use `xcode-select -p` output 5. Fallback to `/Applications/Xcode.app/Contents/Developer` @@ -489,8 +469,6 @@ Override discovery by setting `IOS_DEVELOPER_DIR`: } ``` -The discovered path is cached for 1 hour to improve shell startup performance. - ### Performance Optimization #### Skip iOS Setup for Faster Initialization @@ -542,10 +520,7 @@ This shows: - Scripts directory - Default device - Selected devices (from `IOS_DEVICES`) -- App project path -- App scheme -- App bundle ID -- App artifact path +- App artifact path (or auto-detect) - Runtime download setting ## Troubleshooting @@ -642,9 +617,9 @@ This shows: **Solutions**: -1. Verify app bundle exists: +1. Verify app bundle exists (check your build output directory): ```bash - ls -la $IOS_APP_ARTIFACT + find . -name '*.app' -type d -not -path '*/.devbox/*' ``` 2. Check simulator is booted: @@ -652,28 +627,30 @@ This shows: xcrun simctl list devices | grep Booted ``` -3. Verify bundle ID: +3. Verify bundle ID from a .app bundle: ```bash - defaults read "$IOS_APP_ARTIFACT/Info.plist" CFBundleIdentifier + /usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' /path/to/MyApp.app/Info.plist ``` -4. Check app bundle structure: - ```bash - ls -la "$IOS_APP_ARTIFACT/" +4. If auto-detection fails, set `IOS_APP_ARTIFACT` explicitly in `devbox.json`: + ```json + { + "env": { + "IOS_APP_ARTIFACT": "DerivedData/Build/Products/Debug-iphonesimulator/MyApp.app" + } + } ``` ### Build Failures with Nix Flags **Symptom**: Xcode build errors related to linker flags or Nix environment variables. -**Solution**: The plugin automatically strips Nix-related flags from the build environment. If you still encounter issues, the build script uses: +**Solution**: The iOS init hook strips Nix compilation variables (`LD`, `LDFLAGS`, `NIX_LDFLAGS`, `NIX_CFLAGS_COMPILE`, `NIX_CFLAGS_LINK`) at shell startup, so `xcodebuild` works natively in devbox shell. If you still encounter issues, use the `ios.sh xcodebuild` wrapper which strips these variables in a subshell: ```bash -env -u LD -u LDFLAGS -u NIX_LDFLAGS xcodebuild ... +ios.sh xcodebuild -scheme MyApp build ``` -This ensures Xcode uses its native toolchain without interference from Nix. - ### Lock File Out of Sync **Symptom**: "Warning: devices.lock may be stale" or checksum mismatch. @@ -700,8 +677,6 @@ Commit the updated lock file to version control. 2. Check system resources (CPU, memory): ```bash top - # or - htop ``` 3. View simulator logs: @@ -726,9 +701,6 @@ IOS_DEBUG=1 devbox shell # Global debug DEBUG=1 devbox shell - -# Debug during tests -IOS_DEBUG=1 devbox run test:e2e ``` Debug logs show: @@ -742,13 +714,13 @@ Debug logs show: **Check Test Logs:** -Test logs are written to `reports/ios-e2e-logs/`: +Test logs are written to `reports/`: ```bash # View all logs -ls -la reports/ios-e2e-logs/ +ls -la reports/ -# View specific process log +# View specific process log (paths depend on your test suite configuration) cat reports/ios-e2e-logs/build-app.log cat reports/ios-e2e-logs/ios-simulator.log cat reports/ios-e2e-logs/deploy-app.log @@ -767,13 +739,6 @@ xcrun simctl list devices | grep "Booted" tail -f ~/Library/Logs/CoreSimulator/*/system.log ``` -**Common Issues:** - -- **Build Failures**: Check Xcode installation, verify project path, check scheme exists, view build log -- **Simulator Won't Start**: Check CoreSimulatorService, restart service, check disk space, view simulator log -- **App Won't Install**: Verify app bundle exists, check simulator is booted, check bundle ID -- **Timeout Errors**: Increase `BOOT_TIMEOUT` for slow machines, increase `TEST_TIMEOUT` for large builds, check system resources - ## Common Use Cases ### Multi-Device Testing @@ -799,9 +764,13 @@ Test your app across multiple iOS versions: 4. Test on each device: ```bash - devbox run start:ios iphone_ios15 - devbox run start:ios iphone_ios17 - devbox run start:ios iphone_ios18 + devbox run start:sim iphone_ios15 + # run your tests... + devbox run stop:sim + + devbox run start:sim iphone_ios18 + # run your tests... + devbox run stop:sim ``` ### CI/CD Integration @@ -901,7 +870,7 @@ The device name in the JSON file should match the device type from `xcrun simctl ### Example Projects -- **[iOS Example](../../examples/ios/)**: Minimal iOS app demonstrating plugin usage +- **[iOS Example](../../examples/ios/)**: Complete iOS app with build scripts, deploy commands, and E2E test suites - **[React Native Example](../../examples/react-native/)**: Cross-platform app using both iOS and Android plugins ### Community and Support diff --git a/wiki/guides/quick-start.md b/wiki/guides/quick-start.md index dfa6665..0940f4a 100644 --- a/wiki/guides/quick-start.md +++ b/wiki/guides/quick-start.md @@ -1,18 +1,18 @@ # Quick Start Guide -Get up and running with Android, iOS, or React Native development in 5 minutes. +Set up a project-local Android or iOS development environment from scratch. ## Prerequisites -Install Devbox if you haven't already: +Install [Devbox](https://www.jetify.com/devbox/docs/installing_devbox/) if you haven't already: ```sh curl -fsSL https://get.jetify.com/devbox | bash ``` -## Choose Your Platform +Devbox handles downloading all build tools (JDK, Gradle, Xcode CLI tools, etc.) so you don't need to install them separately. -Pick your platform and follow the quickstart below: +## Choose Your Platform - **[Android](#android-quickstart)** - Native Android development with emulators - **[iOS](#ios-quickstart)** - Native iOS development with simulators (macOS only) @@ -24,12 +24,13 @@ Pick your platform and follow the quickstart below: ### 1. Initialize Your Project +In your existing Android project directory: + ```sh -# Initialize devbox in your existing Android project devbox init ``` -Add the Android plugin to your `devbox.json`: +Replace the contents of your `devbox.json` with: ```json { @@ -37,16 +38,13 @@ Add the Android plugin to your `devbox.json`: "packages": { "jdk17": "latest", "gradle": "latest" - }, - "env": { - "ANDROID_APP_APK": "app/build/outputs/apk/debug/app-debug.apk" } } ``` -Set `ANDROID_APP_APK` to the path where your build outputs the APK. The app's package name (`ANDROID_APP_ID`) is auto-detected from the APK at install time. +The `include` line adds the Android plugin from GitHub. Devbox downloads the Android SDK, emulator, and device management tools automatically. The `packages` section adds JDK and Gradle for building your app. -> **Note:** These are custom plugins hosted on GitHub, not built-in devbox plugins. You cannot use `devbox add plugin:android` โ€” add the `include` URL to your `devbox.json` manually. +> **Note:** Plugins are included via URL in `devbox.json`, not with `devbox add`. You cannot use `devbox add plugin:android`. ### 2. Enter the Development Environment @@ -54,7 +52,11 @@ Set `ANDROID_APP_APK` to the path where your build outputs the APK. The app's pa devbox shell ``` -This installs the Android SDK, build tools, and emulator without touching your global `~/.android` directory. +On first run, this downloads the Android SDK via Nix. Subsequent runs are fast. The SDK is stored project-locally โ€” nothing is written to `~/.android`. + +Two quick devbox concepts: +- `devbox shell` enters an interactive shell with all tools on your PATH +- `devbox run