diff --git a/.github/workflows/all_plugins.yaml b/.github/workflows/all_plugins.yaml index 1cbfd23dd3da..7c7a93f39ae5 100644 --- a/.github/workflows/all_plugins.yaml +++ b/.github/workflows/all_plugins.yaml @@ -22,11 +22,24 @@ permissions: contents: read jobs: + # Home for every check that needs nothing but a bootstrapped workspace. The + # pub dry run, the example `pub get` and the license-header check used to be + # three separate jobs; each ran for a couple of minutes and paid a full + # checkout + setup + bootstrap to get there, so they are steps here instead. + # Analysis runs first - it is the signal people wait on - and the cheap + # checks follow with `if: always()`, so one failing check still reports the + # others, exactly as separate jobs did. analyze: - timeout-minutes: 50 + # Covers the folded checks as well as analysis itself. + timeout-minutes: 70 runs-on: ubuntu-latest steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 + with: + # flutter_plugin_tools resolves a git base ref even when analyzing + # everything; a shallow clone has neither history nor origin/main + # ('fatal: Not a valid object name main'). Same as the format job. + fetch-depth: 0 - uses: ./.github/actions/setup-flutterfire with: node: 'false' @@ -38,47 +51,55 @@ jobs: dart run scripts/generate_versions_spm.dart git diff --exit-code -- ':(glob)packages/**/Package.swift' - name: 'Run Analyze' + # flutter_plugin_tools is the flutter/packages-standard analyzer driver + # (already used by the format job). The custom-analysis config lists + # every package: FlutterFire manages analysis options centrally, which + # the tool otherwise treats as unexpected. publish-check was evaluated + # too but does not fit this repo's release model (it errors on + # already-published versions and requires AUTHORS files), so the pub + # dry-run below stays on melos. run: | dart ./.github/workflows/scripts/generate-dummy-firebase-configs.dart - melos analyze-ci + dart pub global activate flutter_plugin_tools 0.14.2 + dart pub global run flutter_plugin_tools analyze \ + --base-branch=origin/main \ + --custom-analysis=.github/workflows/config/custom_analysis.yaml + (cd tests && dart analyze .) - name: 'Validate Workspace' if: always() run: melos run validate:workspace - - # Separated from "analyse" action as pubspec_override file is not being taken into account when running `flutter pub publish --dry-run` - # This will fail on CI until this is fixed: https://github.com/invertase/melos/issues/467 - # You need to switch to Flutter 3.3.0, and run this test manually to check it works and update PR to confirm its success - pub_dry_run: - timeout-minutes: 30 - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - - uses: ./.github/actions/setup-flutterfire - with: - node: 'false' - java: 'false' - # This job needs the whole workspace bootstrapped, which melos-action does itself. - melos-run-bootstrap: 'true' - - name: 'Pub Check' + # Note `pubspec_overrides.yaml` is not taken into account by + # `pub publish --dry-run`, which is why this used to live in its own job - + # the setup is identical either way, so the split bought nothing. + # This will fail on CI until this is fixed: https://github.com/invertase/melos/issues/467 + # You need to switch to Flutter 3.3.0, and run this test manually to check it works and update PR to confirm its success + - name: 'Pub Check (publish dry run)' + if: always() + # `melos bootstrap` (run for the analysis above) syncs versions into the + # Package.swift / Constants.swift files, and `pub publish --dry-run` + # refuses a dirty tree ("checked-in files are modified in git") - the + # standalone job this was folded from started with a fresh checkout. + # Restore the committed state before validating. run: | + git checkout -- packages/ melos exec -c 1 --no-private --ignore="*example*" -- \ dart pub publish --dry-run - - pub_get_check: - timeout-minutes: 30 - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - - uses: ./.github/actions/setup-flutterfire - with: - node: 'false' - java: 'false' - # This job needs the whole workspace bootstrapped, which melos-action does itself. - melos-run-bootstrap: 'true' - - name: 'Flutter Pub Get' + - name: 'Flutter Pub Get (examples)' + if: always() run: | melos exec -c 1 --scope="*example*" -- \ "flutter pub get" + # Go is used by addlicense command (addlicense is used in melos run + # check-license-header) + - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 + if: always() + with: + go-version: '^1.13.1' + - name: 'Check license headers' + if: always() + run: | + go install github.com/google/addlicense@latest + melos run check-license-header format: # switch back to ubuntu-latest when swiftformat is working again @@ -140,7 +161,6 @@ jobs: with: node: 'false' java: 'false' - flutter-version: '3.41.9' # This job needs the whole workspace bootstrapped, which melos-action does itself. melos-run-bootstrap: 'true' - name: 'Build Examples' @@ -148,9 +168,14 @@ jobs: dart ./.github/workflows/scripts/generate-dummy-firebase-configs.dart melos exec -c 1 --scope="*example*" --dir-exists="web" -- \ "flutter build web" - swift-integration: + # iOS and macOS used to be built back to back in one `swift-integration` job, + # which meant a macOS-only break waited out the whole iOS build first. They + # are independent builds of the same example app, so they run as two jobs. + # `swift-integration.dart` still builds both when no `--platform` is passed, + # which is how it is run locally. + swift-integration-ios: runs-on: macos-15 - timeout-minutes: 45 + timeout-minutes: 35 env: FLUTTER_DEPENDENCIES: "cloud_firestore firebase_remote_config cloud_functions firebase_database firebase_auth firebase_storage firebase_analytics firebase_messaging firebase_app_check firebase_in_app_messaging firebase_performance firebase_crashlytics firebase_ml_model_downloader firebase_app_installations firebase_ai" PR_HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }} @@ -163,7 +188,6 @@ jobs: with: node: 'false' java: 'false' - flutter-version: '3.41.9' # This job needs the whole workspace bootstrapped, which melos-action does itself. melos-run-bootstrap: 'true' - name: Setup firebase_core example app to test Swift integration @@ -174,43 +198,56 @@ jobs: cd ../../../.. - name: 'Swift Integration Setup' run: flutter config --enable-swift-package-manager - - name: 'Build Apps with Swift Package Manager' + - name: 'Build iOS App with Swift Package Manager' run: | dart ./.github/workflows/scripts/generate-dummy-firebase-configs.dart - dart ./.github/workflows/scripts/swift-integration.dart $FLUTTER_DEPENDENCIES + dart ./.github/workflows/scripts/swift-integration.dart --platform=ios $FLUTTER_DEPENDENCIES - test: - runs-on: ubuntu-latest - timeout-minutes: 30 + swift-integration-macos: + runs-on: macos-15 + timeout-minutes: 35 + env: + FLUTTER_DEPENDENCIES: "cloud_firestore firebase_remote_config cloud_functions firebase_database firebase_auth firebase_storage firebase_analytics firebase_messaging firebase_app_check firebase_in_app_messaging firebase_performance firebase_crashlytics firebase_ml_model_downloader firebase_app_installations firebase_ai" + PR_HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }} steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 + - name: Xcode + # Firebase iOS SDK: minimum Xcode 26.2. + run: sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer - uses: ./.github/actions/setup-flutterfire with: node: 'false' java: 'false' # This job needs the whole workspace bootstrapped, which melos-action does itself. melos-run-bootstrap: 'true' - - name: 'Flutter Test' - run: melos run test --no-select - - name: 'Flutter Test - Web' - run: melos run test:web --no-select + - name: Setup firebase_core example app to test Swift integration + # Run after melos bootstrap so workspace pubspec_overrides resolve unpublished package versions. + run: | + cd packages/firebase_core/firebase_core/example + flutter pub add $FLUTTER_DEPENDENCIES + cd ../../../.. + - name: 'Swift Integration Setup' + run: flutter config --enable-swift-package-manager + - name: 'Build macOS App with Swift Package Manager' + # The script drops firebase_messaging from the example app before the + # macOS build; with iOS in its own job that no longer has to happen + # after a successful iOS build in the same checkout. + run: | + dart ./.github/workflows/scripts/generate-dummy-firebase-configs.dart + dart ./.github/workflows/scripts/swift-integration.dart --platform=macos $FLUTTER_DEPENDENCIES - check-files-license-headers: + test: runs-on: ubuntu-latest timeout-minutes: 30 steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 - with: - go-version: '^1.13.1' - # Go is used by addlicense command (addlicense is used in melos run - # check-license-header) - - run: go install github.com/google/addlicense@latest - # Running `melos bootstrap` is not needed because we use Melos just for the - # `check-license-header` script, so no `bootstrap-scope` is passed here. - uses: ./.github/actions/setup-flutterfire with: node: 'false' java: 'false' - - name: Check license header - run: melos run check-license-header + # This job needs the whole workspace bootstrapped, which melos-action does itself. + melos-run-bootstrap: 'true' + - name: 'Flutter Test' + run: melos run test --no-select + - name: 'Flutter Test - Web' + run: melos run test:web --no-select diff --git a/.github/workflows/android.yaml b/.github/workflows/android.yaml deleted file mode 100644 index 7ab981b5b012..000000000000 --- a/.github/workflows/android.yaml +++ /dev/null @@ -1,211 +0,0 @@ -name: e2e-android - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }}-android - cancel-in-progress: true - -permissions: - contents: read - -on: - pull_request: - paths-ignore: - - 'docs/**' - - 'website/**' - - '**/example/**' - - '!**/example/integration_test/**' - - '**/flutterfire_ui/**' - - '**.md' - push: - branches: - - main - paths-ignore: - - 'docs/**' - - 'website/**' - - '**/example/**' - - '!**/example/integration_test/**' - - '**/flutterfire_ui/**' - - '**.md' - workflow_call: - inputs: - nightly_test_mode: - type: boolean - default: false - -jobs: - android: - name: android (${{ matrix.suite.name }}) - permissions: - contents: read - runs-on: ubuntu-latest - timeout-minutes: ${{ inputs.nightly_test_mode && 5 || 25 }} - strategy: - fail-fast: false - matrix: - # The `tests` app is sharded by product suite so a hang or flake costs - # one small job instead of the whole run. `integration_test/e2e_test.dart` - # still aggregates every suite for Windows and local runs. - suite: - - name: core_misc - working_directory: tests - target: integration_test/shards/core_misc_shard_test.dart - scope: tests - - name: firestore - working_directory: packages/cloud_firestore/cloud_firestore/example - target: integration_test/e2e_test.dart - scope: 'cloud_firestore*' - exclude: - # Nightly drops the firestore example leg. The entry is repeated in - # full because matrix `exclude` compares the whole object; only `name` - # is switched, so outside nightly it matches nothing. - - suite: - name: ${{ inputs.nightly_test_mode && 'firestore' || 'none' }} - working_directory: packages/cloud_firestore/cloud_firestore/example - target: integration_test/e2e_test.dart - scope: 'cloud_firestore*' - env: - AVD_ARCH: x86_64 - AVD_API_LEVEL: 34 - AVD_TARGET: google_apis - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - - uses: ./.github/actions/setup-flutterfire - with: - firebase-tools-version: '15.25.1' - # Each matrix leg bootstraps only the packages it exercises. - bootstrap-scope: ${{ matrix.suite.scope }} - - name: Firebase Emulator Cache - id: firebase-emulator-cache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - # Must match the save path exactly - path: ~/.cache/firebase/emulators - key: firebase-emulators-v5-${{ env.FIREBASE_TOOLS_VERSION }} - # Trailing dash so this never prefix-matches a future version's key - restore-keys: firebase-emulators-v5- - - name: Generate dummy Firebase configs - run: dart ./.github/workflows/scripts/generate-dummy-firebase-configs.dart --firestore-native - - name: Install Cloud Functions dependencies - # `firebase emulators:exec` does not install these itself (the old - # start-firebase-emulator.sh wrapper did); without them the functions - # emulator fails to load the function definitions. - run: npm ci --prefix .github/workflows/scripts/functions - - name: Enable KVM - run: | - echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules - sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm - - name: Gradle cache - uses: gradle/actions/setup-gradle@90ddb51e90a5fd9ba75f40cf85156b7b41bf76a3 - - name: Free Disk Space (Ubuntu) - uses: AdityaGarg8/remove-unwanted-software@90e01b21170618765a73370fcc3abbd1684a7793 - with: - remove-dotnet: true - remove-haskell: true - remove-codeql: true - remove-docker-images: true - remove-large-packages: true - - name: Prepare AVD home on /mnt - # GitHub-hosted runners mount a ~74GB volume at /mnt. Create it before AVD cache - # restore and android-emulator-runner (avdmanager needs the space at create time). - run: | - sudo mkdir -p /mnt/avd - sudo chown "$USER:$USER" /mnt/avd - df -h / /mnt - - name: AVD cache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - id: avd-cache - with: - # Must match the save path exactly - path: | - /mnt/avd/* - ~/.android/adb* - # The emulator build is part of the key so bumping `emulator-build:` below - # invalidates the cached AVD instead of reusing an image from the old build. - key: avd-${{ runner.os }}-${{ env.AVD_API_LEVEL }}-${{ env.AVD_TARGET }}-${{ env.AVD_ARCH }}-14214601 - - name: Link AVD home to /mnt - # android-emulator-runner exportVariables ANDROID_AVD_HOME to $HOME/.android/avd - run: | - mkdir -p "$HOME/.android" - rm -rf "$HOME/.android/avd" - ln -s /mnt/avd "$HOME/.android/avd" - - name: Pre-build APK - # Build outside the emulator so the AVD does not boot and idle through the - # whole Gradle build. `flutter test` below reuses this warm build cache. - # The validation skip matches `flutter test`, which does not enforce the - # minimum Gradle version either (the firestore example pins Gradle 8.4). - working-directory: ${{ matrix.suite.working_directory }} - timeout-minutes: 25 - run: flutter build apk --debug --target=${{ matrix.suite.target }} --android-skip-build-dependency-validation - - name: Start AVD then run E2E tests - uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a - timeout-minutes: 20 - env: - ANDROID_AVD_HOME: /mnt/avd - STORAGE_EMULATOR_DEBUG: 'true' - with: - api-level: ${{ env.AVD_API_LEVEL }} - target: ${{ env.AVD_TARGET }} - arch: ${{ env.AVD_ARCH }} - emulator-build: 14214601 - # The default (true) wipes and recreates the AVD, making the cache above useless. - force-avd-creation: false - # firebase.json and the emulator rule files live here, so `emulators:exec` - # has to run from this directory; the test command cds back to the - # package under test. - working-directory: .github/workflows/scripts - # `emulators:exec` owns the emulator lifecycle: it boots the suite, runs - # the command and tears the suite down, exiting with the command's exit - # code. Nothing is left running between steps. - script: | - firebase emulators:exec --project flutterfire-e2e-tests "cd ${GITHUB_WORKSPACE}/${{ matrix.suite.working_directory }} && flutter test ${{ matrix.suite.target }} --timeout 10x --dart-define=CI=true -d emulator-5554" - - name: Ensure Appium is shut down - # Required because of below issue where emulator failing to shut down properly causes tests to fail - # https://github.com/ReactiveCircus/android-emulator-runner/issues/385 - run: | - pgrep -f appium && pkill -f appium || echo "No Appium process found" - - name: Save Firestore Emulator Cache - # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. - if: github.ref == 'refs/heads/main' - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - key: ${{ steps.firebase-emulator-cache.outputs.cache-primary-key }} - # Must match the restore path exactly - path: ~/.cache/firebase/emulators - - name: Save Android Emulator Cache - # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. - # Skip on a cache hit: the AVD image is multi-GB and re-uploading it unchanged on - # every main run is pure waste. - if: github.ref == 'refs/heads/main' && steps.avd-cache.outputs.cache-hit != 'true' - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - with: - key: ${{ steps.avd-cache.outputs.cache-primary-key }} - # Must match the restore path exactly - path: | - /mnt/avd/* - ~/.android/adb* - - agp9-compatibility: - permissions: - contents: read - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - - uses: ./.github/actions/setup-flutterfire - with: - node: 'false' - bootstrap-scope: 'tests' - - name: Gradle cache - uses: gradle/actions/setup-gradle@90ddb51e90a5fd9ba75f40cf85156b7b41bf76a3 - - name: 'Build tests app with AGP 9' - timeout-minutes: 25 - run: bash ./.github/workflows/scripts/agp9-compatibility.sh diff --git a/.github/workflows/config/custom_analysis.yaml b/.github/workflows/config/custom_analysis.yaml new file mode 100644 index 000000000000..cd2664f7b992 --- /dev/null +++ b/.github/workflows/config/custom_analysis.yaml @@ -0,0 +1,47 @@ +# Packages allowed to have their own analysis_options.yaml. +# FlutterFire manages analysis centrally via melos + the root config; +# every package (and its example) carries local options by design. +- cloud_firestore +- cloud_firestore/cloud_firestore_platform_interface +- cloud_firestore/cloud_firestore_web +- cloud_functions +- cloud_functions/cloud_functions_platform_interface +- cloud_functions/cloud_functions_web +- firebase_ai +- firebase_analytics +- firebase_analytics/firebase_analytics_platform_interface +- firebase_analytics/firebase_analytics_web +- firebase_app_check +- firebase_app_check/firebase_app_check_platform_interface +- firebase_app_check/firebase_app_check_web +- firebase_app_installations +- firebase_app_installations/firebase_app_installations_platform_interface +- firebase_app_installations/firebase_app_installations_web +- firebase_auth +- firebase_auth/firebase_auth_platform_interface +- firebase_auth/firebase_auth_web +- firebase_core +- firebase_core/firebase_core_platform_interface +- firebase_core/firebase_core_web +- firebase_crashlytics +- firebase_crashlytics/firebase_crashlytics_platform_interface +- firebase_data_connect +- firebase_database +- firebase_database/firebase_database_platform_interface +- firebase_database/firebase_database_web +- firebase_in_app_messaging +- firebase_in_app_messaging/firebase_in_app_messaging_platform_interface +- firebase_messaging +- firebase_messaging/firebase_messaging_platform_interface +- firebase_messaging/firebase_messaging_web +- firebase_ml_model_downloader +- firebase_ml_model_downloader/firebase_ml_model_downloader_platform_interface +- firebase_performance +- firebase_performance/firebase_performance_platform_interface +- firebase_performance/firebase_performance_web +- firebase_remote_config +- firebase_remote_config/firebase_remote_config_platform_interface +- firebase_remote_config/firebase_remote_config_web +- firebase_storage +- firebase_storage/firebase_storage_platform_interface +- firebase_storage/firebase_storage_web diff --git a/.github/workflows/e2e_tests_ai.yaml b/.github/workflows/e2e_tests_ai.yaml new file mode 100644 index 000000000000..860101dc13cc --- /dev/null +++ b/.github/workflows/e2e_tests_ai.yaml @@ -0,0 +1,117 @@ +name: e2e-ai + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-ai + cancel-in-progress: true + +# Live tier: this product has no emulator, so every job talks to the real +# `flutterfire-e2e-tests` project using the config in repository secrets. +# Fork and dependabot PRs do not get those secrets, so the reusable workflow +# guards every job. +# +# Thin caller: one job per platform this product supports, each delegating +# to the matching `reusable_e2e_.yaml`. Platforms the product does +# not support have no job here, so they render no check at all. +# Path-filtered so it only runs when this product or its dependencies change; +# the nightly workflow_call still runs it unconditionally. +on: + pull_request: + paths: + - 'packages/firebase_ai/**' + - 'packages/firebase_core/**' + - '.github/workflows/e2e_tests_ai.yaml' + - '.github/workflows/reusable_e2e_*.yaml' + - '.github/actions/setup-flutterfire/**' + - '.github/workflows/scripts/**' + push: + branches: + - main + paths: + - 'packages/firebase_ai/**' + - 'packages/firebase_core/**' + - '.github/workflows/e2e_tests_ai.yaml' + - '.github/workflows/reusable_e2e_*.yaml' + - '.github/actions/setup-flutterfire/**' + - '.github/workflows/scripts/**' + workflow_call: + inputs: + nightly_test_mode: + type: boolean + default: false + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: + required: true + TESTS_E2E_GOOGLE_SERVICES_JSON: + required: true + +permissions: + contents: read + +jobs: + changes: + uses: ./.github/workflows/reusable_e2e_changes.yaml + with: + package-path: 'packages/firebase_ai/firebase_ai' + inject-config-secrets: true + + android: + needs: changes + if: needs.changes.outputs.android == 'true' + uses: ./.github/workflows/reusable_e2e_android.yaml + with: + package-path: 'packages/firebase_ai/firebase_ai' + package-scope: 'firebase_ai*' + native-config-args: '--live-tier-plist=ai' + inject-config-secrets: true + use-firebase-emulators: false + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} + + ios: + needs: changes + if: needs.changes.outputs.ios == 'true' + uses: ./.github/workflows/reusable_e2e_ios.yaml + with: + package-path: 'packages/firebase_ai/firebase_ai' + package-scope: 'firebase_ai*' + cache-key-suffix: 'ai' + native-config-args: '--live-tier-plist=ai' + inject-config-secrets: true + use-firebase-emulators: false + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} + + macos: + needs: changes + if: needs.changes.outputs.macos == 'true' + uses: ./.github/workflows/reusable_e2e_macos.yaml + with: + package-path: 'packages/firebase_ai/firebase_ai' + package-scope: 'firebase_ai*' + cache-key-suffix: 'ai' + native-config-args: '--live-tier-plist=ai' + inject-config-secrets: true + use-firebase-emulators: false + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} + + web: + needs: changes + if: needs.changes.outputs.web == 'true' + uses: ./.github/workflows/reusable_e2e_web.yaml + with: + package-path: 'packages/firebase_ai/firebase_ai' + package-scope: 'firebase_ai*' + native-config-args: '--live-tier-plist=ai' + inject-config-secrets: true + use-firebase-emulators: false + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} diff --git a/.github/workflows/e2e_tests_analytics.yaml b/.github/workflows/e2e_tests_analytics.yaml new file mode 100644 index 000000000000..bd5acc729d94 --- /dev/null +++ b/.github/workflows/e2e_tests_analytics.yaml @@ -0,0 +1,119 @@ +name: e2e-analytics + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-analytics + cancel-in-progress: true + +# Live tier: this product has no emulator, so every job talks to the real +# `flutterfire-e2e-tests` project using the config in repository secrets. +# Fork and dependabot PRs do not get those secrets, so the reusable workflow +# guards every job. +# +# Thin caller: one job per platform this product supports, each delegating +# to the matching `reusable_e2e_.yaml`. Platforms the product does +# not support have no job here, so they render no check at all. +# Path-filtered so it only runs when this product or its dependencies change; +# the nightly workflow_call still runs it unconditionally. +on: + pull_request: + paths: + - 'packages/firebase_analytics/**' + - 'packages/firebase_core/**' + - '.github/workflows/e2e_tests_analytics.yaml' + - '.github/workflows/reusable_e2e_*.yaml' + - '.github/actions/setup-flutterfire/**' + - '.github/workflows/scripts/**' + push: + branches: + - main + paths: + - 'packages/firebase_analytics/**' + - 'packages/firebase_core/**' + - '.github/workflows/e2e_tests_analytics.yaml' + - '.github/workflows/reusable_e2e_*.yaml' + - '.github/actions/setup-flutterfire/**' + - '.github/workflows/scripts/**' + workflow_call: + inputs: + nightly_test_mode: + type: boolean + default: false + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: + required: true + TESTS_E2E_GOOGLE_SERVICES_JSON: + required: true + +permissions: + contents: read + +jobs: + changes: + uses: ./.github/workflows/reusable_e2e_changes.yaml + with: + package-path: 'packages/firebase_analytics/firebase_analytics' + platform-interface-package: 'firebase_analytics_platform_interface' + web-package: 'firebase_analytics_web' + inject-config-secrets: true + + android: + needs: changes + if: needs.changes.outputs.android == 'true' + uses: ./.github/workflows/reusable_e2e_android.yaml + with: + package-path: 'packages/firebase_analytics/firebase_analytics' + package-scope: 'firebase_analytics*' + native-config-args: '--live-tier-plist=analytics' + inject-config-secrets: true + use-firebase-emulators: false + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} + + ios: + needs: changes + if: needs.changes.outputs.ios == 'true' + uses: ./.github/workflows/reusable_e2e_ios.yaml + with: + package-path: 'packages/firebase_analytics/firebase_analytics' + package-scope: 'firebase_analytics*' + cache-key-suffix: 'analytics' + native-config-args: '--live-tier-plist=analytics' + inject-config-secrets: true + use-firebase-emulators: false + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} + + macos: + needs: changes + if: needs.changes.outputs.macos == 'true' + uses: ./.github/workflows/reusable_e2e_macos.yaml + with: + package-path: 'packages/firebase_analytics/firebase_analytics' + package-scope: 'firebase_analytics*' + cache-key-suffix: 'analytics' + native-config-args: '--live-tier-plist=analytics' + inject-config-secrets: true + use-firebase-emulators: false + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} + + web: + needs: changes + if: needs.changes.outputs.web == 'true' + uses: ./.github/workflows/reusable_e2e_web.yaml + with: + package-path: 'packages/firebase_analytics/firebase_analytics' + package-scope: 'firebase_analytics*' + native-config-args: '--live-tier-plist=analytics' + inject-config-secrets: true + use-firebase-emulators: false + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} diff --git a/.github/workflows/e2e_tests_app_check.yaml b/.github/workflows/e2e_tests_app_check.yaml new file mode 100644 index 000000000000..14b05d188005 --- /dev/null +++ b/.github/workflows/e2e_tests_app_check.yaml @@ -0,0 +1,134 @@ +name: e2e-app-check + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-app_check + cancel-in-progress: true + +# Live tier: this product has no emulator, so every job talks to the real +# `flutterfire-e2e-tests` project using the config in repository secrets. +# Fork and dependabot PRs do not get those secrets, so the reusable workflow +# guards every job. +# +# Thin caller: one job per platform this product supports, each delegating +# to the matching `reusable_e2e_.yaml`. Platforms the product does +# not support have no job here, so they render no check at all. +# Path-filtered so it only runs when this product or its dependencies change; +# the nightly workflow_call still runs it unconditionally. +on: + pull_request: + paths: + - 'packages/firebase_app_check/**' + - 'packages/firebase_core/**' + - '.github/workflows/e2e_tests_app_check.yaml' + - '.github/workflows/reusable_e2e_*.yaml' + - '.github/actions/setup-flutterfire/**' + - '.github/workflows/scripts/**' + push: + branches: + - main + paths: + - 'packages/firebase_app_check/**' + - 'packages/firebase_core/**' + - '.github/workflows/e2e_tests_app_check.yaml' + - '.github/workflows/reusable_e2e_*.yaml' + - '.github/actions/setup-flutterfire/**' + - '.github/workflows/scripts/**' + workflow_call: + inputs: + nightly_test_mode: + type: boolean + default: false + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: + required: true + TESTS_E2E_GOOGLE_SERVICES_JSON: + required: true + +permissions: + contents: read + +jobs: + changes: + uses: ./.github/workflows/reusable_e2e_changes.yaml + with: + package-path: 'packages/firebase_app_check/firebase_app_check' + platform-interface-package: 'firebase_app_check_platform_interface' + web-package: 'firebase_app_check_web' + inject-config-secrets: true + + android: + needs: changes + if: needs.changes.outputs.android == 'true' + uses: ./.github/workflows/reusable_e2e_android.yaml + with: + package-path: 'packages/firebase_app_check/firebase_app_check' + package-scope: 'firebase_app_check*' + native-config-args: '--live-tier-plist=app_check' + inject-config-secrets: true + use-firebase-emulators: false + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} + + ios: + needs: changes + if: needs.changes.outputs.ios == 'true' + uses: ./.github/workflows/reusable_e2e_ios.yaml + with: + package-path: 'packages/firebase_app_check/firebase_app_check' + package-scope: 'firebase_app_check*' + cache-key-suffix: 'app_check' + native-config-args: '--live-tier-plist=app_check' + inject-config-secrets: true + use-firebase-emulators: false + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} + + macos: + needs: changes + if: needs.changes.outputs.macos == 'true' + uses: ./.github/workflows/reusable_e2e_macos.yaml + with: + package-path: 'packages/firebase_app_check/firebase_app_check' + package-scope: 'firebase_app_check*' + cache-key-suffix: 'app_check' + native-config-args: '--live-tier-plist=app_check' + inject-config-secrets: true + use-firebase-emulators: false + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} + + web: + needs: changes + if: needs.changes.outputs.web == 'true' + uses: ./.github/workflows/reusable_e2e_web.yaml + with: + package-path: 'packages/firebase_app_check/firebase_app_check' + package-scope: 'firebase_app_check*' + native-config-args: '--live-tier-plist=app_check' + inject-config-secrets: true + use-firebase-emulators: false + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} + + windows: + needs: changes + if: needs.changes.outputs.windows == 'true' + uses: ./.github/workflows/reusable_e2e_windows.yaml + with: + package-path: 'packages/firebase_app_check/firebase_app_check' + package-scope: 'firebase_app_check*' + native-config-args: '--live-tier-plist=app_check' + inject-config-secrets: true + use-firebase-emulators: false + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} diff --git a/.github/workflows/e2e_tests_app_installations.yaml b/.github/workflows/e2e_tests_app_installations.yaml new file mode 100644 index 000000000000..97e0a49325f4 --- /dev/null +++ b/.github/workflows/e2e_tests_app_installations.yaml @@ -0,0 +1,109 @@ +name: e2e-app-installations + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-app_installations + cancel-in-progress: true + +# Live tier: this product has no emulator, so every job talks to the real +# `flutterfire-e2e-tests` project using the config in repository secrets. +# Fork and dependabot PRs do not get those secrets, so the reusable workflow +# guards every job. +# +# Thin caller: one job per platform this product supports, each delegating +# to the matching `reusable_e2e_.yaml`. Platforms the product does +# not support have no job here, so they render no check at all. +# Path-filtered so it only runs when this product or its dependencies change; +# the nightly workflow_call still runs it unconditionally. +on: + pull_request: + paths: + - 'packages/firebase_app_installations/**' + - 'packages/firebase_core/**' + - '.github/workflows/e2e_tests_app_installations.yaml' + - '.github/workflows/reusable_e2e_*.yaml' + - '.github/actions/setup-flutterfire/**' + - '.github/workflows/scripts/**' + push: + branches: + - main + paths: + - 'packages/firebase_app_installations/**' + - 'packages/firebase_core/**' + - '.github/workflows/e2e_tests_app_installations.yaml' + - '.github/workflows/reusable_e2e_*.yaml' + - '.github/actions/setup-flutterfire/**' + - '.github/workflows/scripts/**' + workflow_call: + inputs: + nightly_test_mode: + type: boolean + default: false + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: + required: true + TESTS_E2E_GOOGLE_SERVICES_JSON: + required: true + +permissions: + contents: read + +jobs: + changes: + uses: ./.github/workflows/reusable_e2e_changes.yaml + with: + package-path: 'packages/firebase_app_installations/firebase_app_installations' + platform-interface-package: 'firebase_app_installations_platform_interface' + web-package: 'firebase_app_installations_web' + inject-config-secrets: true + + android: + needs: changes + if: needs.changes.outputs.android == 'true' + uses: ./.github/workflows/reusable_e2e_android.yaml + with: + package-path: 'packages/firebase_app_installations/firebase_app_installations' + package-scope: 'firebase_app_installations*' + native-config-args: '--live-tier-plist=app_installations' + inject-config-secrets: true + use-firebase-emulators: false + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} + + ios: + needs: changes + if: needs.changes.outputs.ios == 'true' + uses: ./.github/workflows/reusable_e2e_ios.yaml + with: + package-path: 'packages/firebase_app_installations/firebase_app_installations' + package-scope: 'firebase_app_installations*' + cache-key-suffix: 'app_installations' + native-config-args: '--live-tier-plist=app_installations' + inject-config-secrets: true + use-firebase-emulators: false + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} + + # No macos job: every installations e2e test skips itself on macOS pending + # the keychain-sharing entitlement work + # (https://github.com/firebase/flutterfire/issues/9538), so the job could + # only ever report "0 passed, 4 skipped" - which the tally guard rightly + # refuses to call green. + + web: + needs: changes + if: needs.changes.outputs.web == 'true' + uses: ./.github/workflows/reusable_e2e_web.yaml + with: + package-path: 'packages/firebase_app_installations/firebase_app_installations' + package-scope: 'firebase_app_installations*' + native-config-args: '--live-tier-plist=app_installations' + inject-config-secrets: true + use-firebase-emulators: false + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} diff --git a/.github/workflows/e2e_tests_auth.yaml b/.github/workflows/e2e_tests_auth.yaml index 1cfb05ec0414..a0ce1ca07dcb 100644 --- a/.github/workflows/e2e_tests_auth.yaml +++ b/.github/workflows/e2e_tests_auth.yaml @@ -4,15 +4,27 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }}-auth cancel-in-progress: true +# Emulator tier: every job runs under `firebase emulators:exec`, so no live +# project credentials are needed. +# +# Auth needs BOTH tiers: its tests run against the Auth emulator, but the +# validatePassword() suite calls the live password-policy REST API, which +# rejects placeholder credentials - so real config is injected while the +# emulators stay on. The example commits its plists and +# google-services.json, so no generator run is needed. +# +# Thin caller: one job per platform this product supports, each delegating +# to the matching `reusable_e2e_.yaml`. Platforms the product does +# not support have no job here, so they render no check at all. +# Path-filtered so it only runs when this product or its dependencies change; +# the nightly workflow_call still runs it unconditionally. on: pull_request: - # Auth e2e only exercises this package; running it on every PR costs 5 - # jobs (2 macOS) for changes that cannot affect it. The nightly - # workflow_call still runs it unconditionally. paths: - 'packages/firebase_auth/**' - 'packages/firebase_core/**' - '.github/workflows/e2e_tests_auth.yaml' + - '.github/workflows/reusable_e2e_*.yaml' - '.github/actions/setup-flutterfire/**' - '.github/workflows/scripts/**' push: @@ -22,6 +34,7 @@ on: - 'packages/firebase_auth/**' - 'packages/firebase_core/**' - '.github/workflows/e2e_tests_auth.yaml' + - '.github/workflows/reusable_e2e_*.yaml' - '.github/actions/setup-flutterfire/**' - '.github/workflows/scripts/**' workflow_call: @@ -29,567 +42,93 @@ on: nightly_test_mode: type: boolean default: false + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: + required: true + TESTS_E2E_GOOGLE_SERVICES_JSON: + required: true permissions: contents: read jobs: - # Maps changed paths to affected platforms so a Kotlin-only change runs only - # the android job, a Swift-only change only ios/macos, etc. Dart code, the - # tests themselves, firebase_core and CI plumbing affect every platform. - # Non-PR events (push to main, the nightly workflow_call) always run - # everything: the filter step is skipped and its empty outputs fall back to - # 'true' below. changes: - permissions: - contents: read - runs-on: ubuntu-latest - timeout-minutes: 5 - outputs: - android: ${{ steps.filter.outputs.android || 'true' }} - ios: ${{ steps.filter.outputs.ios || 'true' }} - macos: ${{ steps.filter.outputs.macos || 'true' }} - web: ${{ steps.filter.outputs.web || 'true' }} - windows: ${{ steps.filter.outputs.windows || 'true' }} - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - if: github.event_name == 'pull_request' - - uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 - if: github.event_name == 'pull_request' - id: filter - with: - filters: | - shared: &shared - - 'packages/firebase_core/**' - - 'packages/firebase_auth/firebase_auth/lib/**' - - 'packages/firebase_auth/firebase_auth/pubspec.yaml' - - 'packages/firebase_auth/firebase_auth_platform_interface/**' - - 'packages/firebase_auth/firebase_auth/example/integration_test/**' - - 'packages/firebase_auth/firebase_auth/example/lib/**' - - 'packages/firebase_auth/firebase_auth/example/pubspec.yaml' - - '.github/workflows/e2e_tests_auth.yaml' - - '.github/actions/setup-flutterfire/**' - - '.github/workflows/scripts/**' - android: - - *shared - - 'packages/firebase_auth/firebase_auth/android/**' - - 'packages/firebase_auth/firebase_auth/example/android/**' - ios: - - *shared - - 'packages/firebase_auth/firebase_auth/ios/**' - - 'packages/firebase_auth/firebase_auth/darwin/**' - - 'packages/firebase_auth/firebase_auth/example/ios/**' - macos: - - *shared - - 'packages/firebase_auth/firebase_auth/macos/**' - - 'packages/firebase_auth/firebase_auth/darwin/**' - - 'packages/firebase_auth/firebase_auth/example/macos/**' - web: - - *shared - - 'packages/firebase_auth/firebase_auth_web/**' - - 'packages/firebase_auth/firebase_auth/example/web/**' - windows: - - *shared - - 'packages/firebase_auth/firebase_auth/windows/**' - - 'packages/firebase_auth/firebase_auth/example/windows/**' + uses: ./.github/workflows/reusable_e2e_changes.yaml + with: + package-path: 'packages/firebase_auth/firebase_auth' + platform-interface-package: 'firebase_auth_platform_interface' + web-package: 'firebase_auth_web' + inject-config-secrets: true android: needs: changes if: needs.changes.outputs.android == 'true' - permissions: - contents: read - runs-on: ubuntu-latest - timeout-minutes: ${{ inputs.nightly_test_mode && 5 || 25 }} - env: - AVD_ARCH: x86_64 - AVD_API_LEVEL: 34 - AVD_TARGET: google_apis - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - - uses: ./.github/actions/setup-flutterfire - with: - firebase-tools-version: '15.25.1' - bootstrap-scope: 'firebase_auth*' - - name: Firebase Emulator Cache - id: firebase-emulator-cache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - # Must match the save path exactly - path: ~/.cache/firebase/emulators - # Shared with the platform workflows: same emulator payload, same key. - key: firebase-emulators-v5-${{ env.FIREBASE_TOOLS_VERSION }} - # Trailing dash so this never prefix-matches a future version's key - restore-keys: firebase-emulators-v5- - - name: Generate dummy Firebase configs - # `--auth-native` is required here: the example's Android app applies - # the `google-services` plugin, which fails the build when - # google-services.json is missing. - run: dart ./.github/workflows/scripts/generate-dummy-firebase-configs.dart --auth-native - - name: Install Cloud Functions dependencies - # `firebase emulators:exec` does not install these itself (the old - # start-firebase-emulator.sh wrapper did); without them the functions - # emulator fails to load the function definitions. - run: npm ci --prefix .github/workflows/scripts/functions - - name: Enable KVM - run: | - echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules - sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm - - name: Gradle cache - uses: gradle/actions/setup-gradle@90ddb51e90a5fd9ba75f40cf85156b7b41bf76a3 - - name: Free Disk Space (Ubuntu) - uses: AdityaGarg8/remove-unwanted-software@90e01b21170618765a73370fcc3abbd1684a7793 - with: - remove-dotnet: true - remove-haskell: true - remove-codeql: true - remove-docker-images: true - remove-large-packages: true - - name: Prepare AVD home on /mnt - # GitHub-hosted runners mount a ~74GB volume at /mnt. Create it before AVD cache - # restore and android-emulator-runner (avdmanager needs the space at create time). - run: | - sudo mkdir -p /mnt/avd - sudo chown "$USER:$USER" /mnt/avd - df -h / /mnt - - name: AVD cache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - id: avd-cache - with: - # Must match the save path exactly - path: | - /mnt/avd/* - ~/.android/adb* - # Same AVD as the platform workflows, so deliberately the same key - - # this job reuses their image instead of building a second copy. - # The emulator build is part of the key so bumping `emulator-build:` below - # invalidates the cached AVD instead of reusing an image from the old build. - key: avd-${{ runner.os }}-${{ env.AVD_API_LEVEL }}-${{ env.AVD_TARGET }}-${{ env.AVD_ARCH }}-14214601 - - name: Link AVD home to /mnt - # android-emulator-runner exportVariables ANDROID_AVD_HOME to $HOME/.android/avd - run: | - mkdir -p "$HOME/.android" - rm -rf "$HOME/.android/avd" - ln -s /mnt/avd "$HOME/.android/avd" - - name: Pre-build APK - # Build outside the emulator so the AVD does not boot and idle through the - # whole Gradle build. `flutter test` below reuses this warm build cache. - working-directory: packages/firebase_auth/firebase_auth/example - timeout-minutes: 25 - run: flutter build apk --debug --target=integration_test/e2e_test.dart --android-skip-build-dependency-validation - - name: Start AVD then run E2E tests - uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a - timeout-minutes: 20 - env: - ANDROID_AVD_HOME: /mnt/avd - with: - api-level: ${{ env.AVD_API_LEVEL }} - target: ${{ env.AVD_TARGET }} - arch: ${{ env.AVD_ARCH }} - emulator-build: 14214601 - # The default (true) wipes and recreates the AVD, making the cache above useless. - force-avd-creation: false - # firebase.json and the emulator rule files live here, so `emulators:exec` - # has to run from this directory; the test command cds back to the - # package under test. - working-directory: .github/workflows/scripts - # `emulators:exec` owns the emulator lifecycle: it boots the suite, runs - # the command and tears the suite down, exiting with the command's exit - # code. Nothing is left running between steps. - script: | - firebase emulators:exec --project flutterfire-e2e-tests "cd ${GITHUB_WORKSPACE}/packages/firebase_auth/firebase_auth/example && flutter test integration_test/e2e_test.dart --timeout 10x --dart-define=CI=true -d emulator-5554" - - name: Ensure Appium is shut down - # Required because of below issue where emulator failing to shut down properly causes tests to fail - # https://github.com/ReactiveCircus/android-emulator-runner/issues/385 - run: | - pgrep -f appium && pkill -f appium || echo "No Appium process found" - - name: Save Firestore Emulator Cache - # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. - if: github.ref == 'refs/heads/main' - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - key: ${{ steps.firebase-emulator-cache.outputs.cache-primary-key }} - # Must match the restore path exactly - path: ~/.cache/firebase/emulators - - name: Save Android Emulator Cache - # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. - # Skip on a cache hit: the AVD image is multi-GB and re-uploading it unchanged on - # every main run is pure waste. - if: github.ref == 'refs/heads/main' && steps.avd-cache.outputs.cache-hit != 'true' - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - with: - key: ${{ steps.avd-cache.outputs.cache-primary-key }} - # Must match the restore path exactly - path: | - /mnt/avd/* - ~/.android/adb* + uses: ./.github/workflows/reusable_e2e_android.yaml + with: + package-path: 'packages/firebase_auth/firebase_auth' + package-scope: 'firebase_auth*' + inject-config-secrets: true + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} ios: needs: changes if: needs.changes.outputs.ios == 'true' - permissions: - contents: read - runs-on: macos-15 - timeout-minutes: ${{ inputs.nightly_test_mode && 5 || 35 }} - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - - name: Xcode - # Firebase iOS SDK: minimum Xcode 26.2. - run: sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer - - uses: ./.github/actions/setup-flutterfire - with: - flutter-version: '3.41.9' - firebase-tools-version: '15.25.1' - bootstrap-scope: 'firebase_auth*' - # This job is the repository's iOS Swift Package Manager coverage for - # firebase_auth. Every plugin the example depends on ships a - # Package.swift (flutter_facebook_auth since 7.2.0, which the example now - # requires). The macOS job below stays on CocoaPods, so both dependency - # managers are exercised. - - name: Enable Swift Package Manager for iOS - run: flutter config --enable-swift-package-manager - - uses: hendrikmuhs/ccache-action@d62db5f07c26379fc4b4e0916f098a92573c3b03 - name: Xcode Compile Cache - with: - # Distinct from every other macos-15 job's key, otherwise the workflows - # clobber each other's cache. - key: xcode-ccache-auth-ios - save: "${{ github.ref == 'refs/heads/main' }}" - max-size: 700M - - name: Firebase Emulator Cache - id: firebase-emulator-cache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - # Must match the save path exactly - path: ~/.cache/firebase/emulators - key: firebase-emulators-v5-${{ env.FIREBASE_TOOLS_VERSION }} - # Trailing dash so this never prefix-matches a future version's key - restore-keys: firebase-emulators-v5- - - name: Generate dummy Firebase configs - # `--auth-native` is required here: the example's Xcode project lists - # GoogleService-Info.plist in its Resources build phase, so the build - # fails when the file is missing. - run: dart ./.github/workflows/scripts/generate-dummy-firebase-configs.dart --auth-native - - name: Prepare iOS project for Swift Package Manager - # Done here rather than in the repository: the committed Podfile is what - # CocoaPods users of the example rely on, and `flutter build` prefers - # CocoaPods whenever a Podfile is present. - working-directory: packages/firebase_auth/firebase_auth/example/ios - run: | - if [ -f Podfile ]; then pod deintegrate; fi - rm -f Podfile Podfile.lock - rm -rf Pods - - name: 'Build Application' - working-directory: packages/firebase_auth/firebase_auth/example - timeout-minutes: 25 - run: | - export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH" - export CCACHE_SLOPPINESS=clang_index_store,file_stat_matches,include_file_ctime,include_file_mtime,ivfsoverlay,pch_defines,modules,system_headers,time_macros - export CCACHE_FILECLONE=true - export CCACHE_DEPEND=true - export CCACHE_INODECACHE=true - ccache -s - flutter build ios --no-codesign --simulator --debug --target=./integration_test/e2e_test.dart --dart-define=CI=true - ccache -s - - name: Install Cloud Functions dependencies - # `firebase emulators:exec` does not install these itself (the old - # start-firebase-emulator.sh wrapper did); without them the functions - # emulator fails to load the function definitions. - # Chown the npm cache directory to the runner user to avoid permission issues. - run: sudo chown -R 501:20 "/Users/runner/.npm" && npm ci --prefix .github/workflows/scripts/functions - - uses: futureware-tech/simulator-action@e89aa8f93d3aec35083ff49d2854d07f7186f7f5 - id: simulator - with: - # List of available simulators: https://github.com/actions/runner-images/blob/main/images/macos/macos-15-Readme.md#installed-simulators - model: "iPhone 16" - - name: Ensure Simulator Ready - timeout-minutes: 13 - env: - SIMULATOR: ${{ steps.simulator.outputs.udid }} - ENSURE_BOOT_IF_NEEDED: "0" - run: .github/workflows/scripts/ensure-simulator-ready.sh - - name: 'E2E Tests' - # firebase.json and the emulator rule files live here, so `emulators:exec` - # has to run from this directory; the test command cds back to the package - # under test. - working-directory: ./.github/workflows/scripts - # Covers booting the emulator suite as well as the tests themselves, since - # `emulators:exec` now owns both. - timeout-minutes: 20 - env: - SIMULATOR: ${{ steps.simulator.outputs.udid }} - run: | - # `emulators:exec` boots the suite, runs the command and tears the suite - # down, exiting with the command's exit code. - firebase emulators:exec --project flutterfire-e2e-tests "cd ${GITHUB_WORKSPACE}/packages/firebase_auth/firebase_auth/example && flutter test integration_test/e2e_test.dart -d \"$SIMULATOR\" --timeout 10x --dart-define=CI=true" - - name: Save Firestore Emulator Cache - # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. - if: github.ref == 'refs/heads/main' - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - key: ${{ steps.firebase-emulator-cache.outputs.cache-primary-key }} - # Must match the restore path exactly - path: ~/.cache/firebase/emulators + uses: ./.github/workflows/reusable_e2e_ios.yaml + with: + package-path: 'packages/firebase_auth/firebase_auth' + package-scope: 'firebase_auth*' + cache-key-suffix: 'auth' + # Derives GoogleService-Info.plist from the injected real options. The + # old committed plist carried an invalid-shape `dummy-api-key`, which + # FIRInstallations aborts on at launch. + native-config-args: '--live-tier-plist=auth' + inject-config-secrets: true + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} + macos: needs: changes if: needs.changes.outputs.macos == 'true' - permissions: - contents: read - runs-on: macos-15 - timeout-minutes: ${{ inputs.nightly_test_mode && 5 || 35 }} - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - - name: Xcode - # Firebase iOS SDK: minimum Xcode 26.2. - run: sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer - - uses: ./.github/actions/setup-flutterfire - with: - flutter-version: '3.41.9' - firebase-tools-version: '15.25.1' - bootstrap-scope: 'firebase_auth*' - - uses: hendrikmuhs/ccache-action@d62db5f07c26379fc4b4e0916f098a92573c3b03 - name: Xcode Compile Cache - with: - # Distinct from every other macos-15 job's key, otherwise the workflows - # clobber each other's cache. - key: xcode-ccache-auth-macos - save: "${{ github.ref == 'refs/heads/main' }}" - max-size: 700M - - name: Pods Cache - continue-on-error: true - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - id: pods-cache - with: - # Must match the save path exactly - path: packages/firebase_auth/firebase_auth/example/macos/Pods - # Keyed on the Podfile and the pinned Firebase SDK version, not on a - # pubspec.lock: those are gitignored, so hashFiles() returns an empty - # string and the key could never be invalidated. - key: pods-v1-${{ runner.os }}-auth-macos-${{ hashFiles('packages/firebase_auth/firebase_auth/example/macos/Podfile', 'packages/firebase_core/firebase_core/ios/firebase_sdk_version.rb') }} - restore-keys: pods-v1-${{ runner.os }}-auth-macos- - - name: Firebase Emulator Cache - id: firebase-emulator-cache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - # Must match the save path exactly - path: ~/.cache/firebase/emulators - key: firebase-emulators-v5-${{ env.FIREBASE_TOOLS_VERSION }} - # Trailing dash so this never prefix-matches a future version's key - restore-keys: firebase-emulators-v5- - - name: Generate dummy Firebase configs - # `--auth-native` is required here: the example's Xcode project lists - # GoogleService-Info.plist in its Resources build phase, so the build - # fails when the file is missing. - run: dart ./.github/workflows/scripts/generate-dummy-firebase-configs.dart --auth-native - - name: 'Build Application' - working-directory: packages/firebase_auth/firebase_auth/example - timeout-minutes: 25 - run: | - export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH" - export CCACHE_SLOPPINESS=clang_index_store,file_stat_matches,include_file_ctime,include_file_mtime,ivfsoverlay,pch_defines,modules,system_headers,time_macros - export CCACHE_FILECLONE=true - export CCACHE_DEPEND=true - export CCACHE_INODECACHE=true - ccache -s - flutter build macos --debug --target=./integration_test/e2e_test.dart --device-id=macos --dart-define=CI=true - ccache -s - - name: Install Cloud Functions dependencies - # `firebase emulators:exec` does not install these itself (the old - # start-firebase-emulator.sh wrapper did); without them the functions - # emulator fails to load the function definitions. - # Chown the npm cache directory to the runner user to avoid permission issues. - run: sudo chown -R 501:20 "/Users/runner/.npm" && npm ci --prefix .github/workflows/scripts/functions - - name: 'E2E Tests' - # firebase.json and the emulator rule files live here, so `emulators:exec` - # has to run from this directory; the test command cds back to the package - # under test. - working-directory: ./.github/workflows/scripts - # Covers booting the emulator suite as well as the tests themselves, since - # `emulators:exec` now owns both. - timeout-minutes: 20 - run: | - # The test command is handed to `emulators:exec` as a single string; a - # quoted heredoc keeps the script below verbatim instead of forcing a - # layer of quote escaping onto it. - TEST_COMMAND=$(cat <<'EOF' - cd "${GITHUB_WORKSPACE}/packages/firebase_auth/firebase_auth/example" - # flutter test on macOS CI may exit 1 due to "Failed to foreground app" - # even when all tests pass. Check actual test results to determine success. - set +e - OUTPUT=$(flutter test \ - integration_test/e2e_test.dart \ - -d macos \ - --dart-define=CI=true \ - --timeout 10x 2>&1) - EXIT_CODE=$? - echo "$OUTPUT" - if [ $EXIT_CODE -ne 0 ]; then - if echo "$OUTPUT" | grep -q "Some tests failed" || echo "$OUTPUT" | grep -q "test failed"; then - exit 1 - fi - if echo "$OUTPUT" | grep -q "tests passed"; then - echo "All tests passed but flutter test exited with $EXIT_CODE (likely 'Failed to foreground app'). Treating as success." - exit 0 - fi - exit $EXIT_CODE - fi - EOF - ) - # `emulators:exec` boots the suite, runs the command and tears the suite - # down, exiting with the command's exit code - so the laundering above - # still decides whether this step passes. - firebase emulators:exec --project flutterfire-e2e-tests "$TEST_COMMAND" - - name: Save Firestore Emulator Cache - continue-on-error: true - # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. - if: github.ref == 'refs/heads/main' - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - key: ${{ steps.firebase-emulator-cache.outputs.cache-primary-key }} - # Must match the restore path exactly - path: ~/.cache/firebase/emulators - - name: Save Pods Cache - continue-on-error: true - # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. - if: github.ref == 'refs/heads/main' - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - with: - key: ${{ steps.pods-cache.outputs.cache-primary-key }} - # Must match the restore path exactly - path: packages/firebase_auth/firebase_auth/example/macos/Pods + uses: ./.github/workflows/reusable_e2e_macos.yaml + with: + package-path: 'packages/firebase_auth/firebase_auth' + package-scope: 'firebase_auth*' + cache-key-suffix: 'auth' + # Same plist derivation as the iOS job above. + native-config-args: '--live-tier-plist=auth' + inject-config-secrets: true + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} web: needs: changes if: needs.changes.outputs.web == 'true' - permissions: - contents: read - runs-on: ubuntu-latest - timeout-minutes: ${{ inputs.nightly_test_mode && 5 || 25 }} - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - - uses: ./.github/actions/setup-flutterfire - timeout-minutes: 15 - with: - firebase-tools-version: '15.25.1' - bootstrap-scope: 'firebase_auth*' - # The ubuntu runner image ships Google Chrome and a chromedriver build - # that is matched to it — that pairing IS our pinning strategy, so we use - # the image binaries rather than downloading our own. - - name: 'Set up Chrome and chromedriver' - run: | - echo "$CHROMEWEBDRIVER" >> "$GITHUB_PATH" - google-chrome --version - "$CHROMEWEBDRIVER/chromedriver" --version - - name: Generate dummy Firebase configs - run: dart ./.github/workflows/scripts/generate-dummy-firebase-configs.dart - - name: Firebase Emulator Cache - id: firebase-emulator-cache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - # Must match the save path exactly - path: ~/.cache/firebase/emulators - key: firebase-emulators-v5-${{ env.FIREBASE_TOOLS_VERSION }} - restore-keys: firebase-emulators-v5- - - name: Install Cloud Functions dependencies - # `firebase emulators:exec` does not install these itself (the old - # start-firebase-emulator.sh wrapper did); without them the functions - # emulator fails to load the function definitions. - run: npm ci --prefix .github/workflows/scripts/functions - - name: 'E2E Tests' - # Covers booting the emulator suite as well as the tests themselves, since - # `emulators:exec` now owns both. - timeout-minutes: 20 - # firebase.json and the emulator rule files live here, so `emulators:exec` - # has to run from this directory; the test command cds back to the - # package under test. - working-directory: ./.github/workflows/scripts - # Web devices are not supported for the `flutter test` command yet. As a - # workaround we can use the `flutter drive` command. Tracking issue: - # https://github.com/flutter/flutter/issues/66264 - # The retry script only retries infrastructure startup failures - # (timeouts, AppConnectionException, "Failed to exit Chromium"); real - # test/compile failures fail fast. It also owns the chromedriver - # lifecycle, so nothing here starts chromedriver. - # The retry script reads its FLUTTER_DRIVE_* configuration from the - # environment, which `emulators:exec` passes through to the command. - env: - FLUTTER_DRIVE_TARGET: './integration_test/e2e_test.dart' - FLUTTER_DRIVE_DRIVER: './test_driver/integration_test.dart' - FLUTTER_DRIVE_EXTRA_ARGS: '--dart-define=CI=true' - run: | - firebase emulators:exec --project flutterfire-e2e-tests "cd ${GITHUB_WORKSPACE}/packages/firebase_auth/firebase_auth/example && ${GITHUB_WORKSPACE}/.github/workflows/scripts/flutter-drive-web-retry.sh" - - name: Save Firestore Emulator Cache - # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. - if: github.ref == 'refs/heads/main' - continue-on-error: true - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - key: ${{ steps.firebase-emulator-cache.outputs.cache-primary-key }} - # Must match the restore path exactly - path: ~/.cache/firebase/emulators + uses: ./.github/workflows/reusable_e2e_web.yaml + with: + package-path: 'packages/firebase_auth/firebase_auth' + package-scope: 'firebase_auth*' + inject-config-secrets: true + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} windows: needs: changes - # Auth used to get its Windows coverage from the `tests` app's aggregated - # `e2e_test.dart` (whose windows branch ran core/auth/remote_config/app_check); - # the auth entry is gone from there, so the coverage moves here with the rest - # of the suite. Skipped in nightly test mode, where the 5 minute budget - # cannot fit a Windows build (same as `windows-firestore`). - if: ${{ !inputs.nightly_test_mode && needs.changes.outputs.windows == 'true' }} - permissions: - contents: read - runs-on: windows-latest - timeout-minutes: 45 - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - - uses: ./.github/actions/setup-flutterfire - with: - npm-cache: 'false' - bootstrap-scope: 'firebase_auth*' - - name: Generate dummy Firebase configs - run: dart ./.github/workflows/scripts/generate-dummy-firebase-configs.dart - - name: "Install Tools" - # Not the composite action's firebase-tools install: that one uses `sudo npm`, - # which does not exist on the Windows runners. - run: | - npm install -g firebase-tools@15.25.1 - - name: Install Cloud Functions dependencies - # `firebase emulators:exec` does not install these itself; without them the - # functions emulator logs "Failed to load function definition" on every run. - run: npm ci --prefix .github/workflows/scripts/functions - - name: Start Firebase Emulator and run tests - timeout-minutes: 30 - run: | - cd ./.github/workflows/scripts - firebase emulators:exec --project flutterfire-e2e-tests "cd ../../../packages/firebase_auth/firebase_auth/example && flutter drive --target=.\integration_test\e2e_test.dart --driver=.\test_driver\integration_test.dart -d windows --verbose" 2>&1 | Tee-Object -FilePath output.log - $exitCode = $LASTEXITCODE - $output = Get-Content output.log -Raw - if ($output -match '\[E\]' -or $output -match 'Some tests failed') { - Write-Error "All tests did not pass. Please check the logs for more information." - exit 1 - } - exit $exitCode + if: needs.changes.outputs.windows == 'true' + uses: ./.github/workflows/reusable_e2e_windows.yaml + with: + package-path: 'packages/firebase_auth/firebase_auth' + package-scope: 'firebase_auth*' + inject-config-secrets: true + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} diff --git a/.github/workflows/e2e_tests_crashlytics.yaml b/.github/workflows/e2e_tests_crashlytics.yaml new file mode 100644 index 000000000000..8212b63f28c4 --- /dev/null +++ b/.github/workflows/e2e_tests_crashlytics.yaml @@ -0,0 +1,103 @@ +name: e2e-crashlytics + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-crashlytics + cancel-in-progress: true + +# Deliberately CocoaPods on iOS as well as macOS: the example's Xcode project +# runs `"${PODS_ROOT}/FirebaseCrashlytics/run"` as a build phase, and PODS_ROOT +# does not exist under Swift Package Manager. +# +# Thin caller: one job per platform this product supports, each delegating +# to the matching `reusable_e2e_.yaml`. Platforms the product does +# not support have no job here, so they render no check at all. +# Path-filtered so it only runs when this product or its dependencies change; +# the nightly workflow_call still runs it unconditionally. +on: + pull_request: + paths: + - 'packages/firebase_crashlytics/**' + - 'packages/firebase_core/**' + - '.github/workflows/e2e_tests_crashlytics.yaml' + - '.github/workflows/reusable_e2e_*.yaml' + - '.github/actions/setup-flutterfire/**' + - '.github/workflows/scripts/**' + push: + branches: + - main + paths: + - 'packages/firebase_crashlytics/**' + - 'packages/firebase_core/**' + - '.github/workflows/e2e_tests_crashlytics.yaml' + - '.github/workflows/reusable_e2e_*.yaml' + - '.github/actions/setup-flutterfire/**' + - '.github/workflows/scripts/**' + workflow_call: + inputs: + nightly_test_mode: + type: boolean + default: false + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: + required: true + TESTS_E2E_GOOGLE_SERVICES_JSON: + required: true + +permissions: + contents: read + +jobs: + changes: + uses: ./.github/workflows/reusable_e2e_changes.yaml + with: + package-path: 'packages/firebase_crashlytics/firebase_crashlytics' + platform-interface-package: 'firebase_crashlytics_platform_interface' + inject-config-secrets: true + + android: + needs: changes + if: needs.changes.outputs.android == 'true' + uses: ./.github/workflows/reusable_e2e_android.yaml + with: + package-path: 'packages/firebase_crashlytics/firebase_crashlytics' + package-scope: 'firebase_crashlytics*' + native-config-args: '--live-tier-plist=crashlytics' + inject-config-secrets: true + use-firebase-emulators: false + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} + + ios: + needs: changes + if: needs.changes.outputs.ios == 'true' + uses: ./.github/workflows/reusable_e2e_ios.yaml + with: + package-path: 'packages/firebase_crashlytics/firebase_crashlytics' + package-scope: 'firebase_crashlytics*' + cache-key-suffix: 'crashlytics' + ios-spm: false + native-config-args: '--live-tier-plist=crashlytics' + inject-config-secrets: true + use-firebase-emulators: false + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} + + macos: + needs: changes + if: needs.changes.outputs.macos == 'true' + uses: ./.github/workflows/reusable_e2e_macos.yaml + with: + package-path: 'packages/firebase_crashlytics/firebase_crashlytics' + package-scope: 'firebase_crashlytics*' + cache-key-suffix: 'crashlytics' + native-config-args: '--live-tier-plist=crashlytics' + inject-config-secrets: true + use-firebase-emulators: false + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} diff --git a/.github/workflows/e2e_tests_database.yaml b/.github/workflows/e2e_tests_database.yaml index 21b83a6fb7d2..fd100936a7c4 100644 --- a/.github/workflows/e2e_tests_database.yaml +++ b/.github/workflows/e2e_tests_database.yaml @@ -4,15 +4,21 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }}-database cancel-in-progress: true +# Emulator tier: every job runs under `firebase emulators:exec`, so no live +# project credentials are needed. +# +# Thin caller: one job per platform this product supports, each delegating +# to the matching `reusable_e2e_.yaml`. Platforms the product does +# not support have no job here, so they render no check at all. +# Path-filtered so it only runs when this product or its dependencies change; +# the nightly workflow_call still runs it unconditionally. on: pull_request: - # Database e2e only exercises this package; running it on every PR costs 4 - # jobs (2 macOS) for changes that cannot affect it. The nightly - # workflow_call still runs it unconditionally. paths: - 'packages/firebase_database/**' - 'packages/firebase_core/**' - '.github/workflows/e2e_tests_database.yaml' + - '.github/workflows/reusable_e2e_*.yaml' - '.github/actions/setup-flutterfire/**' - '.github/workflows/scripts/**' push: @@ -22,6 +28,7 @@ on: - 'packages/firebase_database/**' - 'packages/firebase_core/**' - '.github/workflows/e2e_tests_database.yaml' + - '.github/workflows/reusable_e2e_*.yaml' - '.github/actions/setup-flutterfire/**' - '.github/workflows/scripts/**' workflow_call: @@ -34,518 +41,51 @@ permissions: contents: read jobs: - # Maps changed paths to affected platforms so a Kotlin-only change runs only - # the android job, a Swift-only change only ios/macos, etc. Dart code, the - # tests themselves, firebase_core and CI plumbing affect every platform. - # Non-PR events (push to main, the nightly workflow_call) always run - # everything: the filter step is skipped and its empty outputs fall back to - # 'true' below. changes: - permissions: - contents: read - runs-on: ubuntu-latest - timeout-minutes: 5 - outputs: - android: ${{ steps.filter.outputs.android || 'true' }} - ios: ${{ steps.filter.outputs.ios || 'true' }} - macos: ${{ steps.filter.outputs.macos || 'true' }} - web: ${{ steps.filter.outputs.web || 'true' }} - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - if: github.event_name == 'pull_request' - - uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 - if: github.event_name == 'pull_request' - id: filter - with: - filters: | - shared: &shared - - 'packages/firebase_core/**' - - 'packages/firebase_database/firebase_database/lib/**' - - 'packages/firebase_database/firebase_database/pubspec.yaml' - - 'packages/firebase_database/firebase_database_platform_interface/**' - - 'packages/firebase_database/firebase_database/example/integration_test/**' - - 'packages/firebase_database/firebase_database/example/lib/**' - - 'packages/firebase_database/firebase_database/example/pubspec.yaml' - - '.github/workflows/e2e_tests_database.yaml' - - '.github/actions/setup-flutterfire/**' - - '.github/workflows/scripts/**' - android: - - *shared - - 'packages/firebase_database/firebase_database/android/**' - - 'packages/firebase_database/firebase_database/example/android/**' - ios: - - *shared - - 'packages/firebase_database/firebase_database/ios/**' - - 'packages/firebase_database/firebase_database/darwin/**' - - 'packages/firebase_database/firebase_database/example/ios/**' - macos: - - *shared - - 'packages/firebase_database/firebase_database/macos/**' - - 'packages/firebase_database/firebase_database/darwin/**' - - 'packages/firebase_database/firebase_database/example/macos/**' - web: - - *shared - - 'packages/firebase_database/firebase_database_web/**' - - 'packages/firebase_database/firebase_database/example/web/**' + uses: ./.github/workflows/reusable_e2e_changes.yaml + with: + package-path: 'packages/firebase_database/firebase_database' + platform-interface-package: 'firebase_database_platform_interface' + web-package: 'firebase_database_web' android: needs: changes if: needs.changes.outputs.android == 'true' - permissions: - contents: read - runs-on: ubuntu-latest - timeout-minutes: ${{ inputs.nightly_test_mode && 5 || 25 }} - env: - AVD_ARCH: x86_64 - AVD_API_LEVEL: 34 - AVD_TARGET: google_apis - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - - uses: ./.github/actions/setup-flutterfire - with: - firebase-tools-version: '15.25.1' - bootstrap-scope: 'firebase_database*' - - name: Firebase Emulator Cache - id: firebase-emulator-cache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - # Must match the save path exactly - path: ~/.cache/firebase/emulators - # Shared with the platform workflows: same emulator payload, same key. - key: firebase-emulators-v5-${{ env.FIREBASE_TOOLS_VERSION }} - # Trailing dash so this never prefix-matches a future version's key - restore-keys: firebase-emulators-v5- - - name: Generate dummy Firebase configs - # `--database-native` is required here: the example's Android app applies - # the `google-services` plugin, which fails the build when - # google-services.json is missing. - run: dart ./.github/workflows/scripts/generate-dummy-firebase-configs.dart --database-native - - name: Install Cloud Functions dependencies - # `firebase emulators:exec` does not install these itself (the old - # start-firebase-emulator.sh wrapper did); without them the functions - # emulator fails to load the function definitions. - run: npm ci --prefix .github/workflows/scripts/functions - - name: Enable KVM - run: | - echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules - sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm - - name: Gradle cache - uses: gradle/actions/setup-gradle@90ddb51e90a5fd9ba75f40cf85156b7b41bf76a3 - - name: Free Disk Space (Ubuntu) - uses: AdityaGarg8/remove-unwanted-software@90e01b21170618765a73370fcc3abbd1684a7793 - with: - remove-dotnet: true - remove-haskell: true - remove-codeql: true - remove-docker-images: true - remove-large-packages: true - - name: Prepare AVD home on /mnt - # GitHub-hosted runners mount a ~74GB volume at /mnt. Create it before AVD cache - # restore and android-emulator-runner (avdmanager needs the space at create time). - run: | - sudo mkdir -p /mnt/avd - sudo chown "$USER:$USER" /mnt/avd - df -h / /mnt - - name: AVD cache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - id: avd-cache - with: - # Must match the save path exactly - path: | - /mnt/avd/* - ~/.android/adb* - # Same AVD as the platform workflows, so deliberately the same key - - # this job reuses their image instead of building a second copy. - # The emulator build is part of the key so bumping `emulator-build:` below - # invalidates the cached AVD instead of reusing an image from the old build. - key: avd-${{ runner.os }}-${{ env.AVD_API_LEVEL }}-${{ env.AVD_TARGET }}-${{ env.AVD_ARCH }}-14214601 - - name: Link AVD home to /mnt - # android-emulator-runner exportVariables ANDROID_AVD_HOME to $HOME/.android/avd - run: | - mkdir -p "$HOME/.android" - rm -rf "$HOME/.android/avd" - ln -s /mnt/avd "$HOME/.android/avd" - - name: Pre-build APK - # Build outside the emulator so the AVD does not boot and idle through the - # whole Gradle build. `flutter test` below reuses this warm build cache. - working-directory: packages/firebase_database/firebase_database/example - timeout-minutes: 25 - run: flutter build apk --debug --target=integration_test/e2e_test.dart --android-skip-build-dependency-validation - - name: Start AVD then run E2E tests - uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a - timeout-minutes: 20 - env: - ANDROID_AVD_HOME: /mnt/avd - with: - api-level: ${{ env.AVD_API_LEVEL }} - target: ${{ env.AVD_TARGET }} - arch: ${{ env.AVD_ARCH }} - emulator-build: 14214601 - # The default (true) wipes and recreates the AVD, making the cache above useless. - force-avd-creation: false - # firebase.json and the emulator rule files live here, so `emulators:exec` - # has to run from this directory; the test command cds back to the - # package under test. - working-directory: .github/workflows/scripts - # `emulators:exec` owns the emulator lifecycle: it boots the suite, runs - # the command and tears the suite down, exiting with the command's exit - # code. Nothing is left running between steps. - script: | - firebase emulators:exec --project flutterfire-e2e-tests "cd ${GITHUB_WORKSPACE}/packages/firebase_database/firebase_database/example && flutter test integration_test/e2e_test.dart --timeout 10x --dart-define=CI=true -d emulator-5554" - - name: Ensure Appium is shut down - # Required because of below issue where emulator failing to shut down properly causes tests to fail - # https://github.com/ReactiveCircus/android-emulator-runner/issues/385 - run: | - pgrep -f appium && pkill -f appium || echo "No Appium process found" - - name: Save Firestore Emulator Cache - # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. - if: github.ref == 'refs/heads/main' - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - key: ${{ steps.firebase-emulator-cache.outputs.cache-primary-key }} - # Must match the restore path exactly - path: ~/.cache/firebase/emulators - - name: Save Android Emulator Cache - # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. - # Skip on a cache hit: the AVD image is multi-GB and re-uploading it unchanged on - # every main run is pure waste. - if: github.ref == 'refs/heads/main' && steps.avd-cache.outputs.cache-hit != 'true' - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - with: - key: ${{ steps.avd-cache.outputs.cache-primary-key }} - # Must match the restore path exactly - path: | - /mnt/avd/* - ~/.android/adb* + uses: ./.github/workflows/reusable_e2e_android.yaml + with: + package-path: 'packages/firebase_database/firebase_database' + package-scope: 'firebase_database*' + native-config-args: '--database-native' + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} ios: needs: changes if: needs.changes.outputs.ios == 'true' - permissions: - contents: read - runs-on: macos-15 - timeout-minutes: ${{ inputs.nightly_test_mode && 5 || 35 }} - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - - name: Xcode - # Firebase iOS SDK: minimum Xcode 26.2. - run: sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer - - uses: ./.github/actions/setup-flutterfire - with: - flutter-version: '3.41.9' - firebase-tools-version: '15.25.1' - bootstrap-scope: 'firebase_database*' - # This job is the repository's iOS Swift Package Manager coverage for - # firebase_database. Both plugins the example depends on (firebase_core, - # firebase_database) ship a Package.swift, so the whole app resolves under - # SPM. The macOS job below stays on CocoaPods, so both dependency managers - # are exercised. - - name: Enable Swift Package Manager for iOS - run: flutter config --enable-swift-package-manager - - uses: hendrikmuhs/ccache-action@d62db5f07c26379fc4b4e0916f098a92573c3b03 - name: Xcode Compile Cache - with: - # Distinct from every other macos-15 job's key, otherwise the workflows - # clobber each other's cache. - key: xcode-ccache-database-ios - save: "${{ github.ref == 'refs/heads/main' }}" - max-size: 700M - - name: Firebase Emulator Cache - id: firebase-emulator-cache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - # Must match the save path exactly - path: ~/.cache/firebase/emulators - key: firebase-emulators-v5-${{ env.FIREBASE_TOOLS_VERSION }} - # Trailing dash so this never prefix-matches a future version's key - restore-keys: firebase-emulators-v5- - - name: Generate dummy Firebase configs - # The iOS Runner target does not list GoogleService-Info.plist in a - # Resources build phase, so this is not strictly load-bearing here - - # but the generator writes both Apple targets in one shot and the file - # is what a real user's project would carry. - run: dart ./.github/workflows/scripts/generate-dummy-firebase-configs.dart --database-native - - name: Prepare iOS project for Swift Package Manager - # Done here rather than in the repository: the committed Podfile is what - # CocoaPods users of the example rely on, and `flutter build` prefers - # CocoaPods whenever a Podfile is present. - working-directory: packages/firebase_database/firebase_database/example/ios - run: | - if [ -f Podfile ]; then pod deintegrate; fi - rm -f Podfile Podfile.lock - rm -rf Pods - - name: 'Build Application' - working-directory: packages/firebase_database/firebase_database/example - timeout-minutes: 25 - run: | - export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH" - export CCACHE_SLOPPINESS=clang_index_store,file_stat_matches,include_file_ctime,include_file_mtime,ivfsoverlay,pch_defines,modules,system_headers,time_macros - export CCACHE_FILECLONE=true - export CCACHE_DEPEND=true - export CCACHE_INODECACHE=true - ccache -s - flutter build ios --no-codesign --simulator --debug --target=./integration_test/e2e_test.dart --dart-define=CI=true - ccache -s - - name: Install Cloud Functions dependencies - # `firebase emulators:exec` does not install these itself (the old - # start-firebase-emulator.sh wrapper did); without them the functions - # emulator fails to load the function definitions. - # Chown the npm cache directory to the runner user to avoid permission issues. - run: sudo chown -R 501:20 "/Users/runner/.npm" && npm ci --prefix .github/workflows/scripts/functions - - uses: futureware-tech/simulator-action@e89aa8f93d3aec35083ff49d2854d07f7186f7f5 - id: simulator - with: - # List of available simulators: https://github.com/actions/runner-images/blob/main/images/macos/macos-15-Readme.md#installed-simulators - model: "iPhone 16" - - name: Ensure Simulator Ready - timeout-minutes: 13 - env: - SIMULATOR: ${{ steps.simulator.outputs.udid }} - ENSURE_BOOT_IF_NEEDED: "0" - run: .github/workflows/scripts/ensure-simulator-ready.sh - - name: 'E2E Tests' - # firebase.json and the emulator rule files live here, so `emulators:exec` - # has to run from this directory; the test command cds back to the package - # under test. - working-directory: ./.github/workflows/scripts - # Covers booting the emulator suite as well as the tests themselves, since - # `emulators:exec` now owns both. - timeout-minutes: 20 - env: - SIMULATOR: ${{ steps.simulator.outputs.udid }} - run: | - # `emulators:exec` boots the suite, runs the command and tears the suite - # down, exiting with the command's exit code. - firebase emulators:exec --project flutterfire-e2e-tests "cd ${GITHUB_WORKSPACE}/packages/firebase_database/firebase_database/example && flutter test integration_test/e2e_test.dart -d \"$SIMULATOR\" --timeout 10x --dart-define=CI=true" - - name: Save Firestore Emulator Cache - # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. - if: github.ref == 'refs/heads/main' - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - key: ${{ steps.firebase-emulator-cache.outputs.cache-primary-key }} - # Must match the restore path exactly - path: ~/.cache/firebase/emulators + uses: ./.github/workflows/reusable_e2e_ios.yaml + with: + package-path: 'packages/firebase_database/firebase_database' + package-scope: 'firebase_database*' + cache-key-suffix: 'database' + native-config-args: '--database-native' + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} macos: needs: changes if: needs.changes.outputs.macos == 'true' - permissions: - contents: read - runs-on: macos-15 - timeout-minutes: ${{ inputs.nightly_test_mode && 5 || 35 }} - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - - name: Xcode - # Firebase iOS SDK: minimum Xcode 26.2. - run: sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer - - uses: ./.github/actions/setup-flutterfire - with: - flutter-version: '3.41.9' - firebase-tools-version: '15.25.1' - bootstrap-scope: 'firebase_database*' - - uses: hendrikmuhs/ccache-action@d62db5f07c26379fc4b4e0916f098a92573c3b03 - name: Xcode Compile Cache - with: - # Distinct from every other macos-15 job's key, otherwise the workflows - # clobber each other's cache. - key: xcode-ccache-database-macos - save: "${{ github.ref == 'refs/heads/main' }}" - max-size: 700M - - name: Pods Cache - continue-on-error: true - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - id: pods-cache - with: - # Must match the save path exactly - path: packages/firebase_database/firebase_database/example/macos/Pods - # Keyed on the Podfile and the pinned Firebase SDK version, not on a - # pubspec.lock: those are gitignored, so hashFiles() returns an empty - # string and the key could never be invalidated. - key: pods-v1-${{ runner.os }}-database-macos-${{ hashFiles('packages/firebase_database/firebase_database/example/macos/Podfile', 'packages/firebase_core/firebase_core/ios/firebase_sdk_version.rb') }} - restore-keys: pods-v1-${{ runner.os }}-database-macos- - - name: Firebase Emulator Cache - id: firebase-emulator-cache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - # Must match the save path exactly - path: ~/.cache/firebase/emulators - key: firebase-emulators-v5-${{ env.FIREBASE_TOOLS_VERSION }} - # Trailing dash so this never prefix-matches a future version's key - restore-keys: firebase-emulators-v5- - - name: Generate dummy Firebase configs - # `--database-native` is required here: the example's Xcode project lists - # GoogleService-Info.plist in its Resources build phase, so the build - # fails when the file is missing. - run: dart ./.github/workflows/scripts/generate-dummy-firebase-configs.dart --database-native - - name: 'Build Application' - working-directory: packages/firebase_database/firebase_database/example - timeout-minutes: 25 - run: | - export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH" - export CCACHE_SLOPPINESS=clang_index_store,file_stat_matches,include_file_ctime,include_file_mtime,ivfsoverlay,pch_defines,modules,system_headers,time_macros - export CCACHE_FILECLONE=true - export CCACHE_DEPEND=true - export CCACHE_INODECACHE=true - ccache -s - flutter build macos --debug --target=./integration_test/e2e_test.dart --device-id=macos --dart-define=CI=true - ccache -s - - name: Install Cloud Functions dependencies - # `firebase emulators:exec` does not install these itself (the old - # start-firebase-emulator.sh wrapper did); without them the functions - # emulator fails to load the function definitions. - # Chown the npm cache directory to the runner user to avoid permission issues. - run: sudo chown -R 501:20 "/Users/runner/.npm" && npm ci --prefix .github/workflows/scripts/functions - - name: 'E2E Tests' - # firebase.json and the emulator rule files live here, so `emulators:exec` - # has to run from this directory; the test command cds back to the package - # under test. - working-directory: ./.github/workflows/scripts - # Covers booting the emulator suite as well as the tests themselves, since - # `emulators:exec` now owns both. - timeout-minutes: 20 - run: | - # The test command is handed to `emulators:exec` as a single string; a - # quoted heredoc keeps the script below verbatim instead of forcing a - # layer of quote escaping onto it. - TEST_COMMAND=$(cat <<'EOF' - cd "${GITHUB_WORKSPACE}/packages/firebase_database/firebase_database/example" - # flutter test on macOS CI may exit 1 due to "Failed to foreground app" - # even when all tests pass. Check actual test results to determine success. - set +e - OUTPUT=$(flutter test \ - integration_test/e2e_test.dart \ - -d macos \ - --dart-define=CI=true \ - --timeout 10x 2>&1) - EXIT_CODE=$? - echo "$OUTPUT" - if [ $EXIT_CODE -ne 0 ]; then - if echo "$OUTPUT" | grep -q "Some tests failed" || echo "$OUTPUT" | grep -q "test failed"; then - exit 1 - fi - if echo "$OUTPUT" | grep -q "tests passed"; then - echo "All tests passed but flutter test exited with $EXIT_CODE (likely 'Failed to foreground app'). Treating as success." - exit 0 - fi - exit $EXIT_CODE - fi - EOF - ) - # `emulators:exec` boots the suite, runs the command and tears the suite - # down, exiting with the command's exit code - so the laundering above - # still decides whether this step passes. - firebase emulators:exec --project flutterfire-e2e-tests "$TEST_COMMAND" - - name: Save Firestore Emulator Cache - continue-on-error: true - # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. - if: github.ref == 'refs/heads/main' - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - key: ${{ steps.firebase-emulator-cache.outputs.cache-primary-key }} - # Must match the restore path exactly - path: ~/.cache/firebase/emulators - - name: Save Pods Cache - continue-on-error: true - # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. - if: github.ref == 'refs/heads/main' - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - with: - key: ${{ steps.pods-cache.outputs.cache-primary-key }} - # Must match the restore path exactly - path: packages/firebase_database/firebase_database/example/macos/Pods + uses: ./.github/workflows/reusable_e2e_macos.yaml + with: + package-path: 'packages/firebase_database/firebase_database' + package-scope: 'firebase_database*' + cache-key-suffix: 'database' + native-config-args: '--database-native' + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} web: needs: changes if: needs.changes.outputs.web == 'true' - permissions: - contents: read - runs-on: ubuntu-latest - timeout-minutes: ${{ inputs.nightly_test_mode && 5 || 25 }} - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - - uses: ./.github/actions/setup-flutterfire - timeout-minutes: 15 - with: - firebase-tools-version: '15.25.1' - bootstrap-scope: 'firebase_database*' - # The ubuntu runner image ships Google Chrome and a chromedriver build - # that is matched to it — that pairing IS our pinning strategy, so we use - # the image binaries rather than downloading our own. - - name: 'Set up Chrome and chromedriver' - run: | - echo "$CHROMEWEBDRIVER" >> "$GITHUB_PATH" - google-chrome --version - "$CHROMEWEBDRIVER/chromedriver" --version - - name: Generate dummy Firebase configs - run: dart ./.github/workflows/scripts/generate-dummy-firebase-configs.dart - - name: Firebase Emulator Cache - id: firebase-emulator-cache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - # Must match the save path exactly - path: ~/.cache/firebase/emulators - key: firebase-emulators-v5-${{ env.FIREBASE_TOOLS_VERSION }} - restore-keys: firebase-emulators-v5- - - name: Install Cloud Functions dependencies - # `firebase emulators:exec` does not install these itself (the old - # start-firebase-emulator.sh wrapper did); without them the functions - # emulator fails to load the function definitions. - run: npm ci --prefix .github/workflows/scripts/functions - - name: 'E2E Tests' - # Covers booting the emulator suite as well as the tests themselves, since - # `emulators:exec` now owns both. - timeout-minutes: 20 - # firebase.json and the emulator rule files live here, so `emulators:exec` - # has to run from this directory; the test command cds back to the - # package under test. - working-directory: ./.github/workflows/scripts - # Web devices are not supported for the `flutter test` command yet. As a - # workaround we can use the `flutter drive` command. Tracking issue: - # https://github.com/flutter/flutter/issues/66264 - # The retry script only retries infrastructure startup failures - # (timeouts, AppConnectionException, "Failed to exit Chromium"); real - # test/compile failures fail fast. It also owns the chromedriver - # lifecycle, so nothing here starts chromedriver. - # The retry script reads its FLUTTER_DRIVE_* configuration from the - # environment, which `emulators:exec` passes through to the command. - env: - FLUTTER_DRIVE_TARGET: './integration_test/e2e_test.dart' - FLUTTER_DRIVE_DRIVER: './test_driver/integration_test.dart' - FLUTTER_DRIVE_EXTRA_ARGS: '--dart-define=CI=true' - run: | - firebase emulators:exec --project flutterfire-e2e-tests "cd ${GITHUB_WORKSPACE}/packages/firebase_database/firebase_database/example && ${GITHUB_WORKSPACE}/.github/workflows/scripts/flutter-drive-web-retry.sh" - - name: Save Firestore Emulator Cache - # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. - if: github.ref == 'refs/heads/main' - continue-on-error: true - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - key: ${{ steps.firebase-emulator-cache.outputs.cache-primary-key }} - # Must match the restore path exactly - path: ~/.cache/firebase/emulators - + uses: ./.github/workflows/reusable_e2e_web.yaml + with: + package-path: 'packages/firebase_database/firebase_database' + package-scope: 'firebase_database*' + native-config-args: '--database-native' + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} diff --git a/.github/workflows/e2e_tests_firestore.yaml b/.github/workflows/e2e_tests_firestore.yaml new file mode 100644 index 000000000000..d5d06b3e250b --- /dev/null +++ b/.github/workflows/e2e_tests_firestore.yaml @@ -0,0 +1,120 @@ +name: e2e-firestore + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-firestore + cancel-in-progress: true + +# Emulator tier: every job runs under `firebase emulators:exec`, so no live +# project credentials are needed. +# +# The only product with a wasm job (its example ships web/wasm_index.html) and +# the only one whose iOS build needs the extra disk cleanup. +# +# Thin caller: one job per platform this product supports, each delegating +# to the matching `reusable_e2e_.yaml`. Platforms the product does +# not support have no job here, so they render no check at all. +# Path-filtered so it only runs when this product or its dependencies change; +# the nightly workflow_call still runs it unconditionally. +on: + pull_request: + paths: + - 'packages/cloud_firestore/**' + - 'packages/firebase_core/**' + - '.github/workflows/e2e_tests_firestore.yaml' + - '.github/workflows/reusable_e2e_*.yaml' + - '.github/actions/setup-flutterfire/**' + - '.github/workflows/scripts/**' + push: + branches: + - main + paths: + - 'packages/cloud_firestore/**' + - 'packages/firebase_core/**' + - '.github/workflows/e2e_tests_firestore.yaml' + - '.github/workflows/reusable_e2e_*.yaml' + - '.github/actions/setup-flutterfire/**' + - '.github/workflows/scripts/**' + workflow_call: + inputs: + nightly_test_mode: + type: boolean + default: false + +permissions: + contents: read + +jobs: + changes: + uses: ./.github/workflows/reusable_e2e_changes.yaml + with: + package-path: 'packages/cloud_firestore/cloud_firestore' + platform-interface-package: 'cloud_firestore_platform_interface' + web-package: 'cloud_firestore_web' + + android: + needs: changes + if: needs.changes.outputs.android == 'true' + uses: ./.github/workflows/reusable_e2e_android.yaml + with: + package-path: 'packages/cloud_firestore/cloud_firestore' + package-scope: 'cloud_firestore*' + native-config-args: '--firestore-native' + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + + ios: + needs: changes + if: needs.changes.outputs.ios == 'true' + uses: ./.github/workflows/reusable_e2e_ios.yaml + with: + package-path: 'packages/cloud_firestore/cloud_firestore' + package-scope: 'cloud_firestore*' + cache-key-suffix: 'firestore' + ios-free-up-space: true + native-config-args: '--firestore-native' + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + + macos: + needs: changes + if: needs.changes.outputs.macos == 'true' + uses: ./.github/workflows/reusable_e2e_macos.yaml + with: + package-path: 'packages/cloud_firestore/cloud_firestore' + package-scope: 'cloud_firestore*' + cache-key-suffix: 'firestore' + native-config-args: '--firestore-native' + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + + web: + needs: changes + if: needs.changes.outputs.web == 'true' + uses: ./.github/workflows/reusable_e2e_web.yaml + with: + package-path: 'packages/cloud_firestore/cloud_firestore' + package-scope: 'cloud_firestore*' + native-config-args: '--firestore-native' + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + + web-wasm: + needs: changes + # Gated on the same `web` filter output: a change that affects the JS + # web build affects the wasm one too. + if: needs.changes.outputs.web == 'true' + uses: ./.github/workflows/reusable_e2e_web.yaml + with: + package-path: 'packages/cloud_firestore/cloud_firestore' + package-scope: 'cloud_firestore*' + wasm: true + drive-timeout: '300' + max-attempts: '2' + native-config-args: '--firestore-native' + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + + windows: + needs: changes + if: needs.changes.outputs.windows == 'true' + uses: ./.github/workflows/reusable_e2e_windows.yaml + with: + package-path: 'packages/cloud_firestore/cloud_firestore' + package-scope: 'cloud_firestore*' + native-config-args: '--firestore-native' + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} diff --git a/.github/workflows/e2e_tests_functions.yaml b/.github/workflows/e2e_tests_functions.yaml index 5d608baa901e..701cabe3923b 100644 --- a/.github/workflows/e2e_tests_functions.yaml +++ b/.github/workflows/e2e_tests_functions.yaml @@ -4,15 +4,21 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }}-functions cancel-in-progress: true +# Emulator tier: every job runs under `firebase emulators:exec`, so no live +# project credentials are needed. +# +# Thin caller: one job per platform this product supports, each delegating +# to the matching `reusable_e2e_.yaml`. Platforms the product does +# not support have no job here, so they render no check at all. +# Path-filtered so it only runs when this product or its dependencies change; +# the nightly workflow_call still runs it unconditionally. on: pull_request: - # Functions e2e only exercises this package; running it on every PR costs 4 - # jobs (2 macOS) for changes that cannot affect it. The nightly - # workflow_call still runs it unconditionally. paths: - 'packages/cloud_functions/**' - 'packages/firebase_core/**' - '.github/workflows/e2e_tests_functions.yaml' + - '.github/workflows/reusable_e2e_*.yaml' - '.github/actions/setup-flutterfire/**' - '.github/workflows/scripts/**' push: @@ -22,6 +28,7 @@ on: - 'packages/cloud_functions/**' - 'packages/firebase_core/**' - '.github/workflows/e2e_tests_functions.yaml' + - '.github/workflows/reusable_e2e_*.yaml' - '.github/actions/setup-flutterfire/**' - '.github/workflows/scripts/**' workflow_call: @@ -34,517 +41,59 @@ permissions: contents: read jobs: - # Maps changed paths to affected platforms so a Kotlin-only change runs only - # the android job, a Swift-only change only ios/macos, etc. Dart code, the - # tests themselves, firebase_core and CI plumbing affect every platform. - # Non-PR events (push to main, the nightly workflow_call) always run - # everything: the filter step is skipped and its empty outputs fall back to - # 'true' below. changes: - permissions: - contents: read - runs-on: ubuntu-latest - timeout-minutes: 5 - outputs: - android: ${{ steps.filter.outputs.android || 'true' }} - ios: ${{ steps.filter.outputs.ios || 'true' }} - macos: ${{ steps.filter.outputs.macos || 'true' }} - web: ${{ steps.filter.outputs.web || 'true' }} - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - if: github.event_name == 'pull_request' - - uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 - if: github.event_name == 'pull_request' - id: filter - with: - filters: | - shared: &shared - - 'packages/firebase_core/**' - - 'packages/cloud_functions/cloud_functions/lib/**' - - 'packages/cloud_functions/cloud_functions/pubspec.yaml' - - 'packages/cloud_functions/cloud_functions_platform_interface/**' - - 'packages/cloud_functions/cloud_functions/example/integration_test/**' - - 'packages/cloud_functions/cloud_functions/example/lib/**' - - 'packages/cloud_functions/cloud_functions/example/pubspec.yaml' - - '.github/workflows/e2e_tests_functions.yaml' - - '.github/actions/setup-flutterfire/**' - - '.github/workflows/scripts/**' - android: - - *shared - - 'packages/cloud_functions/cloud_functions/android/**' - - 'packages/cloud_functions/cloud_functions/example/android/**' - ios: - - *shared - - 'packages/cloud_functions/cloud_functions/ios/**' - - 'packages/cloud_functions/cloud_functions/darwin/**' - - 'packages/cloud_functions/cloud_functions/example/ios/**' - macos: - - *shared - - 'packages/cloud_functions/cloud_functions/macos/**' - - 'packages/cloud_functions/cloud_functions/darwin/**' - - 'packages/cloud_functions/cloud_functions/example/macos/**' - web: - - *shared - - 'packages/cloud_functions/cloud_functions_web/**' - - 'packages/cloud_functions/cloud_functions/example/web/**' + uses: ./.github/workflows/reusable_e2e_changes.yaml + with: + package-path: 'packages/cloud_functions/cloud_functions' + platform-interface-package: 'cloud_functions_platform_interface' + web-package: 'cloud_functions_web' android: needs: changes if: needs.changes.outputs.android == 'true' - permissions: - contents: read - runs-on: ubuntu-latest - timeout-minutes: ${{ inputs.nightly_test_mode && 5 || 25 }} - env: - AVD_ARCH: x86_64 - AVD_API_LEVEL: 34 - AVD_TARGET: google_apis - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - - uses: ./.github/actions/setup-flutterfire - with: - firebase-tools-version: '15.25.1' - bootstrap-scope: 'cloud_functions*' - - name: Firebase Emulator Cache - id: firebase-emulator-cache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - # Must match the save path exactly - path: ~/.cache/firebase/emulators - # Shared with the platform workflows: same emulator payload, same key. - key: firebase-emulators-v5-${{ env.FIREBASE_TOOLS_VERSION }} - # Trailing dash so this never prefix-matches a future version's key - restore-keys: firebase-emulators-v5- - - name: Generate dummy Firebase configs - # `--functions-native` is required here: the example's Android app applies - # the `google-services` plugin, which fails the build when - # google-services.json is missing. - run: dart ./.github/workflows/scripts/generate-dummy-firebase-configs.dart --functions-native - - name: Install Cloud Functions dependencies - # `firebase emulators:exec` does not install these itself (the old - # start-firebase-emulator.sh wrapper did); without them the functions - # emulator fails to load the function definitions. - run: npm ci --prefix .github/workflows/scripts/functions - - name: Enable KVM - run: | - echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules - sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm - - name: Gradle cache - uses: gradle/actions/setup-gradle@90ddb51e90a5fd9ba75f40cf85156b7b41bf76a3 - - name: Free Disk Space (Ubuntu) - uses: AdityaGarg8/remove-unwanted-software@90e01b21170618765a73370fcc3abbd1684a7793 - with: - remove-dotnet: true - remove-haskell: true - remove-codeql: true - remove-docker-images: true - remove-large-packages: true - - name: Prepare AVD home on /mnt - # GitHub-hosted runners mount a ~74GB volume at /mnt. Create it before AVD cache - # restore and android-emulator-runner (avdmanager needs the space at create time). - run: | - sudo mkdir -p /mnt/avd - sudo chown "$USER:$USER" /mnt/avd - df -h / /mnt - - name: AVD cache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - id: avd-cache - with: - # Must match the save path exactly - path: | - /mnt/avd/* - ~/.android/adb* - # Same AVD as the platform workflows, so deliberately the same key - - # this job reuses their image instead of building a second copy. - # The emulator build is part of the key so bumping `emulator-build:` below - # invalidates the cached AVD instead of reusing an image from the old build. - key: avd-${{ runner.os }}-${{ env.AVD_API_LEVEL }}-${{ env.AVD_TARGET }}-${{ env.AVD_ARCH }}-14214601 - - name: Link AVD home to /mnt - # android-emulator-runner exportVariables ANDROID_AVD_HOME to $HOME/.android/avd - run: | - mkdir -p "$HOME/.android" - rm -rf "$HOME/.android/avd" - ln -s /mnt/avd "$HOME/.android/avd" - - name: Pre-build APK - # Build outside the emulator so the AVD does not boot and idle through the - # whole Gradle build. `flutter test` below reuses this warm build cache. - working-directory: packages/cloud_functions/cloud_functions/example - timeout-minutes: 25 - run: flutter build apk --debug --target=integration_test/e2e_test.dart --android-skip-build-dependency-validation - - name: Start AVD then run E2E tests - uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a - timeout-minutes: 20 - env: - ANDROID_AVD_HOME: /mnt/avd - with: - api-level: ${{ env.AVD_API_LEVEL }} - target: ${{ env.AVD_TARGET }} - arch: ${{ env.AVD_ARCH }} - emulator-build: 14214601 - # The default (true) wipes and recreates the AVD, making the cache above useless. - force-avd-creation: false - # firebase.json and the emulator rule files live here, so `emulators:exec` - # has to run from this directory; the test command cds back to the - # package under test. - working-directory: .github/workflows/scripts - # `emulators:exec` owns the emulator lifecycle: it boots the suite, runs - # the command and tears the suite down, exiting with the command's exit - # code. Nothing is left running between steps. - script: | - firebase emulators:exec --project flutterfire-e2e-tests "cd ${GITHUB_WORKSPACE}/packages/cloud_functions/cloud_functions/example && flutter test integration_test/e2e_test.dart --timeout 10x --dart-define=CI=true -d emulator-5554" - - name: Ensure Appium is shut down - # Required because of below issue where emulator failing to shut down properly causes tests to fail - # https://github.com/ReactiveCircus/android-emulator-runner/issues/385 - run: | - pgrep -f appium && pkill -f appium || echo "No Appium process found" - - name: Save Firestore Emulator Cache - # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. - if: github.ref == 'refs/heads/main' - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - key: ${{ steps.firebase-emulator-cache.outputs.cache-primary-key }} - # Must match the restore path exactly - path: ~/.cache/firebase/emulators - - name: Save Android Emulator Cache - # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. - # Skip on a cache hit: the AVD image is multi-GB and re-uploading it unchanged on - # every main run is pure waste. - if: github.ref == 'refs/heads/main' && steps.avd-cache.outputs.cache-hit != 'true' - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - with: - key: ${{ steps.avd-cache.outputs.cache-primary-key }} - # Must match the restore path exactly - path: | - /mnt/avd/* - ~/.android/adb* + uses: ./.github/workflows/reusable_e2e_android.yaml + with: + package-path: 'packages/cloud_functions/cloud_functions' + package-scope: 'cloud_functions*' + native-config-args: '--functions-native' + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} ios: needs: changes if: needs.changes.outputs.ios == 'true' - permissions: - contents: read - runs-on: macos-15 - timeout-minutes: ${{ inputs.nightly_test_mode && 5 || 35 }} - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - - name: Xcode - # Firebase iOS SDK: minimum Xcode 26.2. - run: sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer - - uses: ./.github/actions/setup-flutterfire - with: - flutter-version: '3.41.9' - firebase-tools-version: '15.25.1' - bootstrap-scope: 'cloud_functions*' - # This job is the repository's iOS Swift Package Manager coverage for - # cloud_functions. Both plugins the example depends on (firebase_core, - # cloud_functions) ship a Package.swift, so the whole app resolves under - # SPM. The macOS job below stays on CocoaPods, so both dependency managers - # are exercised. - - name: Enable Swift Package Manager for iOS - run: flutter config --enable-swift-package-manager - - uses: hendrikmuhs/ccache-action@d62db5f07c26379fc4b4e0916f098a92573c3b03 - name: Xcode Compile Cache - with: - # Distinct from every other macos-15 job's key, otherwise the workflows - # clobber each other's cache. - key: xcode-ccache-functions-ios - save: "${{ github.ref == 'refs/heads/main' }}" - max-size: 700M - - name: Firebase Emulator Cache - id: firebase-emulator-cache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - # Must match the save path exactly - path: ~/.cache/firebase/emulators - key: firebase-emulators-v5-${{ env.FIREBASE_TOOLS_VERSION }} - # Trailing dash so this never prefix-matches a future version's key - restore-keys: firebase-emulators-v5- - - name: Generate dummy Firebase configs - # `--functions-native` is required here: the example's Xcode project lists - # GoogleService-Info.plist in its Resources build phase, so the build - # fails when the file is missing. - run: dart ./.github/workflows/scripts/generate-dummy-firebase-configs.dart --functions-native - - name: Prepare iOS project for Swift Package Manager - # Done here rather than in the repository: the committed Podfile is what - # CocoaPods users of the example rely on, and `flutter build` prefers - # CocoaPods whenever a Podfile is present. - working-directory: packages/cloud_functions/cloud_functions/example/ios - run: | - if [ -f Podfile ]; then pod deintegrate; fi - rm -f Podfile Podfile.lock - rm -rf Pods - - name: 'Build Application' - working-directory: packages/cloud_functions/cloud_functions/example - timeout-minutes: 25 - run: | - export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH" - export CCACHE_SLOPPINESS=clang_index_store,file_stat_matches,include_file_ctime,include_file_mtime,ivfsoverlay,pch_defines,modules,system_headers,time_macros - export CCACHE_FILECLONE=true - export CCACHE_DEPEND=true - export CCACHE_INODECACHE=true - ccache -s - flutter build ios --no-codesign --simulator --debug --target=./integration_test/e2e_test.dart --dart-define=CI=true - ccache -s - - name: Install Cloud Functions dependencies - # `firebase emulators:exec` does not install these itself (the old - # start-firebase-emulator.sh wrapper did); without them the functions - # emulator fails to load the function definitions. - # Chown the npm cache directory to the runner user to avoid permission issues. - run: sudo chown -R 501:20 "/Users/runner/.npm" && npm ci --prefix .github/workflows/scripts/functions - - uses: futureware-tech/simulator-action@e89aa8f93d3aec35083ff49d2854d07f7186f7f5 - id: simulator - with: - # List of available simulators: https://github.com/actions/runner-images/blob/main/images/macos/macos-15-Readme.md#installed-simulators - model: "iPhone 16" - - name: Ensure Simulator Ready - timeout-minutes: 13 - env: - SIMULATOR: ${{ steps.simulator.outputs.udid }} - ENSURE_BOOT_IF_NEEDED: "0" - run: .github/workflows/scripts/ensure-simulator-ready.sh - - name: 'E2E Tests' - # firebase.json and the emulator rule files live here, so `emulators:exec` - # has to run from this directory; the test command cds back to the package - # under test. - working-directory: ./.github/workflows/scripts - # Covers booting the emulator suite as well as the tests themselves, since - # `emulators:exec` now owns both. - timeout-minutes: 20 - env: - SIMULATOR: ${{ steps.simulator.outputs.udid }} - run: | - # `emulators:exec` boots the suite, runs the command and tears the suite - # down, exiting with the command's exit code. - firebase emulators:exec --project flutterfire-e2e-tests "cd ${GITHUB_WORKSPACE}/packages/cloud_functions/cloud_functions/example && flutter test integration_test/e2e_test.dart -d \"$SIMULATOR\" --timeout 10x --dart-define=CI=true" - - name: Save Firestore Emulator Cache - # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. - if: github.ref == 'refs/heads/main' - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - key: ${{ steps.firebase-emulator-cache.outputs.cache-primary-key }} - # Must match the restore path exactly - path: ~/.cache/firebase/emulators + uses: ./.github/workflows/reusable_e2e_ios.yaml + with: + package-path: 'packages/cloud_functions/cloud_functions' + package-scope: 'cloud_functions*' + cache-key-suffix: 'functions' + # CocoaPods for now: Flutter's experimental SPM integration kept failing + # to resolve this example's plugin siblings ("Could not resolve package + # dependencies ... ios/firebase_core does not exist"). Dropping the Xcode + # 26.2 pin (see `reusable_e2e_ios.yaml`) is the likely fix, but one + # variable at a time - these two rejoin SPM once the default-Xcode runs + # prove clean. iOS SPM coverage is unaffected meanwhile: storage, + # database, firestore and auth still build under it. + ios-spm: false + native-config-args: '--functions-native' + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} macos: needs: changes if: needs.changes.outputs.macos == 'true' - permissions: - contents: read - runs-on: macos-15 - timeout-minutes: ${{ inputs.nightly_test_mode && 5 || 35 }} - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - - name: Xcode - # Firebase iOS SDK: minimum Xcode 26.2. - run: sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer - - uses: ./.github/actions/setup-flutterfire - with: - flutter-version: '3.41.9' - firebase-tools-version: '15.25.1' - bootstrap-scope: 'cloud_functions*' - - uses: hendrikmuhs/ccache-action@d62db5f07c26379fc4b4e0916f098a92573c3b03 - name: Xcode Compile Cache - with: - # Distinct from every other macos-15 job's key, otherwise the workflows - # clobber each other's cache. - key: xcode-ccache-functions-macos - save: "${{ github.ref == 'refs/heads/main' }}" - max-size: 700M - - name: Pods Cache - continue-on-error: true - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - id: pods-cache - with: - # Must match the save path exactly - path: packages/cloud_functions/cloud_functions/example/macos/Pods - # Keyed on the Podfile and the pinned Firebase SDK version, not on a - # pubspec.lock: those are gitignored, so hashFiles() returns an empty - # string and the key could never be invalidated. - key: pods-v1-${{ runner.os }}-functions-macos-${{ hashFiles('packages/cloud_functions/cloud_functions/example/macos/Podfile', 'packages/firebase_core/firebase_core/ios/firebase_sdk_version.rb') }} - restore-keys: pods-v1-${{ runner.os }}-functions-macos- - - name: Firebase Emulator Cache - id: firebase-emulator-cache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - # Must match the save path exactly - path: ~/.cache/firebase/emulators - key: firebase-emulators-v5-${{ env.FIREBASE_TOOLS_VERSION }} - # Trailing dash so this never prefix-matches a future version's key - restore-keys: firebase-emulators-v5- - - name: Generate dummy Firebase configs - # `--functions-native` is required here: the example's Xcode project lists - # GoogleService-Info.plist in its Resources build phase, so the build - # fails when the file is missing. - run: dart ./.github/workflows/scripts/generate-dummy-firebase-configs.dart --functions-native - - name: 'Build Application' - working-directory: packages/cloud_functions/cloud_functions/example - timeout-minutes: 25 - run: | - export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH" - export CCACHE_SLOPPINESS=clang_index_store,file_stat_matches,include_file_ctime,include_file_mtime,ivfsoverlay,pch_defines,modules,system_headers,time_macros - export CCACHE_FILECLONE=true - export CCACHE_DEPEND=true - export CCACHE_INODECACHE=true - ccache -s - flutter build macos --debug --target=./integration_test/e2e_test.dart --device-id=macos --dart-define=CI=true - ccache -s - - name: Install Cloud Functions dependencies - # `firebase emulators:exec` does not install these itself (the old - # start-firebase-emulator.sh wrapper did); without them the functions - # emulator fails to load the function definitions. - # Chown the npm cache directory to the runner user to avoid permission issues. - run: sudo chown -R 501:20 "/Users/runner/.npm" && npm ci --prefix .github/workflows/scripts/functions - - name: 'E2E Tests' - # firebase.json and the emulator rule files live here, so `emulators:exec` - # has to run from this directory; the test command cds back to the package - # under test. - working-directory: ./.github/workflows/scripts - # Covers booting the emulator suite as well as the tests themselves, since - # `emulators:exec` now owns both. - timeout-minutes: 20 - run: | - # The test command is handed to `emulators:exec` as a single string; a - # quoted heredoc keeps the script below verbatim instead of forcing a - # layer of quote escaping onto it. - TEST_COMMAND=$(cat <<'EOF' - cd "${GITHUB_WORKSPACE}/packages/cloud_functions/cloud_functions/example" - # flutter test on macOS CI may exit 1 due to "Failed to foreground app" - # even when all tests pass. Check actual test results to determine success. - set +e - OUTPUT=$(flutter test \ - integration_test/e2e_test.dart \ - -d macos \ - --dart-define=CI=true \ - --timeout 10x 2>&1) - EXIT_CODE=$? - echo "$OUTPUT" - if [ $EXIT_CODE -ne 0 ]; then - if echo "$OUTPUT" | grep -q "Some tests failed" || echo "$OUTPUT" | grep -q "test failed"; then - exit 1 - fi - if echo "$OUTPUT" | grep -q "tests passed"; then - echo "All tests passed but flutter test exited with $EXIT_CODE (likely 'Failed to foreground app'). Treating as success." - exit 0 - fi - exit $EXIT_CODE - fi - EOF - ) - # `emulators:exec` boots the suite, runs the command and tears the suite - # down, exiting with the command's exit code - so the laundering above - # still decides whether this step passes. - firebase emulators:exec --project flutterfire-e2e-tests "$TEST_COMMAND" - - name: Save Firestore Emulator Cache - continue-on-error: true - # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. - if: github.ref == 'refs/heads/main' - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - key: ${{ steps.firebase-emulator-cache.outputs.cache-primary-key }} - # Must match the restore path exactly - path: ~/.cache/firebase/emulators - - name: Save Pods Cache - continue-on-error: true - # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. - if: github.ref == 'refs/heads/main' - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - with: - key: ${{ steps.pods-cache.outputs.cache-primary-key }} - # Must match the restore path exactly - path: packages/cloud_functions/cloud_functions/example/macos/Pods + uses: ./.github/workflows/reusable_e2e_macos.yaml + with: + package-path: 'packages/cloud_functions/cloud_functions' + package-scope: 'cloud_functions*' + cache-key-suffix: 'functions' + native-config-args: '--functions-native' + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} web: needs: changes if: needs.changes.outputs.web == 'true' - permissions: - contents: read - runs-on: ubuntu-latest - timeout-minutes: ${{ inputs.nightly_test_mode && 5 || 25 }} - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - - uses: ./.github/actions/setup-flutterfire - timeout-minutes: 15 - with: - firebase-tools-version: '15.25.1' - bootstrap-scope: 'cloud_functions*' - # The ubuntu runner image ships Google Chrome and a chromedriver build - # that is matched to it — that pairing IS our pinning strategy, so we use - # the image binaries rather than downloading our own. - - name: 'Set up Chrome and chromedriver' - run: | - echo "$CHROMEWEBDRIVER" >> "$GITHUB_PATH" - google-chrome --version - "$CHROMEWEBDRIVER/chromedriver" --version - - name: Generate dummy Firebase configs - run: dart ./.github/workflows/scripts/generate-dummy-firebase-configs.dart - - name: Firebase Emulator Cache - id: firebase-emulator-cache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - # Must match the save path exactly - path: ~/.cache/firebase/emulators - key: firebase-emulators-v5-${{ env.FIREBASE_TOOLS_VERSION }} - restore-keys: firebase-emulators-v5- - - name: Install Cloud Functions dependencies - # `firebase emulators:exec` does not install these itself (the old - # start-firebase-emulator.sh wrapper did); without them the functions - # emulator fails to load the function definitions. - run: npm ci --prefix .github/workflows/scripts/functions - - name: 'E2E Tests' - # Covers booting the emulator suite as well as the tests themselves, since - # `emulators:exec` now owns both. - timeout-minutes: 20 - # firebase.json and the emulator rule files live here, so `emulators:exec` - # has to run from this directory; the test command cds back to the - # package under test. - working-directory: ./.github/workflows/scripts - # Web devices are not supported for the `flutter test` command yet. As a - # workaround we can use the `flutter drive` command. Tracking issue: - # https://github.com/flutter/flutter/issues/66264 - # The retry script only retries infrastructure startup failures - # (timeouts, AppConnectionException, "Failed to exit Chromium"); real - # test/compile failures fail fast. It also owns the chromedriver - # lifecycle, so nothing here starts chromedriver. - # The retry script reads its FLUTTER_DRIVE_* configuration from the - # environment, which `emulators:exec` passes through to the command. - env: - FLUTTER_DRIVE_TARGET: './integration_test/e2e_test.dart' - FLUTTER_DRIVE_DRIVER: './test_driver/integration_test.dart' - FLUTTER_DRIVE_EXTRA_ARGS: '--dart-define=CI=true' - run: | - firebase emulators:exec --project flutterfire-e2e-tests "cd ${GITHUB_WORKSPACE}/packages/cloud_functions/cloud_functions/example && ${GITHUB_WORKSPACE}/.github/workflows/scripts/flutter-drive-web-retry.sh" - - name: Save Firestore Emulator Cache - # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. - if: github.ref == 'refs/heads/main' - continue-on-error: true - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - key: ${{ steps.firebase-emulator-cache.outputs.cache-primary-key }} - # Must match the restore path exactly - path: ~/.cache/firebase/emulators - + uses: ./.github/workflows/reusable_e2e_web.yaml + with: + package-path: 'packages/cloud_functions/cloud_functions' + package-scope: 'cloud_functions*' + native-config-args: '--functions-native' + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} diff --git a/.github/workflows/e2e_tests_messaging.yaml b/.github/workflows/e2e_tests_messaging.yaml new file mode 100644 index 000000000000..a19c915c69d7 --- /dev/null +++ b/.github/workflows/e2e_tests_messaging.yaml @@ -0,0 +1,119 @@ +name: e2e-messaging + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-messaging + cancel-in-progress: true + +# Live tier: this product has no emulator, so every job talks to the real +# `flutterfire-e2e-tests` project using the config in repository secrets. +# Fork and dependabot PRs do not get those secrets, so the reusable workflow +# guards every job. +# +# Thin caller: one job per platform this product supports, each delegating +# to the matching `reusable_e2e_.yaml`. Platforms the product does +# not support have no job here, so they render no check at all. +# Path-filtered so it only runs when this product or its dependencies change; +# the nightly workflow_call still runs it unconditionally. +on: + pull_request: + paths: + - 'packages/firebase_messaging/**' + - 'packages/firebase_core/**' + - '.github/workflows/e2e_tests_messaging.yaml' + - '.github/workflows/reusable_e2e_*.yaml' + - '.github/actions/setup-flutterfire/**' + - '.github/workflows/scripts/**' + push: + branches: + - main + paths: + - 'packages/firebase_messaging/**' + - 'packages/firebase_core/**' + - '.github/workflows/e2e_tests_messaging.yaml' + - '.github/workflows/reusable_e2e_*.yaml' + - '.github/actions/setup-flutterfire/**' + - '.github/workflows/scripts/**' + workflow_call: + inputs: + nightly_test_mode: + type: boolean + default: false + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: + required: true + TESTS_E2E_GOOGLE_SERVICES_JSON: + required: true + +permissions: + contents: read + +jobs: + changes: + uses: ./.github/workflows/reusable_e2e_changes.yaml + with: + package-path: 'packages/firebase_messaging/firebase_messaging' + platform-interface-package: 'firebase_messaging_platform_interface' + web-package: 'firebase_messaging_web' + inject-config-secrets: true + + android: + needs: changes + if: needs.changes.outputs.android == 'true' + uses: ./.github/workflows/reusable_e2e_android.yaml + with: + package-path: 'packages/firebase_messaging/firebase_messaging' + package-scope: 'firebase_messaging*' + native-config-args: '--live-tier-plist=messaging' + inject-config-secrets: true + use-firebase-emulators: false + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} + + ios: + needs: changes + if: needs.changes.outputs.ios == 'true' + uses: ./.github/workflows/reusable_e2e_ios.yaml + with: + package-path: 'packages/firebase_messaging/firebase_messaging' + package-scope: 'firebase_messaging*' + cache-key-suffix: 'messaging' + native-config-args: '--live-tier-plist=messaging' + inject-config-secrets: true + use-firebase-emulators: false + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} + + macos: + needs: changes + if: needs.changes.outputs.macos == 'true' + uses: ./.github/workflows/reusable_e2e_macos.yaml + with: + package-path: 'packages/firebase_messaging/firebase_messaging' + package-scope: 'firebase_messaging*' + cache-key-suffix: 'messaging' + native-config-args: '--live-tier-plist=messaging' + inject-config-secrets: true + use-firebase-emulators: false + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} + + web: + needs: changes + if: needs.changes.outputs.web == 'true' + uses: ./.github/workflows/reusable_e2e_web.yaml + with: + package-path: 'packages/firebase_messaging/firebase_messaging' + package-scope: 'firebase_messaging*' + native-config-args: '--live-tier-plist=messaging' + inject-config-secrets: true + use-firebase-emulators: false + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} diff --git a/.github/workflows/e2e_tests_ml_model_downloader.yaml b/.github/workflows/e2e_tests_ml_model_downloader.yaml new file mode 100644 index 000000000000..4a4f2ce70706 --- /dev/null +++ b/.github/workflows/e2e_tests_ml_model_downloader.yaml @@ -0,0 +1,103 @@ +name: e2e-ml-model-downloader + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-ml_model_downloader + cancel-in-progress: true + +# Live tier: this product has no emulator, so every job talks to the real +# `flutterfire-e2e-tests` project using the config in repository secrets. +# Fork and dependabot PRs do not get those secrets, so the reusable workflow +# guards every job. +# +# Thin caller: one job per platform this product supports, each delegating +# to the matching `reusable_e2e_.yaml`. Platforms the product does +# not support have no job here, so they render no check at all. +# Path-filtered so it only runs when this product or its dependencies change; +# the nightly workflow_call still runs it unconditionally. +on: + pull_request: + paths: + - 'packages/firebase_ml_model_downloader/**' + - 'packages/firebase_core/**' + - '.github/workflows/e2e_tests_ml_model_downloader.yaml' + - '.github/workflows/reusable_e2e_*.yaml' + - '.github/actions/setup-flutterfire/**' + - '.github/workflows/scripts/**' + push: + branches: + - main + paths: + - 'packages/firebase_ml_model_downloader/**' + - 'packages/firebase_core/**' + - '.github/workflows/e2e_tests_ml_model_downloader.yaml' + - '.github/workflows/reusable_e2e_*.yaml' + - '.github/actions/setup-flutterfire/**' + - '.github/workflows/scripts/**' + workflow_call: + inputs: + nightly_test_mode: + type: boolean + default: false + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: + required: true + TESTS_E2E_GOOGLE_SERVICES_JSON: + required: true + +permissions: + contents: read + +jobs: + changes: + uses: ./.github/workflows/reusable_e2e_changes.yaml + with: + package-path: 'packages/firebase_ml_model_downloader/firebase_ml_model_downloader' + platform-interface-package: 'firebase_ml_model_downloader_platform_interface' + inject-config-secrets: true + + android: + needs: changes + if: needs.changes.outputs.android == 'true' + uses: ./.github/workflows/reusable_e2e_android.yaml + with: + package-path: 'packages/firebase_ml_model_downloader/firebase_ml_model_downloader' + package-scope: 'firebase_ml_model_downloader*' + native-config-args: '--live-tier-plist=ml_model_downloader' + inject-config-secrets: true + use-firebase-emulators: false + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} + + ios: + needs: changes + if: needs.changes.outputs.ios == 'true' + uses: ./.github/workflows/reusable_e2e_ios.yaml + with: + package-path: 'packages/firebase_ml_model_downloader/firebase_ml_model_downloader' + package-scope: 'firebase_ml_model_downloader*' + cache-key-suffix: 'ml_model_downloader' + native-config-args: '--live-tier-plist=ml_model_downloader' + inject-config-secrets: true + use-firebase-emulators: false + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} + + macos: + needs: changes + if: needs.changes.outputs.macos == 'true' + uses: ./.github/workflows/reusable_e2e_macos.yaml + with: + package-path: 'packages/firebase_ml_model_downloader/firebase_ml_model_downloader' + package-scope: 'firebase_ml_model_downloader*' + cache-key-suffix: 'ml_model_downloader' + native-config-args: '--live-tier-plist=ml_model_downloader' + inject-config-secrets: true + use-firebase-emulators: false + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} diff --git a/.github/workflows/e2e_tests_performance.yaml b/.github/workflows/e2e_tests_performance.yaml new file mode 100644 index 000000000000..26f0623830b3 --- /dev/null +++ b/.github/workflows/e2e_tests_performance.yaml @@ -0,0 +1,111 @@ +name: e2e-performance + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-performance + cancel-in-progress: true + +# Live tier: this product has no emulator, so every job talks to the real +# `flutterfire-e2e-tests` project using the config in repository secrets. +# Fork and dependabot PRs do not get those secrets, so the reusable workflow +# guards every job. +# +# Thin caller: one job per platform this product supports, each delegating +# to the matching `reusable_e2e_.yaml`. Platforms the product does +# not support have no job here, so they render no check at all. +# Path-filtered so it only runs when this product or its dependencies change; +# the nightly workflow_call still runs it unconditionally. +on: + pull_request: + paths: + - 'packages/firebase_performance/**' + - 'packages/firebase_core/**' + - '.github/workflows/e2e_tests_performance.yaml' + - '.github/workflows/reusable_e2e_*.yaml' + - '.github/actions/setup-flutterfire/**' + - '.github/workflows/scripts/**' + push: + branches: + - main + paths: + - 'packages/firebase_performance/**' + - 'packages/firebase_core/**' + - '.github/workflows/e2e_tests_performance.yaml' + - '.github/workflows/reusable_e2e_*.yaml' + - '.github/actions/setup-flutterfire/**' + - '.github/workflows/scripts/**' + workflow_call: + inputs: + nightly_test_mode: + type: boolean + default: false + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: + required: true + TESTS_E2E_GOOGLE_SERVICES_JSON: + required: true + +permissions: + contents: read + +jobs: + changes: + uses: ./.github/workflows/reusable_e2e_changes.yaml + with: + package-path: 'packages/firebase_performance/firebase_performance' + platform-interface-package: 'firebase_performance_platform_interface' + web-package: 'firebase_performance_web' + inject-config-secrets: true + + android: + needs: changes + if: needs.changes.outputs.android == 'true' + uses: ./.github/workflows/reusable_e2e_android.yaml + with: + package-path: 'packages/firebase_performance/firebase_performance' + package-scope: 'firebase_performance*' + native-config-args: '--live-tier-plist=performance' + inject-config-secrets: true + use-firebase-emulators: false + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} + + ios: + needs: changes + if: needs.changes.outputs.ios == 'true' + uses: ./.github/workflows/reusable_e2e_ios.yaml + with: + package-path: 'packages/firebase_performance/firebase_performance' + package-scope: 'firebase_performance*' + cache-key-suffix: 'performance' + # CocoaPods for now: Flutter's experimental SPM integration kept failing + # to resolve this example's plugin siblings ("Could not resolve package + # dependencies ... ios/firebase_core does not exist"). Dropping the Xcode + # 26.2 pin (see `reusable_e2e_ios.yaml`) is the likely fix, but one + # variable at a time - these two rejoin SPM once the default-Xcode runs + # prove clean. iOS SPM coverage is unaffected meanwhile: storage, + # database, firestore and auth still build under it. + ios-spm: false + native-config-args: '--live-tier-plist=performance' + inject-config-secrets: true + use-firebase-emulators: false + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} + + web: + needs: changes + if: needs.changes.outputs.web == 'true' + uses: ./.github/workflows/reusable_e2e_web.yaml + with: + package-path: 'packages/firebase_performance/firebase_performance' + package-scope: 'firebase_performance*' + native-config-args: '--live-tier-plist=performance' + inject-config-secrets: true + use-firebase-emulators: false + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} diff --git a/.github/workflows/e2e_tests_pipeline.yaml b/.github/workflows/e2e_tests_pipeline.yaml index 342ed7a16b00..1711cfd8dadc 100644 --- a/.github/workflows/e2e_tests_pipeline.yaml +++ b/.github/workflows/e2e_tests_pipeline.yaml @@ -52,7 +52,6 @@ jobs: - uses: ./.github/actions/setup-flutterfire with: npm-cache: 'false' - flutter-version: '3.41.9' bootstrap-scope: 'cloud_firestore*' - name: Inject Firebase config for pipeline E2E env: @@ -184,10 +183,7 @@ jobs: - uses: ./.github/actions/setup-flutterfire with: npm-cache: 'false' - flutter-version: '3.41.9' bootstrap-scope: 'cloud_firestore*' - - name: Enable Swift Package Manager for iOS - run: flutter config --enable-swift-package-manager - name: Inject Firebase config for pipeline E2E env: FIREBASE_OPTIONS_DART: ${{ secrets.PIPELINE_E2E_FIREBASE_OPTIONS_DART }} @@ -201,7 +197,18 @@ jobs: echo "$FIREBASE_OPTIONS_DART" > packages/cloud_firestore/cloud_firestore/pipeline_example/lib/firebase_options.dart echo "$GOOGLE_SERVICES_JSON" > packages/cloud_firestore/cloud_firestore/pipeline_example/android/app/google-services.json echo "$GOOGLE_SERVICE_INFO_PLIST" > packages/cloud_firestore/cloud_firestore/pipeline_example/ios/Runner/GoogleService-Info.plist + - name: Enable Swift Package Manager + # SPM, not CocoaPods: pods compile gRPC-C++/BoringSSL from source, + # which alone overran a 35-minute build budget on these runners, while + # SPM pulls Google's precompiled grpc/abseil binaries. The resolution + # failure that once forced CocoaPods here ("the folder firebase_core + # doesn't exist") was the pinned Xcode 26.2 canonicalizing the plugin + # symlink farm; the image-default Xcode resolves it fine, as the + # e2e-firestore SPM job proves every run. + run: flutter config --enable-swift-package-manager - name: Prepare iOS project for Swift Package Manager + # The committed Podfile is what CocoaPods users of the example rely + # on, and `flutter build` prefers CocoaPods whenever one is present. working-directory: packages/cloud_firestore/cloud_firestore/pipeline_example/ios run: | if [ -f Podfile ]; then pod deintegrate; fi @@ -214,9 +221,25 @@ jobs: model: "iPhone 16" - name: Build iOS (simulator) working-directory: packages/cloud_firestore/cloud_firestore/pipeline_example - timeout-minutes: 25 + # A cold SwiftPM fetch (grpc/abseil binaries) plus the Xcode build has + # been observed to need more than 25 minutes on a cache miss. + timeout-minutes: 35 run: | - flutter build ios --no-codesign --simulator --debug --target=./integration_test/pipeline/pipeline_live_test.dart --dart-define=CI=true + # Flutter's SPM integration intermittently resolves a plugin package + # at its real path instead of through the ephemeral symlink farm + # ("Could not resolve package dependencies ... firebase_core doesn't + # exist"). Retry exactly once, like reusable_e2e_ios.yaml does; the + # second run resolves against the already-generated layout. + set +e + flutter build ios --no-codesign --simulator --debug --target=./integration_test/pipeline/pipeline_live_test.dart --dart-define=CI=true > build_output.log 2>&1 + BUILD_EXIT=$? + cat build_output.log + if [ $BUILD_EXIT -ne 0 ] && grep -q "Could not resolve package dependencies" build_output.log; then + echo "SPM package resolution race detected; retrying the build once." + flutter build ios --no-codesign --simulator --debug --target=./integration_test/pipeline/pipeline_live_test.dart --dart-define=CI=true + BUILD_EXIT=$? + fi + exit $BUILD_EXIT - name: Ensure Simulator Ready env: SIMULATOR: ${{ steps.simulator.outputs.udid }} @@ -227,6 +250,42 @@ jobs: working-directory: packages/cloud_firestore/cloud_firestore/pipeline_example env: SIMULATOR: ${{ steps.simulator.outputs.udid }} - timeout-minutes: 20 + # Fits two alarm-bounded (15 min) attempts, like reusable_e2e_ios.yaml. + timeout-minutes: 40 + # Same wrapper as reusable_e2e_ios.yaml: `flutter test` against a + # simulator intermittently hangs after "Xcode build done" with zero + # output (this step burned its whole 20-minute budget on exactly + # that). Bound each attempt, recycle the simulator, retry once, and + # trust only the numeric tally. run: | - flutter test integration_test/pipeline/pipeline_live_test.dart -d "$SIMULATOR" --timeout 10x --dart-define=CI=true + run_flutter_test() { + rm -f flutter_test_output.log + perl -e 'alarm shift; exec @ARGV' 900 \ + flutter test integration_test/pipeline/pipeline_live_test.dart -d "$SIMULATOR" --timeout 10x --dart-define=CI=true > flutter_test_output.log 2>&1 + FT_EXIT=$? + cat flutter_test_output.log + } + set +e + run_flutter_test + if { ! grep -Eq '[0-9]+ tests? passed' flutter_test_output.log && ! grep -Eq '[0-9]+ failed' flutter_test_output.log; } \ + || grep -q "Unable to start the app on the device" flutter_test_output.log; then + echo "Infrastructure launch failure detected - recycling the simulator and retrying once." + echo "::group::Simulator log around the hang (Runner)" + perl -e 'alarm shift; exec @ARGV' 90 \ + xcrun simctl spawn "$SIMULATOR" log show --last 16m --style compact \ + --predicate 'process == "Runner" OR eventMessage CONTAINS "Runner"' 2>/dev/null | tail -80 || true + echo "::endgroup::" + xcrun simctl shutdown "$SIMULATOR" 2>/dev/null || true + xcrun simctl erase "$SIMULATOR" 2>/dev/null || true + xcrun simctl boot "$SIMULATOR" 2>/dev/null || true + "${GITHUB_WORKSPACE}/.github/workflows/scripts/ensure-simulator-ready.sh" || true + run_flutter_test + fi + PASSED=$(grep -Eo '[0-9]+ tests? passed' flutter_test_output.log | tail -1 | grep -Eo '^[0-9]+') + FAILED=$(grep -Eo '[0-9]+ failed' flutter_test_output.log | tail -1 | grep -Eo '^[0-9]+') + : "${PASSED:=0}"; : "${FAILED:=0}" + echo "Result tally: passed=$PASSED failed=$FAILED (flutter test exit $FT_EXIT)" + if [ "$FAILED" -gt 0 ] || [ "$PASSED" -eq 0 ]; then + exit 1 + fi + exit 0 diff --git a/.github/workflows/e2e_tests_remote_config.yaml b/.github/workflows/e2e_tests_remote_config.yaml new file mode 100644 index 000000000000..96d831c3da8f --- /dev/null +++ b/.github/workflows/e2e_tests_remote_config.yaml @@ -0,0 +1,134 @@ +name: e2e-remote-config + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-remote_config + cancel-in-progress: true + +# Live tier: this product has no emulator, so every job talks to the real +# `flutterfire-e2e-tests` project using the config in repository secrets. +# Fork and dependabot PRs do not get those secrets, so the reusable workflow +# guards every job. +# +# Thin caller: one job per platform this product supports, each delegating +# to the matching `reusable_e2e_.yaml`. Platforms the product does +# not support have no job here, so they render no check at all. +# Path-filtered so it only runs when this product or its dependencies change; +# the nightly workflow_call still runs it unconditionally. +on: + pull_request: + paths: + - 'packages/firebase_remote_config/**' + - 'packages/firebase_core/**' + - '.github/workflows/e2e_tests_remote_config.yaml' + - '.github/workflows/reusable_e2e_*.yaml' + - '.github/actions/setup-flutterfire/**' + - '.github/workflows/scripts/**' + push: + branches: + - main + paths: + - 'packages/firebase_remote_config/**' + - 'packages/firebase_core/**' + - '.github/workflows/e2e_tests_remote_config.yaml' + - '.github/workflows/reusable_e2e_*.yaml' + - '.github/actions/setup-flutterfire/**' + - '.github/workflows/scripts/**' + workflow_call: + inputs: + nightly_test_mode: + type: boolean + default: false + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: + required: true + TESTS_E2E_GOOGLE_SERVICES_JSON: + required: true + +permissions: + contents: read + +jobs: + changes: + uses: ./.github/workflows/reusable_e2e_changes.yaml + with: + package-path: 'packages/firebase_remote_config/firebase_remote_config' + platform-interface-package: 'firebase_remote_config_platform_interface' + web-package: 'firebase_remote_config_web' + inject-config-secrets: true + + android: + needs: changes + if: needs.changes.outputs.android == 'true' + uses: ./.github/workflows/reusable_e2e_android.yaml + with: + package-path: 'packages/firebase_remote_config/firebase_remote_config' + package-scope: 'firebase_remote_config*' + native-config-args: '--live-tier-plist=remote_config' + inject-config-secrets: true + use-firebase-emulators: false + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} + + ios: + needs: changes + if: needs.changes.outputs.ios == 'true' + uses: ./.github/workflows/reusable_e2e_ios.yaml + with: + package-path: 'packages/firebase_remote_config/firebase_remote_config' + package-scope: 'firebase_remote_config*' + cache-key-suffix: 'remote_config' + native-config-args: '--live-tier-plist=remote_config' + inject-config-secrets: true + use-firebase-emulators: false + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} + + macos: + needs: changes + if: needs.changes.outputs.macos == 'true' + uses: ./.github/workflows/reusable_e2e_macos.yaml + with: + package-path: 'packages/firebase_remote_config/firebase_remote_config' + package-scope: 'firebase_remote_config*' + cache-key-suffix: 'remote_config' + native-config-args: '--live-tier-plist=remote_config' + inject-config-secrets: true + use-firebase-emulators: false + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} + + web: + needs: changes + if: needs.changes.outputs.web == 'true' + uses: ./.github/workflows/reusable_e2e_web.yaml + with: + package-path: 'packages/firebase_remote_config/firebase_remote_config' + package-scope: 'firebase_remote_config*' + native-config-args: '--live-tier-plist=remote_config' + inject-config-secrets: true + use-firebase-emulators: false + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} + + windows: + needs: changes + if: needs.changes.outputs.windows == 'true' + uses: ./.github/workflows/reusable_e2e_windows.yaml + with: + package-path: 'packages/firebase_remote_config/firebase_remote_config' + package-scope: 'firebase_remote_config*' + native-config-args: '--live-tier-plist=remote_config' + inject-config-secrets: true + use-firebase-emulators: false + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} diff --git a/.github/workflows/e2e_tests_smoke.yaml b/.github/workflows/e2e_tests_smoke.yaml new file mode 100644 index 000000000000..2d1ae9b4e593 --- /dev/null +++ b/.github/workflows/e2e_tests_smoke.yaml @@ -0,0 +1,647 @@ +name: e2e-smoke + +# The `tests` app depends on every plugin in the repository at once. Its job is +# to prove they still build and boot together, so unlike the per-package e2e +# workflows this one is deliberately NOT path-scoped to a package: any change +# that is not documentation can break the combined app. + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-smoke + cancel-in-progress: true + +permissions: + contents: read + +on: + pull_request: + paths-ignore: + - 'docs/**' + - 'website/**' + - '**/example/**' + - '!**/example/integration_test/**' + - '**/flutterfire_ui/**' + - '**.md' + push: + branches: + - main + paths-ignore: + - 'docs/**' + - 'website/**' + - '**/example/**' + - '!**/example/integration_test/**' + - '**/flutterfire_ui/**' + - '**.md' + workflow_call: + inputs: + nightly_test_mode: + type: boolean + default: false + +jobs: + android: + permissions: + contents: read + runs-on: ubuntu-latest + timeout-minutes: ${{ inputs.nightly_test_mode && 5 || 25 }} + env: + AVD_ARCH: x86_64 + AVD_API_LEVEL: 34 + AVD_TARGET: google_apis + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 + - uses: ./.github/actions/setup-flutterfire + with: + firebase-tools-version: '15.25.1' + bootstrap-scope: tests + - name: Firebase Emulator Cache + id: firebase-emulator-cache + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + continue-on-error: true + with: + # The firebase emulators are pure javascript and java, OS-independent + enableCrossOsArchive: true + # Must match the save path exactly + path: ~/.cache/firebase/emulators + key: firebase-emulators-v5-${{ env.FIREBASE_TOOLS_VERSION }} + # Trailing dash so this never prefix-matches a future version's key + restore-keys: firebase-emulators-v5- + - name: Generate dummy Firebase configs + run: dart ./.github/workflows/scripts/generate-dummy-firebase-configs.dart + - name: Install Cloud Functions dependencies + # `firebase emulators:exec` does not install these itself (the old + # start-firebase-emulator.sh wrapper did); without them the functions + # emulator fails to load the function definitions. + run: npm ci --prefix .github/workflows/scripts/functions + - name: Enable KVM + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + - name: Gradle cache + uses: gradle/actions/setup-gradle@90ddb51e90a5fd9ba75f40cf85156b7b41bf76a3 + - name: Free Disk Space (Ubuntu) + uses: AdityaGarg8/remove-unwanted-software@90e01b21170618765a73370fcc3abbd1684a7793 + with: + remove-dotnet: true + remove-haskell: true + remove-codeql: true + remove-docker-images: true + remove-large-packages: true + - name: Prepare AVD home on /mnt + # GitHub-hosted runners mount a ~74GB volume at /mnt. Create it before AVD cache + # restore and android-emulator-runner (avdmanager needs the space at create time). + run: | + sudo mkdir -p /mnt/avd + sudo chown "$USER:$USER" /mnt/avd + df -h / /mnt + - name: AVD cache + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + continue-on-error: true + id: avd-cache + with: + # Must match the save path exactly + path: | + /mnt/avd/* + ~/.android/adb* + # The emulator build is part of the key so bumping `emulator-build:` below + # invalidates the cached AVD instead of reusing an image from the old build. + key: avd-${{ runner.os }}-${{ env.AVD_API_LEVEL }}-${{ env.AVD_TARGET }}-${{ env.AVD_ARCH }}-14214601 + - name: Link AVD home to /mnt + # android-emulator-runner exportVariables ANDROID_AVD_HOME to $HOME/.android/avd + run: | + mkdir -p "$HOME/.android" + rm -rf "$HOME/.android/avd" + ln -s /mnt/avd "$HOME/.android/avd" + - name: Pre-build APK + # Build outside the emulator so the AVD does not boot and idle through the + # whole Gradle build. `flutter test` below reuses this warm build cache. + # The validation skip matches `flutter test`, which does not enforce the + # minimum Gradle version either. + working-directory: tests + timeout-minutes: 25 + run: flutter build apk --debug --target=integration_test/core_shard_test.dart --android-skip-build-dependency-validation + - name: Start AVD then run E2E tests + uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a + # Two alarm-bounded test attempts (see the retry script) plus AVD boot + # and the emulator suite; a single launch hang used to burn the whole + # 20-minute budget. + timeout-minutes: 40 + env: + ANDROID_AVD_HOME: /mnt/avd + STORAGE_EMULATOR_DEBUG: 'true' + TEST_TARGET: integration_test/core_shard_test.dart + with: + api-level: ${{ env.AVD_API_LEVEL }} + target: ${{ env.AVD_TARGET }} + arch: ${{ env.AVD_ARCH }} + emulator-build: 14214601 + # The default (true) wipes and recreates the AVD, making the cache above useless. + force-avd-creation: false + # firebase.json and the emulator rule files live here, so `emulators:exec` + # has to run from this directory; the test command cds back to the + # package under test. + working-directory: .github/workflows/scripts + # `emulators:exec` owns the emulator lifecycle: it boots the suite, runs + # the command and tears the suite down, exiting with the command's exit + # code. Nothing is left running between steps. + script: | + firebase emulators:exec --project flutterfire-e2e-tests "cd ${GITHUB_WORKSPACE}/tests && ${GITHUB_WORKSPACE}/.github/workflows/scripts/flutter-test-android-retry.sh" + - name: Ensure Appium is shut down + # Required because of below issue where emulator failing to shut down properly causes tests to fail + # https://github.com/ReactiveCircus/android-emulator-runner/issues/385 + run: | + pgrep -f appium && pkill -f appium || echo "No Appium process found" + - name: Save Firestore Emulator Cache + # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. + if: github.ref == 'refs/heads/main' + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + continue-on-error: true + with: + # The firebase emulators are pure javascript and java, OS-independent + enableCrossOsArchive: true + key: ${{ steps.firebase-emulator-cache.outputs.cache-primary-key }} + # Must match the restore path exactly + path: ~/.cache/firebase/emulators + - name: Save Android Emulator Cache + # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. + # Skip on a cache hit: the AVD image is multi-GB and re-uploading it unchanged on + # every main run is pure waste. + if: github.ref == 'refs/heads/main' && steps.avd-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + continue-on-error: true + with: + key: ${{ steps.avd-cache.outputs.cache-primary-key }} + # Must match the restore path exactly + path: | + /mnt/avd/* + ~/.android/adb* + + agp9-compatibility: + permissions: + contents: read + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 + - uses: ./.github/actions/setup-flutterfire + with: + node: 'false' + bootstrap-scope: 'tests' + - name: Gradle cache + uses: gradle/actions/setup-gradle@90ddb51e90a5fd9ba75f40cf85156b7b41bf76a3 + - name: 'Build tests app with AGP 9' + timeout-minutes: 25 + run: bash ./.github/workflows/scripts/agp9-compatibility.sh + + ios: + permissions: + contents: read + runs-on: macos-15 + timeout-minutes: ${{ inputs.nightly_test_mode && 5 || 35 }} + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 + - name: Xcode + # Firebase iOS SDK: minimum Xcode 26.2. + run: sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer + - uses: ./.github/actions/setup-flutterfire + with: + firebase-tools-version: '15.25.1' + bootstrap-scope: tests + - name: Disable Swift Package Manager + # Recent stable Flutter enables SPM by default; the smoke app's Apple + # builds are CocoaPods by design (tests/ios, tests/macos Podfiles). + run: flutter config --no-enable-swift-package-manager + - uses: hendrikmuhs/ccache-action@d62db5f07c26379fc4b4e0916f098a92573c3b03 + name: Xcode Compile Cache + with: + # Distinct from the macOS job's key: both run on macos-15, so a shared + # `xcode-cache-${{ runner.os }}` had the two jobs clobbering each other. + key: xcode-ccache-ios + save: "${{ github.ref == 'refs/heads/main' }}" + max-size: 700M + - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + continue-on-error: true + name: Pods Cache + id: pods-cache + with: + # Must match the save path exactly + path: tests/ios/Pods + # Keyed on the Podfile and the pinned Firebase SDK version, not tests/pubspec.lock: + # that file is gitignored, so hashFiles() returned an empty string and the key + # never changed - the cache could never be invalidated. + key: pods-v4-${{ runner.os }}-ios-${{ hashFiles('tests/ios/Podfile', 'packages/firebase_core/firebase_core/ios/firebase_sdk_version.rb') }} + restore-keys: pods-v4-${{ runner.os }}-ios- + - name: Firebase Emulator Cache + id: firebase-emulator-cache + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + continue-on-error: true + with: + # The firebase emulators are pure javascript and java, OS-independent + enableCrossOsArchive: true + # Must match the save path exactly + path: ~/.cache/firebase/emulators + key: firebase-emulators-v5-${{ env.FIREBASE_TOOLS_VERSION }} + # Trailing dash so this never prefix-matches a future version's key + restore-keys: firebase-emulators-v5- + - name: Generate dummy Firebase configs + run: dart ./.github/workflows/scripts/generate-dummy-firebase-configs.dart + - name: 'Free up space' + run: | + sudo rm -rf \ + /usr/local/share/.cache \ + /opt/microsoft/msedge \ + /opt/microsoft/powershell \ + /opt/pipx \ + /usr/lib/mono \ + /usr/local/julia* \ + /usr/local/lib/android \ + /usr/local/share/chromium \ + /usr/local/share/powershell \ + /usr/share/dotnet + df -h / + - name: 'Build Application' + working-directory: tests + timeout-minutes: 25 + run: | + export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH" + export CCACHE_SLOPPINESS=clang_index_store,file_stat_matches,include_file_ctime,include_file_mtime,ivfsoverlay,pch_defines,modules,system_headers,time_macros + export CCACHE_FILECLONE=true + export CCACHE_DEPEND=true + export CCACHE_INODECACHE=true + ccache -s + flutter build ios --no-codesign --simulator --debug --target=./integration_test/core_shard_test.dart --dart-define=CI=true + ccache -s + - name: Install Cloud Functions dependencies + # `firebase emulators:exec` does not install these itself (the old + # start-firebase-emulator.sh wrapper did); without them the functions + # emulator fails to load the function definitions. + # Chown the npm cache directory to the runner user to avoid permission issues. + run: sudo chown -R 501:20 "/Users/runner/.npm" && npm ci --prefix .github/workflows/scripts/functions + - uses: futureware-tech/simulator-action@e89aa8f93d3aec35083ff49d2854d07f7186f7f5 + id: simulator + with: + # List of available simulators: https://github.com/actions/runner-images/blob/main/images/macos/macos-15-Readme.md#installed-simulators + model: "iPhone 16" + - name: Ensure Simulator Ready + timeout-minutes: 13 + env: + SIMULATOR: ${{ steps.simulator.outputs.udid }} + ENSURE_BOOT_IF_NEEDED: "0" + run: .github/workflows/scripts/ensure-simulator-ready.sh + - name: 'E2E Tests' + # firebase.json and the emulator rule files live here, so `emulators:exec` + # has to run from this directory; the test command cds back to the package + # under test. + working-directory: ./.github/workflows/scripts + # Covers booting the emulator suite as well as the tests themselves, since + # `emulators:exec` now owns both. + timeout-minutes: 20 + env: + SIMULATOR: ${{ steps.simulator.outputs.udid }} + STORAGE_EMULATOR_DEBUG: 'true' + run: | + # Uncomment following line to have simulator logs printed out for debugging purposes. + # xcrun simctl spawn booted log stream --predicate 'eventMessage contains "flutter"' & + # Once the integration test runner has launched, leave it running rather + # than starting a second app instance from a retry. + # `emulators:exec` boots the suite, runs the command and tears the suite + # down, exiting with the command's exit code. + firebase emulators:exec --project flutterfire-e2e-tests "cd ${GITHUB_WORKSPACE}/tests && flutter test integration_test/core_shard_test.dart -d \"$SIMULATOR\" --timeout 10x --dart-define=CI=true" + - name: Save Firestore Emulator Cache + # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. + if: github.ref == 'refs/heads/main' + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + continue-on-error: true + with: + # The firebase emulators are pure javascript and java, OS-independent + enableCrossOsArchive: true + key: ${{ steps.firebase-emulator-cache.outputs.cache-primary-key }} + # Must match the restore paths exactly + path: ~/.cache/firebase/emulators + - name: Save Pods Cache + # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. + if: github.ref == 'refs/heads/main' + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + continue-on-error: true + with: + key: ${{ steps.pods-cache.outputs.cache-primary-key }} + # Must match the restore paths exactly + path: tests/ios/Pods + + macos: + permissions: + contents: read + runs-on: macos-15 + timeout-minutes: ${{ inputs.nightly_test_mode && 5 || 35 }} + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 + - name: Xcode + # Firebase iOS SDK: minimum Xcode 26.2. + run: sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer + - uses: ./.github/actions/setup-flutterfire + with: + firebase-tools-version: '15.25.1' + bootstrap-scope: tests + - name: Disable Swift Package Manager + # Recent stable Flutter enables SPM by default; the smoke app's Apple + # builds are CocoaPods by design (tests/ios, tests/macos Podfiles). + run: flutter config --no-enable-swift-package-manager + - uses: hendrikmuhs/ccache-action@d62db5f07c26379fc4b4e0916f098a92573c3b03 + name: Xcode Compile Cache + with: + # Distinct from the iOS job's key: both run on macos-15, so a shared + # `xcode-cache-${{ runner.os }}` had the two jobs clobbering each other. + key: xcode-ccache-macos + save: "${{ github.ref == 'refs/heads/main' }}" + max-size: 700M + - name: Pods Cache + continue-on-error: true + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + id: pods-cache + with: + # Must match the save path exactly + path: tests/macos/Pods + # Keyed on the Podfile and the pinned Firebase SDK version, not tests/pubspec.lock: + # that file is gitignored, so hashFiles() returned an empty string and the key + # never changed - the cache could never be invalidated. + key: pods-v4-${{ runner.os }}-macos-${{ hashFiles('tests/macos/Podfile', 'packages/firebase_core/firebase_core/ios/firebase_sdk_version.rb') }} + restore-keys: pods-v4-${{ runner.os }}-macos- + - name: Firebase Emulator Cache + id: firebase-emulator-cache + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + continue-on-error: true + with: + # The firebase emulators are pure javascript and java, OS-independent + enableCrossOsArchive: true + # Must match the save path exactly + path: ~/.cache/firebase/emulators + key: firebase-emulators-v5-${{ env.FIREBASE_TOOLS_VERSION }} + # Trailing dash so this never prefix-matches a future version's key + restore-keys: firebase-emulators-v5- + - name: Generate dummy Firebase configs + run: dart ./.github/workflows/scripts/generate-dummy-firebase-configs.dart + - name: 'Build Application' + working-directory: tests + timeout-minutes: 25 + run: | + export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH" + export CCACHE_SLOPPINESS=clang_index_store,file_stat_matches,include_file_ctime,include_file_mtime,ivfsoverlay,pch_defines,modules,system_headers,time_macros + export CCACHE_FILECLONE=true + export CCACHE_DEPEND=true + export CCACHE_INODECACHE=true + ccache -s + flutter build macos --debug --target=./integration_test/core_shard_test.dart --device-id=macos --dart-define=CI=true + ccache -s + - name: Install Cloud Functions dependencies + # `firebase emulators:exec` does not install these itself (the old + # start-firebase-emulator.sh wrapper did); without them the functions + # emulator fails to load the function definitions. + # Chown the npm cache directory to the runner user to avoid permission issues. + run: sudo chown -R 501:20 "/Users/runner/.npm" && npm ci --prefix .github/workflows/scripts/functions + - name: 'E2E Tests' + # firebase.json and the emulator rule files live here, so `emulators:exec` + # has to run from this directory; the test command cds back to the package + # under test. + working-directory: ./.github/workflows/scripts + # Covers booting the emulator suite as well as the tests themselves, since + # `emulators:exec` now owns both. + timeout-minutes: 20 + env: + STORAGE_EMULATOR_DEBUG: 'true' + run: | + # The test command is handed to `emulators:exec` as a single string; a + # quoted heredoc keeps the script below verbatim instead of forcing a + # layer of quote escaping onto it. + TEST_COMMAND=$(cat <<'EOF' + cd "${GITHUB_WORKSPACE}/tests" + # flutter test on macOS CI may exit 1 due to "Failed to foreground app" + # even when all tests pass. Check actual test results to determine success. + set +e + OUTPUT=$(flutter test \ + integration_test/core_shard_test.dart \ + -d macos \ + --dart-define=CI=true \ + --timeout 10x 2>&1) + EXIT_CODE=$? + echo "$OUTPUT" + if [ $EXIT_CODE -ne 0 ]; then + if echo "$OUTPUT" | grep -q "Some tests failed" || echo "$OUTPUT" | grep -q "test failed"; then + exit 1 + fi + if echo "$OUTPUT" | grep -q "tests passed"; then + echo "All tests passed but flutter test exited with $EXIT_CODE (likely 'Failed to foreground app'). Treating as success." + exit 0 + fi + exit $EXIT_CODE + fi + EOF + ) + # `emulators:exec` boots the suite, runs the command and tears the suite + # down, exiting with the command's exit code - so the laundering above + # still decides whether this step passes. + firebase emulators:exec --project flutterfire-e2e-tests "$TEST_COMMAND" + - name: Save Firestore Emulator Cache + continue-on-error: true + # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. + if: github.ref == 'refs/heads/main' + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + with: + # The firebase emulators are pure javascript and java, OS-independent + enableCrossOsArchive: true + key: ${{ steps.firebase-emulator-cache.outputs.cache-primary-key }} + # Must match the restore path exactly + path: ~/.cache/firebase/emulators + - name: Save Pods Cache + continue-on-error: true + # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. + if: github.ref == 'refs/heads/main' + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + with: + key: ${{ steps.pods-cache.outputs.cache-primary-key }} + # Must match the restore paths exactly + path: tests/macos/Pods + + web: + permissions: + contents: read + runs-on: ubuntu-latest + timeout-minutes: ${{ inputs.nightly_test_mode && 5 || 25 }} + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 + - uses: ./.github/actions/setup-flutterfire + timeout-minutes: 15 + with: + firebase-tools-version: '15.25.1' + bootstrap-scope: tests + # The ubuntu runner image ships Google Chrome and a chromedriver build + # that is matched to it — that pairing IS our pinning strategy, so we use + # the image binaries rather than downloading our own. + - name: 'Set up Chrome and chromedriver' + run: | + echo "$CHROMEWEBDRIVER" >> "$GITHUB_PATH" + google-chrome --version + "$CHROMEWEBDRIVER/chromedriver" --version + - name: Generate dummy Firebase configs + run: dart ./.github/workflows/scripts/generate-dummy-firebase-configs.dart + - name: Firebase Emulator Cache + id: firebase-emulator-cache + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + continue-on-error: true + with: + # The firebase emulators are pure javascript and java, OS-independent + enableCrossOsArchive: true + # Must match the save path exactly + path: ~/.cache/firebase/emulators + key: firebase-emulators-v5-${{ env.FIREBASE_TOOLS_VERSION }} + restore-keys: firebase-emulators-v5- + - name: Install Cloud Functions dependencies + # `firebase emulators:exec` does not install these itself (the old + # start-firebase-emulator.sh wrapper did); without them the functions + # emulator fails to load the function definitions. + run: npm ci --prefix .github/workflows/scripts/functions + - name: 'E2E Tests' + # Covers booting the emulator suite as well as the tests themselves, since + # `emulators:exec` now owns both. + timeout-minutes: 20 + # firebase.json and the emulator rule files live here, so `emulators:exec` + # has to run from this directory; the test command cds back to the package + # under test. + working-directory: ./.github/workflows/scripts + # Web devices are not supported for the `flutter test` command yet. As a + # workaround we can use the `flutter drive` command. Tracking issue: + # https://github.com/flutter/flutter/issues/66264 + # The retry script only retries infrastructure startup failures + # (timeouts, AppConnectionException, "Failed to exit Chromium"); real + # test/compile failures fail fast. It also owns the chromedriver + # lifecycle, so nothing here starts chromedriver. + # The retry script reads its FLUTTER_DRIVE_* configuration from the + # environment, which `emulators:exec` passes through to the command. + env: + FLUTTER_DRIVE_TARGET: './integration_test/core_shard_test.dart' + FLUTTER_DRIVE_DRIVER: './test_driver/integration_test.dart' + FLUTTER_DRIVE_EXTRA_ARGS: '--dart-define=CI=true' + STORAGE_EMULATOR_DEBUG: 'true' + run: | + firebase emulators:exec --project flutterfire-e2e-tests "cd ${GITHUB_WORKSPACE}/tests && ${GITHUB_WORKSPACE}/.github/workflows/scripts/flutter-drive-web-retry.sh" + - name: Save Firestore Emulator Cache + # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. + if: github.ref == 'refs/heads/main' + continue-on-error: true + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + with: + # The firebase emulators are pure javascript and java, OS-independent + enableCrossOsArchive: true + key: ${{ steps.firebase-emulator-cache.outputs.cache-primary-key }} + # Must match the restore path exactly + path: ~/.cache/firebase/emulators + + web-wasm: + permissions: + contents: read + runs-on: ubuntu-latest + timeout-minutes: ${{ inputs.nightly_test_mode && 5 || 25 }} + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 + - uses: ./.github/actions/setup-flutterfire + timeout-minutes: 15 + with: + firebase-tools-version: '15.25.1' + bootstrap-scope: tests + # The ubuntu runner image ships Google Chrome and a chromedriver build + # that is matched to it — that pairing IS our pinning strategy, so we use + # the image binaries rather than downloading our own. + - name: 'Set up Chrome and chromedriver' + run: | + echo "$CHROMEWEBDRIVER" >> "$GITHUB_PATH" + google-chrome --version + "$CHROMEWEBDRIVER/chromedriver" --version + - name: Generate dummy Firebase configs + run: dart ./.github/workflows/scripts/generate-dummy-firebase-configs.dart + - name: Firebase Emulator Cache + id: firebase-emulator-cache + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + continue-on-error: true + with: + # The firebase emulators are pure javascript and java, OS-independent + enableCrossOsArchive: true + # Must match the save path exactly + path: ~/.cache/firebase/emulators + key: firebase-emulators-v5-${{ env.FIREBASE_TOOLS_VERSION }} + restore-keys: firebase-emulators-v5- + - name: Install Cloud Functions dependencies + # `firebase emulators:exec` does not install these itself (the old + # start-firebase-emulator.sh wrapper did); without them the functions + # emulator fails to load the function definitions. + run: npm ci --prefix .github/workflows/scripts/functions + - name: 'Use WASM index.html' + working-directory: tests + run: mv ./web/wasm_index.html ./web/index.html + - name: 'E2E Tests' + # Covers booting the emulator suite as well as the tests themselves, since + # `emulators:exec` now owns both. + timeout-minutes: 20 + # firebase.json and the emulator rule files live here, so `emulators:exec` + # has to run from this directory; the test command cds back to the package + # under test. + working-directory: ./.github/workflows/scripts + # Web devices are not supported for the `flutter test` command yet. As a + # workaround we can use the `flutter drive` command. Tracking issue: + # https://github.com/flutter/flutter/issues/66264 + # WASM web runs can hang after building but before the test harness + # connects; the retry script retries only those infrastructure startup + # failures. It also owns the chromedriver lifecycle, so nothing here + # starts chromedriver. + # The retry script reads its FLUTTER_DRIVE_* configuration from the + # environment, which `emulators:exec` passes through to the command. + env: + FLUTTER_DRIVE_TARGET: './integration_test/core_shard_test.dart' + FLUTTER_DRIVE_DRIVER: './test_driver/integration_test.dart' + FLUTTER_DRIVE_EXTRA_ARGS: '--wasm --dart-define=CI=true' + FLUTTER_DRIVE_TIMEOUT_SECONDS: '300' + FLUTTER_DRIVE_MAX_ATTEMPTS: '2' + STORAGE_EMULATOR_DEBUG: 'true' + run: | + firebase emulators:exec --project flutterfire-e2e-tests "cd ${GITHUB_WORKSPACE}/tests && ${GITHUB_WORKSPACE}/.github/workflows/scripts/flutter-drive-web-retry.sh" + - name: Save Firestore Emulator Cache + # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. + if: github.ref == 'refs/heads/main' + continue-on-error: true + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + with: + # The firebase emulators are pure javascript and java, OS-independent + enableCrossOsArchive: true + key: ${{ steps.firebase-emulator-cache.outputs.cache-primary-key }} + # Must match the restore path exactly + path: ~/.cache/firebase/emulators + + windows: + permissions: + contents: read + runs-on: windows-latest + timeout-minutes: ${{ inputs.nightly_test_mode && 5 || 45 }} + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 + - uses: ./.github/actions/setup-flutterfire + with: + npm-cache: 'false' + bootstrap-scope: 'tests cloud_firestore*' + - name: "Install Tools" + # Not the composite action's firebase-tools install: that one uses `sudo npm`, + # which does not exist on the Windows runners. + run: | + npm install -g firebase-tools@15.25.1 + - name: "Build Windows (Release)" + timeout-minutes: 25 + run: cd tests && flutter build windows --release + - name: "Build Windows (Profile)" + timeout-minutes: 25 + run: cd tests && flutter build windows --profile + - name: Install Cloud Functions dependencies + # `firebase emulators:exec` does not install these itself; without them the + # functions emulator logs "Failed to load function definition" on every run. + run: npm ci --prefix .github/workflows/scripts/functions + - name: Start Firebase Emulator and run tests + timeout-minutes: 30 + env: + STORAGE_EMULATOR_DEBUG: 'true' + run: cd ./.github/workflows/scripts && firebase emulators:exec --project flutterfire-e2e-tests "cd ../../../tests && flutter test .\integration_test\e2e_test.dart -d windows --verbose" diff --git a/.github/workflows/e2e_tests_storage.yaml b/.github/workflows/e2e_tests_storage.yaml index 0093a8fe9d84..8beadfce3a67 100644 --- a/.github/workflows/e2e_tests_storage.yaml +++ b/.github/workflows/e2e_tests_storage.yaml @@ -4,15 +4,21 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }}-storage cancel-in-progress: true +# Emulator tier: every job runs under `firebase emulators:exec`, so no live +# project credentials are needed. +# +# Thin caller: one job per platform this product supports, each delegating +# to the matching `reusable_e2e_.yaml`. Platforms the product does +# not support have no job here, so they render no check at all. +# Path-filtered so it only runs when this product or its dependencies change; +# the nightly workflow_call still runs it unconditionally. on: pull_request: - # Storage e2e only exercises this package; running it on every PR costs 4 - # jobs (2 macOS) for changes that cannot affect it. The nightly - # workflow_call still runs it unconditionally. paths: - 'packages/firebase_storage/**' - 'packages/firebase_core/**' - '.github/workflows/e2e_tests_storage.yaml' + - '.github/workflows/reusable_e2e_*.yaml' - '.github/actions/setup-flutterfire/**' - '.github/workflows/scripts/**' push: @@ -22,6 +28,7 @@ on: - 'packages/firebase_storage/**' - 'packages/firebase_core/**' - '.github/workflows/e2e_tests_storage.yaml' + - '.github/workflows/reusable_e2e_*.yaml' - '.github/actions/setup-flutterfire/**' - '.github/workflows/scripts/**' workflow_call: @@ -34,569 +41,66 @@ permissions: contents: read jobs: - # Maps changed paths to affected platforms so a Kotlin-only change runs only - # the android job, a Swift-only change only ios/macos, etc. Dart code, the - # tests themselves, firebase_core and CI plumbing affect every platform. - # Non-PR events (push to main, the nightly workflow_call) always run - # everything: the filter step is skipped and its empty outputs fall back to - # 'true' below. changes: - permissions: - contents: read - runs-on: ubuntu-latest - timeout-minutes: 5 - outputs: - android: ${{ steps.filter.outputs.android || 'true' }} - ios: ${{ steps.filter.outputs.ios || 'true' }} - macos: ${{ steps.filter.outputs.macos || 'true' }} - web: ${{ steps.filter.outputs.web || 'true' }} - windows: ${{ steps.filter.outputs.windows || 'true' }} - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - if: github.event_name == 'pull_request' - - uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 - if: github.event_name == 'pull_request' - id: filter - with: - filters: | - shared: &shared - - 'packages/firebase_core/**' - - 'packages/firebase_storage/firebase_storage/lib/**' - - 'packages/firebase_storage/firebase_storage/pubspec.yaml' - - 'packages/firebase_storage/firebase_storage_platform_interface/**' - - 'packages/firebase_storage/firebase_storage/example/integration_test/**' - - 'packages/firebase_storage/firebase_storage/example/lib/**' - - 'packages/firebase_storage/firebase_storage/example/pubspec.yaml' - - '.github/workflows/e2e_tests_storage.yaml' - - '.github/actions/setup-flutterfire/**' - - '.github/workflows/scripts/**' - android: - - *shared - - 'packages/firebase_storage/firebase_storage/android/**' - - 'packages/firebase_storage/firebase_storage/example/android/**' - ios: - - *shared - - 'packages/firebase_storage/firebase_storage/ios/**' - - 'packages/firebase_storage/firebase_storage/darwin/**' - - 'packages/firebase_storage/firebase_storage/example/ios/**' - macos: - - *shared - - 'packages/firebase_storage/firebase_storage/macos/**' - - 'packages/firebase_storage/firebase_storage/darwin/**' - - 'packages/firebase_storage/firebase_storage/example/macos/**' - web: - - *shared - - 'packages/firebase_storage/firebase_storage_web/**' - - 'packages/firebase_storage/firebase_storage/example/web/**' - windows: - - *shared - - 'packages/firebase_storage/firebase_storage/windows/**' - - 'packages/firebase_storage/firebase_storage/example/windows/**' + uses: ./.github/workflows/reusable_e2e_changes.yaml + with: + package-path: 'packages/firebase_storage/firebase_storage' + platform-interface-package: 'firebase_storage_platform_interface' + web-package: 'firebase_storage_web' android: needs: changes if: needs.changes.outputs.android == 'true' - permissions: - contents: read - runs-on: ubuntu-latest - timeout-minutes: ${{ inputs.nightly_test_mode && 5 || 25 }} - env: - AVD_ARCH: x86_64 - AVD_API_LEVEL: 34 - AVD_TARGET: google_apis - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - - uses: ./.github/actions/setup-flutterfire - with: - firebase-tools-version: '15.25.1' - bootstrap-scope: 'firebase_storage*' - - name: Firebase Emulator Cache - id: firebase-emulator-cache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - # Must match the save path exactly - path: ~/.cache/firebase/emulators - # Shared with the platform workflows: same emulator payload, same key. - key: firebase-emulators-v5-${{ env.FIREBASE_TOOLS_VERSION }} - # Trailing dash so this never prefix-matches a future version's key - restore-keys: firebase-emulators-v5- - - name: Generate dummy Firebase configs - # `--storage-native` is required here: the example's Android app applies - # the `google-services` plugin, which fails the build when - # google-services.json is missing. - run: dart ./.github/workflows/scripts/generate-dummy-firebase-configs.dart --storage-native - - name: Install Cloud Functions dependencies - # `firebase emulators:exec` does not install these itself (the old - # start-firebase-emulator.sh wrapper did); without them the functions - # emulator fails to load the function definitions. - run: npm ci --prefix .github/workflows/scripts/functions - - name: Enable KVM - run: | - echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules - sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm - - name: Gradle cache - uses: gradle/actions/setup-gradle@90ddb51e90a5fd9ba75f40cf85156b7b41bf76a3 - - name: Free Disk Space (Ubuntu) - uses: AdityaGarg8/remove-unwanted-software@90e01b21170618765a73370fcc3abbd1684a7793 - with: - remove-dotnet: true - remove-haskell: true - remove-codeql: true - remove-docker-images: true - remove-large-packages: true - - name: Prepare AVD home on /mnt - # GitHub-hosted runners mount a ~74GB volume at /mnt. Create it before AVD cache - # restore and android-emulator-runner (avdmanager needs the space at create time). - run: | - sudo mkdir -p /mnt/avd - sudo chown "$USER:$USER" /mnt/avd - df -h / /mnt - - name: AVD cache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - id: avd-cache - with: - # Must match the save path exactly - path: | - /mnt/avd/* - ~/.android/adb* - # Same AVD as the platform workflows, so deliberately the same key - - # this job reuses their image instead of building a second copy. - # The emulator build is part of the key so bumping `emulator-build:` below - # invalidates the cached AVD instead of reusing an image from the old build. - key: avd-${{ runner.os }}-${{ env.AVD_API_LEVEL }}-${{ env.AVD_TARGET }}-${{ env.AVD_ARCH }}-14214601 - - name: Link AVD home to /mnt - # android-emulator-runner exportVariables ANDROID_AVD_HOME to $HOME/.android/avd - run: | - mkdir -p "$HOME/.android" - rm -rf "$HOME/.android/avd" - ln -s /mnt/avd "$HOME/.android/avd" - - name: Pre-build APK - # Build outside the emulator so the AVD does not boot and idle through the - # whole Gradle build. `flutter test` below reuses this warm build cache. - working-directory: packages/firebase_storage/firebase_storage/example - timeout-minutes: 25 - run: flutter build apk --debug --target=integration_test/e2e_test.dart --android-skip-build-dependency-validation - - name: Start AVD then run E2E tests - uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a - timeout-minutes: 20 - env: - ANDROID_AVD_HOME: /mnt/avd - STORAGE_EMULATOR_DEBUG: 'true' - with: - api-level: ${{ env.AVD_API_LEVEL }} - target: ${{ env.AVD_TARGET }} - arch: ${{ env.AVD_ARCH }} - emulator-build: 14214601 - # The default (true) wipes and recreates the AVD, making the cache above useless. - force-avd-creation: false - # firebase.json and the emulator rule files live here, so `emulators:exec` - # has to run from this directory; the test command cds back to the - # package under test. - working-directory: .github/workflows/scripts - # `emulators:exec` owns the emulator lifecycle: it boots the suite, runs - # the command and tears the suite down, exiting with the command's exit - # code. Nothing is left running between steps. - script: | - firebase emulators:exec --project flutterfire-e2e-tests "cd ${GITHUB_WORKSPACE}/packages/firebase_storage/firebase_storage/example && flutter test integration_test/e2e_test.dart --timeout 10x --dart-define=CI=true -d emulator-5554" - - name: Ensure Appium is shut down - # Required because of below issue where emulator failing to shut down properly causes tests to fail - # https://github.com/ReactiveCircus/android-emulator-runner/issues/385 - run: | - pgrep -f appium && pkill -f appium || echo "No Appium process found" - - name: Save Firestore Emulator Cache - # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. - if: github.ref == 'refs/heads/main' - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - key: ${{ steps.firebase-emulator-cache.outputs.cache-primary-key }} - # Must match the restore path exactly - path: ~/.cache/firebase/emulators - - name: Save Android Emulator Cache - # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. - # Skip on a cache hit: the AVD image is multi-GB and re-uploading it unchanged on - # every main run is pure waste. - if: github.ref == 'refs/heads/main' && steps.avd-cache.outputs.cache-hit != 'true' - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - with: - key: ${{ steps.avd-cache.outputs.cache-primary-key }} - # Must match the restore path exactly - path: | - /mnt/avd/* - ~/.android/adb* + uses: ./.github/workflows/reusable_e2e_android.yaml + with: + package-path: 'packages/firebase_storage/firebase_storage' + package-scope: 'firebase_storage*' + native-config-args: '--storage-native' + storage-emulator-debug: true + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} ios: needs: changes if: needs.changes.outputs.ios == 'true' - permissions: - contents: read - runs-on: macos-15 - timeout-minutes: ${{ inputs.nightly_test_mode && 5 || 35 }} - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - - name: Xcode - # Firebase iOS SDK: minimum Xcode 26.2. - run: sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer - - uses: ./.github/actions/setup-flutterfire - with: - flutter-version: '3.41.9' - firebase-tools-version: '15.25.1' - bootstrap-scope: 'firebase_storage*' - # This job is the repository's iOS Swift Package Manager coverage for - # firebase_storage. Every plugin the example depends on (firebase_core, - # firebase_storage, image_picker_ios) ships a Package.swift, so the whole - # app resolves under SPM. The macOS job below stays on CocoaPods, so both - # dependency managers are exercised. - - name: Enable Swift Package Manager for iOS - run: flutter config --enable-swift-package-manager - - uses: hendrikmuhs/ccache-action@d62db5f07c26379fc4b4e0916f098a92573c3b03 - name: Xcode Compile Cache - with: - # Distinct from every other macos-15 job's key, otherwise the workflows - # clobber each other's cache. - key: xcode-ccache-storage-ios - save: "${{ github.ref == 'refs/heads/main' }}" - max-size: 700M - - name: Firebase Emulator Cache - id: firebase-emulator-cache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - # Must match the save path exactly - path: ~/.cache/firebase/emulators - key: firebase-emulators-v5-${{ env.FIREBASE_TOOLS_VERSION }} - # Trailing dash so this never prefix-matches a future version's key - restore-keys: firebase-emulators-v5- - - name: Generate dummy Firebase configs - # `--storage-native` is required here: the example's Xcode project lists - # GoogleService-Info.plist in its Resources build phase, so the build - # fails when the file is missing. - run: dart ./.github/workflows/scripts/generate-dummy-firebase-configs.dart --storage-native - - name: Prepare iOS project for Swift Package Manager - # Done here rather than in the repository: the committed Podfile is what - # CocoaPods users of the example rely on, and `flutter build` prefers - # CocoaPods whenever a Podfile is present. - working-directory: packages/firebase_storage/firebase_storage/example/ios - run: | - if [ -f Podfile ]; then pod deintegrate; fi - rm -f Podfile Podfile.lock - rm -rf Pods - - name: 'Build Application' - working-directory: packages/firebase_storage/firebase_storage/example - timeout-minutes: 25 - run: | - export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH" - export CCACHE_SLOPPINESS=clang_index_store,file_stat_matches,include_file_ctime,include_file_mtime,ivfsoverlay,pch_defines,modules,system_headers,time_macros - export CCACHE_FILECLONE=true - export CCACHE_DEPEND=true - export CCACHE_INODECACHE=true - ccache -s - flutter build ios --no-codesign --simulator --debug --target=./integration_test/e2e_test.dart --dart-define=CI=true - ccache -s - - name: Install Cloud Functions dependencies - # `firebase emulators:exec` does not install these itself (the old - # start-firebase-emulator.sh wrapper did); without them the functions - # emulator fails to load the function definitions. - # Chown the npm cache directory to the runner user to avoid permission issues. - run: sudo chown -R 501:20 "/Users/runner/.npm" && npm ci --prefix .github/workflows/scripts/functions - - uses: futureware-tech/simulator-action@e89aa8f93d3aec35083ff49d2854d07f7186f7f5 - id: simulator - with: - # List of available simulators: https://github.com/actions/runner-images/blob/main/images/macos/macos-15-Readme.md#installed-simulators - model: "iPhone 16" - - name: Ensure Simulator Ready - timeout-minutes: 13 - env: - SIMULATOR: ${{ steps.simulator.outputs.udid }} - ENSURE_BOOT_IF_NEEDED: "0" - run: .github/workflows/scripts/ensure-simulator-ready.sh - - name: 'E2E Tests' - # firebase.json and the emulator rule files live here, so `emulators:exec` - # has to run from this directory; the test command cds back to the package - # under test. - working-directory: ./.github/workflows/scripts - # Covers booting the emulator suite as well as the tests themselves, since - # `emulators:exec` now owns both. - timeout-minutes: 20 - env: - SIMULATOR: ${{ steps.simulator.outputs.udid }} - STORAGE_EMULATOR_DEBUG: 'true' - run: | - # `emulators:exec` boots the suite, runs the command and tears the suite - # down, exiting with the command's exit code. - firebase emulators:exec --project flutterfire-e2e-tests "cd ${GITHUB_WORKSPACE}/packages/firebase_storage/firebase_storage/example && flutter test integration_test/e2e_test.dart -d \"$SIMULATOR\" --timeout 10x --dart-define=CI=true" - - name: Save Firestore Emulator Cache - # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. - if: github.ref == 'refs/heads/main' - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - key: ${{ steps.firebase-emulator-cache.outputs.cache-primary-key }} - # Must match the restore path exactly - path: ~/.cache/firebase/emulators + uses: ./.github/workflows/reusable_e2e_ios.yaml + with: + package-path: 'packages/firebase_storage/firebase_storage' + package-scope: 'firebase_storage*' + cache-key-suffix: 'storage' + native-config-args: '--storage-native' + storage-emulator-debug: true + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} macos: needs: changes if: needs.changes.outputs.macos == 'true' - permissions: - contents: read - runs-on: macos-15 - timeout-minutes: ${{ inputs.nightly_test_mode && 5 || 35 }} - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - - name: Xcode - # Firebase iOS SDK: minimum Xcode 26.2. - run: sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer - - uses: ./.github/actions/setup-flutterfire - with: - flutter-version: '3.41.9' - firebase-tools-version: '15.25.1' - bootstrap-scope: 'firebase_storage*' - - uses: hendrikmuhs/ccache-action@d62db5f07c26379fc4b4e0916f098a92573c3b03 - name: Xcode Compile Cache - with: - # Distinct from every other macos-15 job's key, otherwise the workflows - # clobber each other's cache. - key: xcode-ccache-storage-macos - save: "${{ github.ref == 'refs/heads/main' }}" - max-size: 700M - - name: Pods Cache - continue-on-error: true - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - id: pods-cache - with: - # Must match the save path exactly - path: packages/firebase_storage/firebase_storage/example/macos/Pods - # Keyed on the Podfile and the pinned Firebase SDK version, not on a - # pubspec.lock: those are gitignored, so hashFiles() returns an empty - # string and the key could never be invalidated. - key: pods-v1-${{ runner.os }}-storage-macos-${{ hashFiles('packages/firebase_storage/firebase_storage/example/macos/Podfile', 'packages/firebase_core/firebase_core/ios/firebase_sdk_version.rb') }} - restore-keys: pods-v1-${{ runner.os }}-storage-macos- - - name: Firebase Emulator Cache - id: firebase-emulator-cache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - # Must match the save path exactly - path: ~/.cache/firebase/emulators - key: firebase-emulators-v5-${{ env.FIREBASE_TOOLS_VERSION }} - # Trailing dash so this never prefix-matches a future version's key - restore-keys: firebase-emulators-v5- - - name: Generate dummy Firebase configs - # `--storage-native` is required here: the example's Xcode project lists - # GoogleService-Info.plist in its Resources build phase, so the build - # fails when the file is missing. - run: dart ./.github/workflows/scripts/generate-dummy-firebase-configs.dart --storage-native - - name: 'Build Application' - working-directory: packages/firebase_storage/firebase_storage/example - timeout-minutes: 25 - run: | - export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH" - export CCACHE_SLOPPINESS=clang_index_store,file_stat_matches,include_file_ctime,include_file_mtime,ivfsoverlay,pch_defines,modules,system_headers,time_macros - export CCACHE_FILECLONE=true - export CCACHE_DEPEND=true - export CCACHE_INODECACHE=true - ccache -s - flutter build macos --debug --target=./integration_test/e2e_test.dart --device-id=macos --dart-define=CI=true - ccache -s - - name: Install Cloud Functions dependencies - # `firebase emulators:exec` does not install these itself (the old - # start-firebase-emulator.sh wrapper did); without them the functions - # emulator fails to load the function definitions. - # Chown the npm cache directory to the runner user to avoid permission issues. - run: sudo chown -R 501:20 "/Users/runner/.npm" && npm ci --prefix .github/workflows/scripts/functions - - name: 'E2E Tests' - # firebase.json and the emulator rule files live here, so `emulators:exec` - # has to run from this directory; the test command cds back to the package - # under test. - working-directory: ./.github/workflows/scripts - # Covers booting the emulator suite as well as the tests themselves, since - # `emulators:exec` now owns both. - timeout-minutes: 20 - env: - STORAGE_EMULATOR_DEBUG: 'true' - run: | - # The test command is handed to `emulators:exec` as a single string; a - # quoted heredoc keeps the script below verbatim instead of forcing a - # layer of quote escaping onto it. - TEST_COMMAND=$(cat <<'EOF' - cd "${GITHUB_WORKSPACE}/packages/firebase_storage/firebase_storage/example" - # flutter test on macOS CI may exit 1 due to "Failed to foreground app" - # even when all tests pass. Check actual test results to determine success. - set +e - OUTPUT=$(flutter test \ - integration_test/e2e_test.dart \ - -d macos \ - --dart-define=CI=true \ - --timeout 10x 2>&1) - EXIT_CODE=$? - echo "$OUTPUT" - if [ $EXIT_CODE -ne 0 ]; then - if echo "$OUTPUT" | grep -q "Some tests failed" || echo "$OUTPUT" | grep -q "test failed"; then - exit 1 - fi - if echo "$OUTPUT" | grep -q "tests passed"; then - echo "All tests passed but flutter test exited with $EXIT_CODE (likely 'Failed to foreground app'). Treating as success." - exit 0 - fi - exit $EXIT_CODE - fi - EOF - ) - # `emulators:exec` boots the suite, runs the command and tears the suite - # down, exiting with the command's exit code - so the laundering above - # still decides whether this step passes. - firebase emulators:exec --project flutterfire-e2e-tests "$TEST_COMMAND" - - name: Save Firestore Emulator Cache - continue-on-error: true - # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. - if: github.ref == 'refs/heads/main' - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - key: ${{ steps.firebase-emulator-cache.outputs.cache-primary-key }} - # Must match the restore path exactly - path: ~/.cache/firebase/emulators - - name: Save Pods Cache - continue-on-error: true - # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. - if: github.ref == 'refs/heads/main' - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - with: - key: ${{ steps.pods-cache.outputs.cache-primary-key }} - # Must match the restore path exactly - path: packages/firebase_storage/firebase_storage/example/macos/Pods + uses: ./.github/workflows/reusable_e2e_macos.yaml + with: + package-path: 'packages/firebase_storage/firebase_storage' + package-scope: 'firebase_storage*' + cache-key-suffix: 'storage' + native-config-args: '--storage-native' + storage-emulator-debug: true + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} web: needs: changes if: needs.changes.outputs.web == 'true' - permissions: - contents: read - runs-on: ubuntu-latest - timeout-minutes: ${{ inputs.nightly_test_mode && 5 || 25 }} - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - - uses: ./.github/actions/setup-flutterfire - timeout-minutes: 15 - with: - firebase-tools-version: '15.25.1' - bootstrap-scope: 'firebase_storage*' - # The ubuntu runner image ships Google Chrome and a chromedriver build - # that is matched to it — that pairing IS our pinning strategy, so we use - # the image binaries rather than downloading our own. - - name: 'Set up Chrome and chromedriver' - run: | - echo "$CHROMEWEBDRIVER" >> "$GITHUB_PATH" - google-chrome --version - "$CHROMEWEBDRIVER/chromedriver" --version - - name: Generate dummy Firebase configs - run: dart ./.github/workflows/scripts/generate-dummy-firebase-configs.dart - - name: Firebase Emulator Cache - id: firebase-emulator-cache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - # Must match the save path exactly - path: ~/.cache/firebase/emulators - key: firebase-emulators-v5-${{ env.FIREBASE_TOOLS_VERSION }} - restore-keys: firebase-emulators-v5- - - name: Install Cloud Functions dependencies - # `firebase emulators:exec` does not install these itself (the old - # start-firebase-emulator.sh wrapper did); without them the functions - # emulator fails to load the function definitions. - run: npm ci --prefix .github/workflows/scripts/functions - - name: 'E2E Tests' - # Covers booting the emulator suite as well as the tests themselves, since - # `emulators:exec` now owns both. - timeout-minutes: 20 - # firebase.json and the emulator rule files live here, so `emulators:exec` - # has to run from this directory; the test command cds back to the - # package under test. - working-directory: ./.github/workflows/scripts - # Web devices are not supported for the `flutter test` command yet. As a - # workaround we can use the `flutter drive` command. Tracking issue: - # https://github.com/flutter/flutter/issues/66264 - # The retry script only retries infrastructure startup failures - # (timeouts, AppConnectionException, "Failed to exit Chromium"); real - # test/compile failures fail fast. It also owns the chromedriver - # lifecycle, so nothing here starts chromedriver. - # The retry script reads its FLUTTER_DRIVE_* configuration from the - # environment, which `emulators:exec` passes through to the command. - env: - FLUTTER_DRIVE_TARGET: './integration_test/e2e_test.dart' - FLUTTER_DRIVE_DRIVER: './test_driver/integration_test.dart' - FLUTTER_DRIVE_EXTRA_ARGS: '--dart-define=CI=true' - STORAGE_EMULATOR_DEBUG: 'true' - run: | - firebase emulators:exec --project flutterfire-e2e-tests "cd ${GITHUB_WORKSPACE}/packages/firebase_storage/firebase_storage/example && ${GITHUB_WORKSPACE}/.github/workflows/scripts/flutter-drive-web-retry.sh" - - name: Save Firestore Emulator Cache - # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. - if: github.ref == 'refs/heads/main' - continue-on-error: true - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - key: ${{ steps.firebase-emulator-cache.outputs.cache-primary-key }} - # Must match the restore path exactly - path: ~/.cache/firebase/emulators + uses: ./.github/workflows/reusable_e2e_web.yaml + with: + package-path: 'packages/firebase_storage/firebase_storage' + package-scope: 'firebase_storage*' + native-config-args: '--storage-native' + storage-emulator-debug: true + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} windows: needs: changes - permissions: - contents: read - runs-on: windows-latest - # Storage used to get its Windows coverage from the `tests` app's aggregated - # `e2e_test.dart`; that entry is gone, so the coverage moves here with the - # rest of the suite. Skipped in nightly test mode, where the 5 minute budget - # cannot fit a Windows build (same as `windows-firestore`). - if: ${{ !inputs.nightly_test_mode && needs.changes.outputs.windows == 'true' }} - timeout-minutes: 45 - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - - uses: ./.github/actions/setup-flutterfire - with: - npm-cache: 'false' - bootstrap-scope: 'firebase_storage*' - - name: Generate dummy Firebase configs - run: dart ./.github/workflows/scripts/generate-dummy-firebase-configs.dart - - name: "Install Tools" - # Not the composite action's firebase-tools install: that one uses `sudo npm`, - # which does not exist on the Windows runners. - run: | - npm install -g firebase-tools@15.25.1 - - name: Install Cloud Functions dependencies - # `firebase emulators:exec` does not install these itself; without them the - # functions emulator logs "Failed to load function definition" on every run. - run: npm ci --prefix .github/workflows/scripts/functions - - name: Start Firebase Emulator and run tests - timeout-minutes: 30 - env: - STORAGE_EMULATOR_DEBUG: 'true' - run: | - cd ./.github/workflows/scripts - firebase emulators:exec --project flutterfire-e2e-tests "cd ../../../packages/firebase_storage/firebase_storage/example && flutter drive --target=.\integration_test\e2e_test.dart --driver=.\test_driver\integration_test.dart -d windows --verbose" 2>&1 | Tee-Object -FilePath output.log - $exitCode = $LASTEXITCODE - $output = Get-Content output.log -Raw - if ($output -match '\[E\]' -or $output -match 'Some tests failed') { - Write-Error "All tests did not pass. Please check the logs for more information." - exit 1 - } - exit $exitCode + if: needs.changes.outputs.windows == 'true' + uses: ./.github/workflows/reusable_e2e_windows.yaml + with: + package-path: 'packages/firebase_storage/firebase_storage' + package-scope: 'firebase_storage*' + native-config-args: '--storage-native' + storage-emulator-debug: true + nightly_test_mode: ${{ inputs.nightly_test_mode == true }} diff --git a/.github/workflows/ios.yaml b/.github/workflows/ios.yaml deleted file mode 100644 index 5e67456421d3..000000000000 --- a/.github/workflows/ios.yaml +++ /dev/null @@ -1,192 +0,0 @@ -name: e2e-iOS - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }}-ios - cancel-in-progress: true - -permissions: - contents: read - -on: - pull_request: - paths-ignore: - - 'docs/**' - - 'website/**' - - '**/example/**' - - '!**/example/integration_test/**' - - '**/flutterfire_ui/**' - - '**.md' - push: - branches: - - main - paths-ignore: - - 'docs/**' - - 'website/**' - - '**/example/**' - - '!**/example/integration_test/**' - - '**/flutterfire_ui/**' - - '**.md' - workflow_call: - inputs: - nightly_test_mode: - type: boolean - default: false - -jobs: - ios: - name: ios (${{ matrix.suite.name }}) - permissions: - contents: read - runs-on: macos-15 - timeout-minutes: ${{ inputs.nightly_test_mode && 5 || 35 }} - strategy: - fail-fast: false - matrix: - # The `tests` app is sharded by product suite so a hang or flake costs - # one small job instead of the whole run. `integration_test/e2e_test.dart` - # still aggregates every suite for Windows and local runs. - suite: - - name: core_misc - working_directory: tests - target: integration_test/shards/core_misc_shard_test.dart - scope: tests - - name: firestore - working_directory: packages/cloud_firestore/cloud_firestore/example - target: integration_test/e2e_test.dart - scope: 'cloud_firestore*' - exclude: - # Nightly drops the firestore example leg. The entry is repeated in - # full because matrix `exclude` compares the whole object; only `name` - # is switched, so outside nightly it matches nothing. - - suite: - name: ${{ inputs.nightly_test_mode && 'firestore' || 'none' }} - working_directory: packages/cloud_firestore/cloud_firestore/example - target: integration_test/e2e_test.dart - scope: 'cloud_firestore*' - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - - name: Xcode - # Firebase iOS SDK: minimum Xcode 26.2. - run: sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer - - uses: ./.github/actions/setup-flutterfire - with: - flutter-version: '3.41.9' - firebase-tools-version: '15.25.1' - # Each matrix leg bootstraps only the packages it exercises. - bootstrap-scope: ${{ matrix.suite.scope }} - - uses: hendrikmuhs/ccache-action@d62db5f07c26379fc4b4e0916f098a92573c3b03 - name: Xcode Compile Cache - with: - # Distinct from the macOS workflow's key: both run on macos-15, so a shared - # `xcode-cache-${{ runner.os }}` had the two workflows clobbering each other. - key: xcode-ccache-ios - save: "${{ github.ref == 'refs/heads/main' }}" - max-size: 700M - - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - name: Pods Cache - id: pods-cache - with: - # Must match the save path exactly - path: tests/ios/Pods - # Keyed on the Podfile and the pinned Firebase SDK version, not tests/pubspec.lock: - # that file is gitignored, so hashFiles() returned an empty string and the key - # never changed - the cache could never be invalidated. - key: pods-v4-${{ runner.os }}-ios-${{ hashFiles('tests/ios/Podfile', 'packages/firebase_core/firebase_core/ios/firebase_sdk_version.rb') }} - restore-keys: pods-v4-${{ runner.os }}-ios- - - name: Firebase Emulator Cache - id: firebase-emulator-cache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - # Must match the save path exactly - path: ~/.cache/firebase/emulators - key: firebase-emulators-v5-${{ env.FIREBASE_TOOLS_VERSION }} - # Trailing dash so this never prefix-matches a future version's key - restore-keys: firebase-emulators-v5- - - name: Generate dummy Firebase configs - run: dart ./.github/workflows/scripts/generate-dummy-firebase-configs.dart --firestore-native - - name: 'Free up space' - run: | - sudo rm -rf \ - /usr/local/share/.cache \ - /opt/microsoft/msedge \ - /opt/microsoft/powershell \ - /opt/pipx \ - /usr/lib/mono \ - /usr/local/julia* \ - /usr/local/lib/android \ - /usr/local/share/chromium \ - /usr/local/share/powershell \ - /usr/share/dotnet - df -h / - - name: 'Build Application' - working-directory: ${{ matrix.suite.working_directory }} - timeout-minutes: 25 - run: | - export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH" - export CCACHE_SLOPPINESS=clang_index_store,file_stat_matches,include_file_ctime,include_file_mtime,ivfsoverlay,pch_defines,modules,system_headers,time_macros - export CCACHE_FILECLONE=true - export CCACHE_DEPEND=true - export CCACHE_INODECACHE=true - ccache -s - flutter build ios --no-codesign --simulator --debug --target=./${{ matrix.suite.target }} --dart-define=CI=true - ccache -s - - name: Install Cloud Functions dependencies - # `firebase emulators:exec` does not install these itself (the old - # start-firebase-emulator.sh wrapper did); without them the functions - # emulator fails to load the function definitions. - # Chown the npm cache directory to the runner user to avoid permission issues. - run: sudo chown -R 501:20 "/Users/runner/.npm" && npm ci --prefix .github/workflows/scripts/functions - - uses: futureware-tech/simulator-action@e89aa8f93d3aec35083ff49d2854d07f7186f7f5 - id: simulator - with: - # List of available simulators: https://github.com/actions/runner-images/blob/main/images/macos/macos-15-Readme.md#installed-simulators - model: "iPhone 16" - - name: Ensure Simulator Ready - timeout-minutes: 13 - env: - SIMULATOR: ${{ steps.simulator.outputs.udid }} - ENSURE_BOOT_IF_NEEDED: "0" - run: .github/workflows/scripts/ensure-simulator-ready.sh - - name: 'E2E Tests' - # firebase.json and the emulator rule files live here, so `emulators:exec` - # has to run from this directory; the test command cds back to the package - # under test. - working-directory: ./.github/workflows/scripts - # Covers booting the emulator suite as well as the tests themselves, since - # `emulators:exec` now owns both. - timeout-minutes: 20 - env: - SIMULATOR: ${{ steps.simulator.outputs.udid }} - STORAGE_EMULATOR_DEBUG: 'true' - run: | - # Uncomment following line to have simulator logs printed out for debugging purposes. - # xcrun simctl spawn booted log stream --predicate 'eventMessage contains "flutter"' & - # Once the integration test runner has launched, leave it running rather - # than starting a second app instance from a retry. - # `emulators:exec` boots the suite, runs the command and tears the suite - # down, exiting with the command's exit code. - firebase emulators:exec --project flutterfire-e2e-tests "cd ${GITHUB_WORKSPACE}/${{ matrix.suite.working_directory }} && flutter test ${{ matrix.suite.target }} -d \"$SIMULATOR\" --timeout 10x --dart-define=CI=true" - - name: Save Firestore Emulator Cache - # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. - if: github.ref == 'refs/heads/main' - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - key: ${{ steps.firebase-emulator-cache.outputs.cache-primary-key }} - # Must match the restore paths exactly - path: ~/.cache/firebase/emulators - - name: Save Pods Cache - # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. - if: github.ref == 'refs/heads/main' - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - with: - key: ${{ steps.pods-cache.outputs.cache-primary-key }} - # Must match the restore paths exactly - path: tests/ios/Pods diff --git a/.github/workflows/macos.yaml b/.github/workflows/macos.yaml deleted file mode 100644 index 3fc489db9ddd..000000000000 --- a/.github/workflows/macos.yaml +++ /dev/null @@ -1,190 +0,0 @@ -name: e2e-macOS - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }}-macos - cancel-in-progress: true - -permissions: - contents: read - -on: - pull_request: - paths-ignore: - - 'docs/**' - - 'website/**' - - '**/example/**' - - '!**/example/integration_test/**' - - '**/flutterfire_ui/**' - - '**.md' - push: - branches: - - main - paths-ignore: - - 'docs/**' - - 'website/**' - - '**/example/**' - - '!**/example/integration_test/**' - - '**/flutterfire_ui/**' - - '**.md' - workflow_call: - inputs: - nightly_test_mode: - type: boolean - default: false - -jobs: - macos: - name: macos (${{ matrix.suite.name }}) - permissions: - contents: read - runs-on: macos-15 - timeout-minutes: ${{ inputs.nightly_test_mode && 5 || 35 }} - strategy: - fail-fast: false - matrix: - # The `tests` app is sharded by product suite so a hang or flake costs - # one small job instead of the whole run. `integration_test/e2e_test.dart` - # still aggregates every suite for Windows and local runs. - suite: - - name: core_misc - working_directory: tests - target: integration_test/shards/core_misc_shard_test.dart - scope: tests - - name: firestore - working_directory: packages/cloud_firestore/cloud_firestore/example - target: integration_test/e2e_test.dart - scope: 'cloud_firestore*' - exclude: - # Nightly drops the firestore example leg. The entry is repeated in - # full because matrix `exclude` compares the whole object; only `name` - # is switched, so outside nightly it matches nothing. - - suite: - name: ${{ inputs.nightly_test_mode && 'firestore' || 'none' }} - working_directory: packages/cloud_firestore/cloud_firestore/example - target: integration_test/e2e_test.dart - scope: 'cloud_firestore*' - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - - name: Xcode - # Firebase iOS SDK: minimum Xcode 26.2. - run: sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer - - uses: ./.github/actions/setup-flutterfire - with: - flutter-version: '3.41.9' - firebase-tools-version: '15.25.1' - # Each matrix leg bootstraps only the packages it exercises. - bootstrap-scope: ${{ matrix.suite.scope }} - - uses: hendrikmuhs/ccache-action@d62db5f07c26379fc4b4e0916f098a92573c3b03 - name: Xcode Compile Cache - with: - # Distinct from the iOS workflow's key: both run on macos-15, so a shared - # `xcode-cache-${{ runner.os }}` had the two workflows clobbering each other. - key: xcode-ccache-macos - save: "${{ github.ref == 'refs/heads/main' }}" - max-size: 700M - - name: Pods Cache - continue-on-error: true - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - id: pods-cache - with: - # Must match the save path exactly - path: tests/macos/Pods - # Keyed on the Podfile and the pinned Firebase SDK version, not tests/pubspec.lock: - # that file is gitignored, so hashFiles() returned an empty string and the key - # never changed - the cache could never be invalidated. - key: pods-v4-${{ runner.os }}-macos-${{ hashFiles('tests/macos/Podfile', 'packages/firebase_core/firebase_core/ios/firebase_sdk_version.rb') }} - restore-keys: pods-v4-${{ runner.os }}-macos- - - name: Firebase Emulator Cache - id: firebase-emulator-cache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - # Must match the save path exactly - path: ~/.cache/firebase/emulators - key: firebase-emulators-v5-${{ env.FIREBASE_TOOLS_VERSION }} - # Trailing dash so this never prefix-matches a future version's key - restore-keys: firebase-emulators-v5- - - name: Generate dummy Firebase configs - run: dart ./.github/workflows/scripts/generate-dummy-firebase-configs.dart --firestore-native - - name: 'Build Application' - working-directory: ${{ matrix.suite.working_directory }} - timeout-minutes: 25 - run: | - export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH" - export CCACHE_SLOPPINESS=clang_index_store,file_stat_matches,include_file_ctime,include_file_mtime,ivfsoverlay,pch_defines,modules,system_headers,time_macros - export CCACHE_FILECLONE=true - export CCACHE_DEPEND=true - export CCACHE_INODECACHE=true - ccache -s - flutter build macos --debug --target=./${{ matrix.suite.target }} --device-id=macos --dart-define=CI=true - ccache -s - - name: Install Cloud Functions dependencies - # `firebase emulators:exec` does not install these itself (the old - # start-firebase-emulator.sh wrapper did); without them the functions - # emulator fails to load the function definitions. - # Chown the npm cache directory to the runner user to avoid permission issues. - run: sudo chown -R 501:20 "/Users/runner/.npm" && npm ci --prefix .github/workflows/scripts/functions - - name: 'E2E Tests' - # firebase.json and the emulator rule files live here, so `emulators:exec` - # has to run from this directory; the test command cds back to the package - # under test. - working-directory: ./.github/workflows/scripts - # Covers booting the emulator suite as well as the tests themselves, since - # `emulators:exec` now owns both. - timeout-minutes: 20 - env: - STORAGE_EMULATOR_DEBUG: 'true' - run: | - # The test command is handed to `emulators:exec` as a single string; a - # quoted heredoc keeps the script below verbatim instead of forcing a - # layer of quote escaping onto it. - TEST_COMMAND=$(cat <<'EOF' - cd "${GITHUB_WORKSPACE}/${{ matrix.suite.working_directory }}" - # flutter test on macOS CI may exit 1 due to "Failed to foreground app" - # even when all tests pass. Check actual test results to determine success. - set +e - OUTPUT=$(flutter test \ - ${{ matrix.suite.target }} \ - -d macos \ - --dart-define=CI=true \ - --timeout 10x 2>&1) - EXIT_CODE=$? - echo "$OUTPUT" - if [ $EXIT_CODE -ne 0 ]; then - if echo "$OUTPUT" | grep -q "Some tests failed" || echo "$OUTPUT" | grep -q "test failed"; then - exit 1 - fi - if echo "$OUTPUT" | grep -q "tests passed"; then - echo "All tests passed but flutter test exited with $EXIT_CODE (likely 'Failed to foreground app'). Treating as success." - exit 0 - fi - exit $EXIT_CODE - fi - EOF - ) - # `emulators:exec` boots the suite, runs the command and tears the suite - # down, exiting with the command's exit code - so the laundering above - # still decides whether this step passes. - firebase emulators:exec --project flutterfire-e2e-tests "$TEST_COMMAND" - - name: Save Firestore Emulator Cache - continue-on-error: true - # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. - if: github.ref == 'refs/heads/main' - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - key: ${{ steps.firebase-emulator-cache.outputs.cache-primary-key }} - # Must match the restore path exactly - path: ~/.cache/firebase/emulators - - name: Save Pods Cache - continue-on-error: true - # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. - if: github.ref == 'refs/heads/main' - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - with: - key: ${{ steps.pods-cache.outputs.cache-primary-key }} - # Must match the restore paths exactly - path: tests/macos/Pods diff --git a/.github/workflows/nightly.yaml b/.github/workflows/nightly.yaml index 273122f680c3..36e46139b28b 100644 --- a/.github/workflows/nightly.yaml +++ b/.github/workflows/nightly.yaml @@ -20,26 +20,17 @@ permissions: contents: read jobs: - # Only e2e-pipeline needs secrets; the rest run entirely against emulators - # or dummy configs, so nothing is inherited. - e2e-android: - uses: ./.github/workflows/android.yaml + # The emulator-backed workflows (smoke, firestore, fdc, storage, auth, + # database, functions) need no secrets. The nine live-tier product + # workflows and e2e-pipeline talk to real Firebase projects, so they get + # their config passed explicitly - never `secrets: inherit`, which would + # hand every called workflow the whole secret store. + e2e-smoke: + uses: ./.github/workflows/e2e_tests_smoke.yaml with: nightly_test_mode: ${{ github.event.inputs.test_mode == 'true' }} - e2e-ios: - uses: ./.github/workflows/ios.yaml - with: - nightly_test_mode: ${{ github.event.inputs.test_mode == 'true' }} - e2e-macos: - uses: ./.github/workflows/macos.yaml - with: - nightly_test_mode: ${{ github.event.inputs.test_mode == 'true' }} - e2e-web: - uses: ./.github/workflows/web.yaml - with: - nightly_test_mode: ${{ github.event.inputs.test_mode == 'true' }} - e2e-windows: - uses: ./.github/workflows/windows.yaml + e2e-firestore: + uses: ./.github/workflows/e2e_tests_firestore.yaml with: nightly_test_mode: ${{ github.event.inputs.test_mode == 'true' }} e2e-fdc: @@ -54,6 +45,9 @@ jobs: uses: ./.github/workflows/e2e_tests_auth.yaml with: nightly_test_mode: ${{ github.event.inputs.test_mode == 'true' }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} e2e-database: uses: ./.github/workflows/e2e_tests_database.yaml with: @@ -62,6 +56,69 @@ jobs: uses: ./.github/workflows/e2e_tests_functions.yaml with: nightly_test_mode: ${{ github.event.inputs.test_mode == 'true' }} + e2e-ai: + uses: ./.github/workflows/e2e_tests_ai.yaml + with: + nightly_test_mode: ${{ github.event.inputs.test_mode == 'true' }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} + e2e-analytics: + uses: ./.github/workflows/e2e_tests_analytics.yaml + with: + nightly_test_mode: ${{ github.event.inputs.test_mode == 'true' }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} + e2e-app-check: + uses: ./.github/workflows/e2e_tests_app_check.yaml + with: + nightly_test_mode: ${{ github.event.inputs.test_mode == 'true' }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} + e2e-app-installations: + uses: ./.github/workflows/e2e_tests_app_installations.yaml + with: + nightly_test_mode: ${{ github.event.inputs.test_mode == 'true' }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} + e2e-crashlytics: + uses: ./.github/workflows/e2e_tests_crashlytics.yaml + with: + nightly_test_mode: ${{ github.event.inputs.test_mode == 'true' }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} + e2e-messaging: + uses: ./.github/workflows/e2e_tests_messaging.yaml + with: + nightly_test_mode: ${{ github.event.inputs.test_mode == 'true' }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} + e2e-ml-model-downloader: + uses: ./.github/workflows/e2e_tests_ml_model_downloader.yaml + with: + nightly_test_mode: ${{ github.event.inputs.test_mode == 'true' }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} + e2e-performance: + uses: ./.github/workflows/e2e_tests_performance.yaml + with: + nightly_test_mode: ${{ github.event.inputs.test_mode == 'true' }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} + e2e-remote-config: + uses: ./.github/workflows/e2e_tests_remote_config.yaml + with: + nightly_test_mode: ${{ github.event.inputs.test_mode == 'true' }} + secrets: + TESTS_E2E_FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + TESTS_E2E_GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} e2e-pipeline: uses: ./.github/workflows/e2e_tests_pipeline.yaml if: ${{ github.event.inputs.test_mode != 'true' }} @@ -78,7 +135,14 @@ jobs: contents: read issues: write if: ${{ always() && !cancelled() }} - needs: [e2e-android, e2e-ios, e2e-macos, e2e-web, e2e-windows, e2e-fdc, e2e-storage, e2e-auth, e2e-database, e2e-functions, e2e-pipeline] + needs: + [ + e2e-smoke, e2e-firestore, e2e-fdc, + e2e-storage, e2e-auth, e2e-database, e2e-functions, e2e-pipeline, + e2e-ai, e2e-analytics, e2e-app-check, e2e-app-installations, + e2e-crashlytics, e2e-messaging, e2e-ml-model-downloader, + e2e-performance, e2e-remote-config, + ] steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 @@ -88,17 +152,23 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TEST_MODE: ${{ github.event.inputs.test_mode == 'true' }} - ANDROID_STATUS: ${{ needs.e2e-android.result }} - IOS_STATUS: ${{ needs.e2e-ios.result }} - MACOS_STATUS: ${{ needs.e2e-macos.result }} - WEB_STATUS: ${{ needs.e2e-web.result }} - WINDOWS_STATUS: ${{ needs.e2e-windows.result }} + SMOKE_STATUS: ${{ needs.e2e-smoke.result }} + FIRESTORE_STATUS: ${{ needs.e2e-firestore.result }} FDC_STATUS: ${{ needs.e2e-fdc.result }} STORAGE_STATUS: ${{ needs.e2e-storage.result }} AUTH_STATUS: ${{ needs.e2e-auth.result }} DATABASE_STATUS: ${{ needs.e2e-database.result }} FUNCTIONS_STATUS: ${{ needs.e2e-functions.result }} PIPELINE_STATUS: ${{ needs.e2e-pipeline.result }} + AI_STATUS: ${{ needs.e2e-ai.result }} + ANALYTICS_STATUS: ${{ needs.e2e-analytics.result }} + APP_CHECK_STATUS: ${{ needs.e2e-app-check.result }} + APP_INSTALLATIONS_STATUS: ${{ needs.e2e-app-installations.result }} + CRASHLYTICS_STATUS: ${{ needs.e2e-crashlytics.result }} + MESSAGING_STATUS: ${{ needs.e2e-messaging.result }} + ML_MODEL_DOWNLOADER_STATUS: ${{ needs.e2e-ml-model-downloader.result }} + PERFORMANCE_STATUS: ${{ needs.e2e-performance.result }} + REMOTE_CONFIG_STATUS: ${{ needs.e2e-remote-config.result }} REPO: ${{ github.repository }} run: | dart .github/workflows/scripts/nightly_issue_dashboard.dart diff --git a/.github/workflows/reusable_e2e_android.yaml b/.github/workflows/reusable_e2e_android.yaml new file mode 100644 index 000000000000..c9c7bd5a0ddb --- /dev/null +++ b/.github/workflows/reusable_e2e_android.yaml @@ -0,0 +1,279 @@ +name: reusable-e2e-android + +# The Android half of the per-product e2e suite: build the example APK, boot a +# cached AVD and run the integration tests on it. Called by every +# `e2e_tests_.yaml` - every product supports Android. +# +# Two tiers, selected by `inject-config-secrets`: emulator (config generated, +# tests wrapped in `firebase emulators:exec`) and live (config injected from +# repository secrets, job fork/dependabot guarded). See +# `reusable_e2e_changes.yaml` for how the job is gated on the diff. + +on: + workflow_call: + inputs: + package-path: + description: >- + Path of the package under test, e.g. + 'packages/firebase_storage/firebase_storage'. The example app is + `/example` by convention. + type: string + required: true + package-scope: + description: "Melos glob bootstrapped before the tests, e.g. 'firebase_storage*'." + type: string + required: true + test-target: + description: 'Integration test entrypoint, relative to the example directory.' + type: string + default: 'integration_test/e2e_test.dart' + native-config-args: + description: >- + Arguments for generate-dummy-firebase-configs.dart, e.g. + '--storage-native' (emulator tier, writes the native config files the + Android/Xcode builds require) or '--live-tier-plist=messaging' (live + tier, writes only the placeholder plist so it cannot clobber the + injected credentials). Empty = plain run. + type: string + default: '' + inject-config-secrets: + description: >- + true selects the live tier: the "Inject Firebase config" step writes + firebase_options.dart / google-services.json from secrets, no emulator + is started, and the job is fork/dependabot guarded. + type: boolean + default: false + use-firebase-emulators: + description: >- + true (default) runs the test command under `firebase emulators:exec` + with the emulator suite installed and cached. Independent of + inject-config-secrets since a product can need both: firebase_auth + tests run against the Auth emulator but its password-policy tests + call the live REST API, which rejects placeholder credentials. + type: boolean + default: true + storage-emulator-debug: + description: >- + Sets STORAGE_EMULATOR_DEBUG on the test steps. firebase_storage only. + type: boolean + default: false + flutter-version: + description: >- + Pinned Flutter version. Empty (the default) leaves the job unpinned + (stable channel), which is what every product uses today. + type: string + default: '' + firebase-tools-version: + description: 'firebase-tools version. Emulator tier only.' + type: string + default: '15.25.1' + nightly_test_mode: + description: >- + Passed down from nightly.yaml. Shortens the job timeout to 5 minutes. + type: boolean + default: false + secrets: + # `required: false` so the emulator-tier callers can omit the `secrets:` + # block entirely. The live-tier callers declare them as required and pass + # them through explicitly. + TESTS_E2E_FIREBASE_OPTIONS_DART: + required: false + TESTS_E2E_GOOGLE_SERVICES_JSON: + required: false + +permissions: + contents: read + +jobs: + android: + # Live tier requires the live-project secrets, which fork and dependabot + # PRs do not receive. + if: >- + inputs.inject-config-secrets == false || + github.event_name != 'pull_request' || + (github.event.pull_request.head.repo.full_name == github.repository && + github.actor != 'dependabot[bot]') + permissions: + contents: read + runs-on: ubuntu-latest + env: + # Inputs consumed inside run: scripts are routed through env so their + # values are data, never shell/template code (zizmor: template-injection). + PACKAGE_PATH: ${{ inputs.package-path }} + TEST_TARGET: ${{ inputs.test-target }} + NATIVE_CONFIG_ARGS: ${{ inputs.native-config-args }} + AVD_ARCH: x86_64 + AVD_API_LEVEL: 34 + AVD_TARGET: google_apis + timeout-minutes: ${{ inputs.nightly_test_mode && 5 || 35 }} + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 + - uses: ./.github/actions/setup-flutterfire + with: + # Live tier has no emulator suite, so no firebase-tools and no Node. + # Java stays: the Gradle build needs it either way. + node: ${{ inputs.use-firebase-emulators && 'true' || 'false' }} + flutter-version: ${{ inputs.flutter-version }} + firebase-tools-version: ${{ inputs.use-firebase-emulators && inputs.firebase-tools-version || '' }} + bootstrap-scope: ${{ inputs.package-scope }} + - name: Inject Firebase config + if: inputs.inject-config-secrets + # The `flutterfire-e2e-tests` project config lives in repository + # secrets rather than in the tree: this is a public repo and these are + # live backends. Same shape as `e2e_tests_pipeline.yaml`. + env: + FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} + run: | + if [ -z "$FIREBASE_OPTIONS_DART" ]; then + echo "::error::TESTS_E2E_FIREBASE_OPTIONS_DART is empty — secrets are unavailable (fork/dependabot PR?). Failing early instead of producing a broken build." + exit 1 + fi + echo "$FIREBASE_OPTIONS_DART" > "$PACKAGE_PATH/example/lib/firebase_options.dart" + echo "$GOOGLE_SERVICES_JSON" > "$PACKAGE_PATH/example/android/app/google-services.json" + - name: Firebase Emulator Cache + if: inputs.use-firebase-emulators + id: firebase-emulator-cache + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + continue-on-error: true + with: + # The firebase emulators are pure javascript and java, OS-independent + enableCrossOsArchive: true + # Must match the save path exactly + path: ~/.cache/firebase/emulators + # Shared with every other e2e workflow: same emulator payload, same key. + key: firebase-emulators-v5-${{ env.FIREBASE_TOOLS_VERSION }} + # Trailing dash so this never prefix-matches a future version's key + restore-keys: firebase-emulators-v5- + - name: Generate dummy Firebase configs + if: ${{ !inputs.inject-config-secrets }} + # The native flag (e.g. `--storage-native`) is required for products + # whose example applies the `google-services` plugin: the Android build + # fails when google-services.json is missing. + run: | + # shellcheck disable=SC2086 # an argument list: word-splitting is the point + dart ./.github/workflows/scripts/generate-dummy-firebase-configs.dart $NATIVE_CONFIG_ARGS + - name: Install Cloud Functions dependencies + if: inputs.use-firebase-emulators + # `firebase emulators:exec` does not install these itself (the old + # start-firebase-emulator.sh wrapper did); without them the functions + # emulator fails to load the function definitions. + run: npm ci --prefix .github/workflows/scripts/functions + - name: Enable KVM + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + - name: Gradle cache + uses: gradle/actions/setup-gradle@90ddb51e90a5fd9ba75f40cf85156b7b41bf76a3 + - name: Free Disk Space (Ubuntu) + uses: AdityaGarg8/remove-unwanted-software@90e01b21170618765a73370fcc3abbd1684a7793 + with: + remove-dotnet: true + remove-haskell: true + remove-codeql: true + remove-docker-images: true + remove-large-packages: true + - name: Prepare AVD home on /mnt + # GitHub-hosted runners mount a ~74GB volume at /mnt. Create it before AVD cache + # restore and android-emulator-runner (avdmanager needs the space at create time). + run: | + sudo mkdir -p /mnt/avd + sudo chown "$USER:$USER" /mnt/avd + df -h / /mnt + - name: AVD cache + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + continue-on-error: true + id: avd-cache + with: + # Must match the save path exactly + path: | + /mnt/avd/* + ~/.android/adb* + # Deliberately shared across products: every android job wants the same + # image, so they reuse one copy instead of building their own. + # The emulator build is part of the key so bumping `emulator-build:` below + # invalidates the cached AVD instead of reusing an image from the old build. + key: avd-${{ runner.os }}-${{ env.AVD_API_LEVEL }}-${{ env.AVD_TARGET }}-${{ env.AVD_ARCH }}-14214601 + - name: Link AVD home to /mnt + # android-emulator-runner exportVariables ANDROID_AVD_HOME to $HOME/.android/avd + run: | + mkdir -p "$HOME/.android" + rm -rf "$HOME/.android/avd" + ln -s /mnt/avd "$HOME/.android/avd" + - name: Pre-build APK + # Build outside the emulator so the AVD does not boot and idle through the + # whole Gradle build. `flutter test` below reuses this warm build cache. + working-directory: ${{ inputs.package-path }}/example + timeout-minutes: 25 + # Gradle artifact downloads flake; retry exactly that failure once. + run: | + set +e + flutter build apk --debug --target="$TEST_TARGET" --android-skip-build-dependency-validation > build_output.log 2>&1 + BUILD_EXIT=$? + cat build_output.log + if [ $BUILD_EXIT -ne 0 ] && grep -q "error while downloading artifacts from the network" build_output.log; then + echo "Gradle artifact download failure - retrying the build once." + flutter build apk --debug --target="$TEST_TARGET" --android-skip-build-dependency-validation + BUILD_EXIT=$? + fi + exit $BUILD_EXIT + - name: Start AVD then run E2E tests + uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a + # Two alarm-bounded test attempts (15 min each, see the retry script) + # plus AVD boot and the emulator suite have to fit; the old 20 minute + # budget was killed by a single launch hang. + timeout-minutes: 40 + env: + ANDROID_AVD_HOME: /mnt/avd + STORAGE_EMULATOR_DEBUG: ${{ inputs.storage-emulator-debug && 'true' || '' }} + with: + api-level: ${{ env.AVD_API_LEVEL }} + target: ${{ env.AVD_TARGET }} + arch: ${{ env.AVD_ARCH }} + emulator-build: 14214601 + # The default (true) wipes and recreates the AVD, making the cache above useless. + force-avd-creation: false + # Emulator tier: firebase.json and the emulator rule files live in the + # scripts directory, so `emulators:exec` has to run from there and the + # test command cds back to the package under test. `emulators:exec` + # owns the emulator lifecycle - it boots the suite, runs the command + # and tears the suite down, exiting with the command's exit code. + # Live tier: no emulator, so the test runs straight from the example. + # Both tiers run the retry wrapper: `flutter test` against the AVD + # occasionally hangs at app launch without printing anything, and the + # wrapper bounds each attempt and retries that shape once. + working-directory: ${{ inputs.use-firebase-emulators && '.github/workflows/scripts' || format('{0}/example', inputs.package-path) }} + script: >- + ${{ inputs.use-firebase-emulators + && format('firebase emulators:exec --project flutterfire-e2e-tests "cd {0}/{1}/example && {0}/.github/workflows/scripts/flutter-test-android-retry.sh"', github.workspace, inputs.package-path) + || format('{0}/.github/workflows/scripts/flutter-test-android-retry.sh', github.workspace) }} + - name: Ensure Appium is shut down + # Required because of below issue where emulator failing to shut down properly causes tests to fail + # https://github.com/ReactiveCircus/android-emulator-runner/issues/385 + run: | + pgrep -f appium && pkill -f appium || echo "No Appium process found" + - name: Save Firebase Emulator Cache + # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. + if: ${{ inputs.use-firebase-emulators && github.ref == 'refs/heads/main' }} + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + continue-on-error: true + with: + # The firebase emulators are pure javascript and java, OS-independent + enableCrossOsArchive: true + key: ${{ steps.firebase-emulator-cache.outputs.cache-primary-key }} + # Must match the restore path exactly + path: ~/.cache/firebase/emulators + - name: Save Android Emulator Cache + # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. + # Skip on a cache hit: the AVD image is multi-GB and re-uploading it unchanged on + # every main run is pure waste. + if: github.ref == 'refs/heads/main' && steps.avd-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + continue-on-error: true + with: + key: ${{ steps.avd-cache.outputs.cache-primary-key }} + # Must match the restore path exactly + path: | + /mnt/avd/* + ~/.android/adb* diff --git a/.github/workflows/reusable_e2e_changes.yaml b/.github/workflows/reusable_e2e_changes.yaml new file mode 100644 index 000000000000..1bae82f22105 --- /dev/null +++ b/.github/workflows/reusable_e2e_changes.yaml @@ -0,0 +1,133 @@ +name: reusable-e2e-changes + +# Maps changed paths to affected platforms so a Kotlin-only change runs only +# the android job, a Swift-only change only ios/macos, etc. Dart code, the +# tests themselves, firebase_core and CI plumbing affect every platform. +# Non-PR events (push to main, the nightly workflow_call) always run +# everything: the filter step is skipped and its empty outputs fall back to +# 'true' below. +# +# Every `e2e_tests_.yaml` calls this once and gates its platform jobs +# on the outputs. Platforms the product does not support are simply absent from +# the caller, so they never render a check at all. + +on: + workflow_call: + inputs: + package-path: + description: >- + Path of the package under test, e.g. + 'packages/firebase_storage/firebase_storage'. The example app is + `/example` by convention. + type: string + required: true + web-package: + description: >- + Name of the web implementation package, e.g. 'firebase_storage_web'. + Only used to build the `web` paths-filter. Empty means the product has + no web package (firebase_ai). + type: string + default: '' + platform-interface-package: + description: >- + Name of the platform interface package, e.g. + 'firebase_storage_platform_interface'. Only used to build the shared + paths-filter. Empty means the product has none (firebase_ai). + type: string + default: '' + inject-config-secrets: + description: >- + true selects the live tier, whose secrets fork and dependabot PRs do + not receive - this job is then fork/dependabot guarded, and skipping + it skips every platform job that needs its outputs. + type: boolean + default: false + outputs: + android: + description: 'true when the diff can affect the Android build or tests.' + value: ${{ jobs.changes.outputs.android }} + ios: + description: 'true when the diff can affect the iOS build or tests.' + value: ${{ jobs.changes.outputs.ios }} + macos: + description: 'true when the diff can affect the macOS build or tests.' + value: ${{ jobs.changes.outputs.macos }} + web: + description: 'true when the diff can affect the web (JS or wasm) build or tests.' + value: ${{ jobs.changes.outputs.web }} + windows: + description: 'true when the diff can affect the Windows build or tests.' + value: ${{ jobs.changes.outputs.windows }} + +permissions: + contents: read + +jobs: + changes: + permissions: + contents: read + runs-on: ubuntu-latest + timeout-minutes: 5 + # Live tier requires the live-project secrets, which fork and dependabot + # PRs do not receive. + if: >- + inputs.inject-config-secrets == false || + github.event_name != 'pull_request' || + (github.event.pull_request.head.repo.full_name == github.repository && + github.actor != 'dependabot[bot]') + outputs: + android: ${{ steps.filter.outputs.android || 'true' }} + ios: ${{ steps.filter.outputs.ios || 'true' }} + macos: ${{ steps.filter.outputs.macos || 'true' }} + web: ${{ steps.filter.outputs.web || 'true' }} + windows: ${{ steps.filter.outputs.windows || 'true' }} + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 + if: github.event_name == 'pull_request' + - uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 + if: github.event_name == 'pull_request' + id: filter + with: + # The optional package entries fall back to a path that cannot exist + # rather than being omitted: an expression can pick a list *item*, not + # whether the item is there at all. + # `packages/*/` avoids needing the product directory as a + # separate input - plugin package names are unique repo-wide. + # Both this file and the caller are in `shared`: a change to either + # must run everything. + filters: | + shared: &shared + - 'packages/firebase_core/**' + - '${{ inputs.package-path }}/lib/**' + - '${{ inputs.package-path }}/pubspec.yaml' + - '${{ inputs.platform-interface-package && format('packages/*/{0}/**', inputs.platform-interface-package) || 'packages/__none__/**' }}' + - '${{ inputs.package-path }}/example/integration_test/**' + - '${{ inputs.package-path }}/example/test_driver/**' + - '${{ inputs.package-path }}/example/lib/**' + - '${{ inputs.package-path }}/example/pubspec.yaml' + - '.github/workflows/reusable_e2e_*.yaml' + - '.github/workflows/e2e_tests_*.yaml' + - '.github/actions/setup-flutterfire/**' + - '.github/workflows/scripts/**' + android: + - *shared + - '${{ inputs.package-path }}/android/**' + - '${{ inputs.package-path }}/example/android/**' + ios: + - *shared + - '${{ inputs.package-path }}/ios/**' + - '${{ inputs.package-path }}/darwin/**' + - '${{ inputs.package-path }}/example/ios/**' + macos: + - *shared + - '${{ inputs.package-path }}/macos/**' + - '${{ inputs.package-path }}/darwin/**' + - '${{ inputs.package-path }}/example/macos/**' + web: + - *shared + - '${{ inputs.web-package && format('packages/*/{0}/**', inputs.web-package) || 'packages/__none__/**' }}' + - '${{ inputs.package-path }}/example/web/**' + windows: + - *shared + - '${{ inputs.package-path }}/windows/**' + - '${{ inputs.package-path }}/example/windows/**' diff --git a/.github/workflows/reusable_e2e_ios.yaml b/.github/workflows/reusable_e2e_ios.yaml new file mode 100644 index 000000000000..99804f055750 --- /dev/null +++ b/.github/workflows/reusable_e2e_ios.yaml @@ -0,0 +1,475 @@ +name: reusable-e2e-ios + +# The iOS half of the per-product e2e suite: build the example for the +# simulator and run the integration tests on it. Called by every +# `e2e_tests_.yaml` - every product supports iOS. +# +# This job is the repository's Swift Package Manager coverage (`ios-spm`, +# default true); the macOS job is always CocoaPods, so both dependency managers +# stay exercised. + +on: + workflow_call: + inputs: + package-path: + description: >- + Path of the package under test, e.g. + 'packages/firebase_storage/firebase_storage'. The example app is + `/example` by convention. + type: string + required: true + package-scope: + description: "Melos glob bootstrapped before the tests, e.g. 'firebase_storage*'." + type: string + required: true + test-target: + description: 'Integration test entrypoint, relative to the example directory.' + type: string + default: 'integration_test/e2e_test.dart' + native-config-args: + description: >- + Arguments for generate-dummy-firebase-configs.dart, e.g. + '--storage-native' (emulator tier, writes the native config files the + Android/Xcode builds require) or '--live-tier-plist=messaging' (live + tier, writes only the placeholder plist so it cannot clobber the + injected credentials). Empty = plain run. + type: string + default: '' + inject-config-secrets: + description: >- + true selects the live tier: the "Inject Firebase config" step writes + firebase_options.dart / google-services.json from secrets, no emulator + is started, and the job is fork/dependabot guarded. + type: boolean + default: false + use-firebase-emulators: + description: >- + true (default) runs the test command under `firebase emulators:exec` + with the emulator suite installed and cached. Independent of + inject-config-secrets since a product can need both: firebase_auth + tests run against the Auth emulator but its password-policy tests + call the live REST API, which rejects placeholder credentials. + type: boolean + default: true + storage-emulator-debug: + description: >- + Sets STORAGE_EMULATOR_DEBUG on the test steps. firebase_storage only. + type: boolean + default: false + flutter-version: + description: >- + Pinned Flutter version. Empty (the default) leaves the job unpinned + (stable channel), which is what every product uses today. + type: string + default: '' + firebase-tools-version: + description: 'firebase-tools version. Emulator tier only.' + type: string + default: '15.25.1' + cache-key-suffix: + description: >- + Short product token that makes the ccache/Pods cache keys unique, e.g. + 'storage'. Two products sharing a suffix would clobber each other's + caches, so this is required rather than defaulted. + type: string + required: true + ios-spm: + description: >- + true = iOS builds under Swift Package Manager (the Podfile is removed + first, so `flutter build` does not prefer CocoaPods). + false = iOS stays on CocoaPods and gets a Pods cache, as + firebase_crashlytics must (its Xcode project runs + "${PODS_ROOT}/FirebaseCrashlytics/run") and as cloud_functions + and firebase_performance currently must (Flutter's + experimental SPM integration cannot resolve their examples on + the CI Xcode). + The macOS job is always CocoaPods, so both dependency managers stay + covered either way. + type: boolean + default: true + ios-free-up-space: + description: >- + Runs the aggressive `rm -rf` of preinstalled toolchains before the iOS + build. Only cloud_firestore needs it (its build fills the runner disk). + type: boolean + default: false + nightly_test_mode: + description: >- + Passed down from nightly.yaml. Shortens the job timeout to 5 minutes. + type: boolean + default: false + secrets: + # `required: false` so the emulator-tier callers can omit the `secrets:` + # block entirely. The live-tier callers declare them as required and pass + # them through explicitly. + TESTS_E2E_FIREBASE_OPTIONS_DART: + required: false + TESTS_E2E_GOOGLE_SERVICES_JSON: + required: false + +permissions: + contents: read + +jobs: + ios: + # Live tier requires the live-project secrets, which fork and dependabot + # PRs do not receive. + if: >- + inputs.inject-config-secrets == false || + github.event_name != 'pull_request' || + (github.event.pull_request.head.repo.full_name == github.repository && + github.actor != 'dependabot[bot]') + permissions: + contents: read + runs-on: macos-15 + env: + # Inputs consumed inside run: scripts are routed through env so their + # values are data, never shell/template code (zizmor: template-injection). + PACKAGE_PATH: ${{ inputs.package-path }} + TEST_TARGET: ${{ inputs.test-target }} + NATIVE_CONFIG_ARGS: ${{ inputs.native-config-args }} + timeout-minutes: ${{ inputs.nightly_test_mode && 5 || 60 }} + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 + # No `xcode-select` pin: use the runner image's default Xcode. Firebase's + # iOS SDK minimum is 26.2 and every current macos-15 image is at or above + # it. The pin to exactly 26.2 was the reason SPM resolution kept failing: + # that toolchain canonicalizes the ephemeral symlink farm Flutter lays out + # for plugin packages and then looks for the siblings at their real paths + # ("Could not resolve package dependencies ... ios/firebase_core does not + # exist"); newer Xcode resolves the same layout. It is also why + # cloud_functions and firebase_performance are on CocoaPods for now - they + # can rejoin SPM once the default-Xcode runs prove clean. + - uses: ./.github/actions/setup-flutterfire + with: + node: ${{ inputs.use-firebase-emulators && 'true' || 'false' }} + java: ${{ inputs.use-firebase-emulators && 'true' || 'false' }} + flutter-version: ${{ inputs.flutter-version }} + firebase-tools-version: ${{ inputs.use-firebase-emulators && inputs.firebase-tools-version || '' }} + bootstrap-scope: ${{ inputs.package-scope }} + - name: Enable Swift Package Manager for iOS + # This job is the repository's iOS Swift Package Manager coverage for the + # product: every plugin the example depends on ships a Package.swift, so + # the whole app resolves under SPM. The macOS job stays on CocoaPods, so + # both dependency managers are exercised. + if: inputs.ios-spm + run: flutter config --enable-swift-package-manager + - name: Disable Swift Package Manager for iOS + # Recent stable Flutter enables SPM by default, which turns intended + # CocoaPods builds into hybrid ones. This job's CocoaPods coverage is + # deliberate (ios-spm: false), so opt out explicitly. + if: ${{ !inputs.ios-spm }} + run: flutter config --no-enable-swift-package-manager + - uses: hendrikmuhs/ccache-action@d62db5f07c26379fc4b4e0916f098a92573c3b03 + name: Xcode Compile Cache + with: + # Distinct from every other macos-15 job's key, otherwise the workflows + # clobber each other's cache. + key: xcode-ccache-${{ inputs.cache-key-suffix }}-ios + save: "${{ github.ref == 'refs/heads/main' }}" + max-size: 700M + - name: Firebase Emulator Cache + if: inputs.use-firebase-emulators + id: firebase-emulator-cache + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + continue-on-error: true + with: + # The firebase emulators are pure javascript and java, OS-independent + enableCrossOsArchive: true + # Must match the save path exactly + path: ~/.cache/firebase/emulators + key: firebase-emulators-v5-${{ env.FIREBASE_TOOLS_VERSION }} + # Trailing dash so this never prefix-matches a future version's key + restore-keys: firebase-emulators-v5- + - name: Inject Firebase config + if: inputs.inject-config-secrets + # The `flutterfire-e2e-tests` project config lives in repository + # secrets rather than in the tree: this is a public repo and these are + # live backends. Same shape as `e2e_tests_pipeline.yaml`. + env: + FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} + run: | + if [ -z "$FIREBASE_OPTIONS_DART" ]; then + echo "::error::TESTS_E2E_FIREBASE_OPTIONS_DART is empty — secrets are unavailable (fork/dependabot PR?). Failing early instead of producing a broken build." + exit 1 + fi + echo "$FIREBASE_OPTIONS_DART" > "$PACKAGE_PATH/example/lib/firebase_options.dart" + echo "$GOOGLE_SERVICES_JSON" > "$PACKAGE_PATH/example/android/app/google-services.json" + - name: Generate Firebase config files + # The example's Xcode project lists GoogleService-Info.plist in its + # Resources build phase, so the build fails outright when it is missing. + # On the live tier the file is a placeholder only - Firebase is + # initialised from the Dart options injected above, and + # `--live-tier-plist` writes that one file and nothing else, so it can + # never clobber the injected credentials. + if: ${{ !inputs.inject-config-secrets || inputs.native-config-args != '' }} + run: | + # shellcheck disable=SC2086 # an argument list: word-splitting is the point + dart ./.github/workflows/scripts/generate-dummy-firebase-configs.dart $NATIVE_CONFIG_ARGS + - name: 'Free up space' + if: inputs.ios-free-up-space + run: | + sudo rm -rf \ + /usr/local/share/.cache \ + /opt/microsoft/msedge \ + /opt/microsoft/powershell \ + /opt/pipx \ + /usr/lib/mono \ + /usr/local/julia* \ + /usr/local/lib/android \ + /usr/local/share/chromium \ + /usr/local/share/powershell \ + /usr/share/dotnet + df -h / + - name: Prepare iOS project for Swift Package Manager + # Done here rather than in the repository: the committed Podfile is what + # CocoaPods users of the example rely on, and `flutter build` prefers + # CocoaPods whenever a Podfile is present. + if: inputs.ios-spm + working-directory: ${{ inputs.package-path }}/example/ios + run: | + if [ -f Podfile ]; then pod deintegrate; fi + rm -f Podfile Podfile.lock + rm -rf Pods + - name: Pods Cache + if: ${{ !inputs.ios-spm }} + continue-on-error: true + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + id: pods-cache + with: + # Must match the save path exactly + path: ${{ inputs.package-path }}/example/ios/Pods + # Keyed on the Podfile and the pinned Firebase SDK version, not on a + # pubspec.lock: those are gitignored, so hashFiles() returns an empty + # string and the key could never be invalidated. + key: pods-v1-${{ runner.os }}-${{ inputs.cache-key-suffix }}-ios-${{ hashFiles(format('{0}/example/ios/Podfile', inputs.package-path), 'packages/firebase_core/firebase_core/ios/firebase_sdk_version.rb') }} + restore-keys: pods-v1-${{ runner.os }}-${{ inputs.cache-key-suffix }}-ios- + - name: 'Build Application' + working-directory: ${{ inputs.package-path }}/example + timeout-minutes: 25 + run: | + export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH" + export CCACHE_SLOPPINESS=clang_index_store,file_stat_matches,include_file_ctime,include_file_mtime,ivfsoverlay,pch_defines,modules,system_headers,time_macros + export CCACHE_FILECLONE=true + export CCACHE_DEPEND=true + export CCACHE_INODECACHE=true + ccache -s + # Flutter's SPM integration (experimental) intermittently resolves a + # symlinked plugin package at its real path, where the sibling + # `../firebase_core` path dependency does not exist ("Could not + # resolve package dependencies"). Retry exactly once, for exactly + # that error class - anything else fails immediately. + set +e + flutter build ios --no-codesign --simulator --debug --target="./$TEST_TARGET" --dart-define=CI=true > build_output.log 2>&1 + BUILD_EXIT=$? + cat build_output.log + if [ $BUILD_EXIT -ne 0 ] && grep -q "Could not resolve package dependencies" build_output.log; then + echo "SPM package resolution race detected; retrying the build once." + flutter clean > /dev/null + flutter build ios --no-codesign --simulator --debug --target="./$TEST_TARGET" --dart-define=CI=true + BUILD_EXIT=$? + fi + set -e + ccache -s + exit $BUILD_EXIT + - name: Install Cloud Functions dependencies + if: inputs.use-firebase-emulators + # `firebase emulators:exec` does not install these itself (the old + # start-firebase-emulator.sh wrapper did); without them the functions + # emulator fails to load the function definitions. + # Chown the npm cache directory to the runner user to avoid permission issues. + run: sudo chown -R 501:20 "/Users/runner/.npm" && npm ci --prefix .github/workflows/scripts/functions + - uses: futureware-tech/simulator-action@e89aa8f93d3aec35083ff49d2854d07f7186f7f5 + id: simulator + with: + # List of available simulators: https://github.com/actions/runner-images/blob/main/images/macos/macos-15-Readme.md#installed-simulators + model: "iPhone 16" + - name: Ensure Simulator Ready + timeout-minutes: 13 + env: + SIMULATOR: ${{ steps.simulator.outputs.udid }} + ENSURE_BOOT_IF_NEEDED: "0" + run: .github/workflows/scripts/ensure-simulator-ready.sh + - name: 'E2E Tests' + if: inputs.use-firebase-emulators + # firebase.json and the emulator rule files live here, so `emulators:exec` + # has to run from this directory; the test command cds back to the package + # under test. + working-directory: ./.github/workflows/scripts + # Covers booting the emulator suite as well as the tests themselves, since + # `emulators:exec` now owns both. + # Fits one cold (15 min) and two warm (10 min) alarm-bounded test + # attempts plus one simulator recycle. + timeout-minutes: 45 + env: + SIMULATOR: ${{ steps.simulator.outputs.udid }} + STORAGE_EMULATOR_DEBUG: ${{ inputs.storage-emulator-debug && 'true' || '' }} + run: | + TEST_COMMAND=$(cat <<'EOF' + cd "${GITHUB_WORKSPACE}/${PACKAGE_PATH}/example" + # flutter test on Apple simulators may exit 1 (e.g. "Failed to + # foreground app") even when every test passed - same behaviour the + # macOS job launders. Check the actual results to decide. + run_flutter_test() { + rm -f flutter_test_output.log + # File redirect, never capture (iOS spawns log streamers that hold + # a captured pipe open forever). The alarm ($1) bounds the known + # zero-output hang well below the step timeout. + perl -e 'alarm shift; exec @ARGV' "$1" \ + flutter test "$TEST_TARGET" -d "$SIMULATOR" --timeout 10x --dart-define=CI=true > flutter_test_output.log 2>&1 + FT_EXIT=$? + cat flutter_test_output.log + } + attempt_failed() { + { ! grep -Eq '[0-9]+ tests? passed' flutter_test_output.log && ! grep -Eq '[0-9]+ failed' flutter_test_output.log; } \ + || grep -q "Unable to start the app on the device" flutter_test_output.log + } + set +e + # Three bounded attempts. The zero-output hang is in the tool + # process, not the app or the device: the simulator log shows the + # app up and serving network traffic while `flutter test` waits + # forever for the VM service URI its log reader missed. Retries are + # therefore the effective lever; the first alarm covers the cold + # build, the later attempts run against a warm build cache. + run_flutter_test 900 + if attempt_failed; then + echo "Launch failure - retrying (attempt 2/3)." + echo "::group::Simulator log around the hang (Runner)" + perl -e 'alarm shift; exec @ARGV' 90 \ + xcrun simctl spawn "$SIMULATOR" log show --last 16m --style compact \ + --predicate 'process == "Runner" OR eventMessage CONTAINS "Runner"' 2>/dev/null | tail -80 || true + echo "::endgroup::" + run_flutter_test 600 + fi + if attempt_failed; then + # Last resort: recycle the simulator too, in case the device is + # the wedged part after all. + echo "Launch failure - recycling the simulator and retrying (attempt 3/3)." + xcrun simctl shutdown "$SIMULATOR" 2>/dev/null || true + xcrun simctl erase "$SIMULATOR" 2>/dev/null || true + xcrun simctl boot "$SIMULATOR" 2>/dev/null || true + "${GITHUB_WORKSPACE}/.github/workflows/scripts/ensure-simulator-ready.sh" || true + run_flutter_test 600 + fi + # Trust only the runner's own tally, parsed numerically. Substring + # checks are how '0 tests passed, 1 failed' once laundered into a + # green job. + PASSED=$(grep -Eo '[0-9]+ tests? passed' flutter_test_output.log | tail -1 | grep -Eo '^[0-9]+') + FAILED=$(grep -Eo '[0-9]+ failed' flutter_test_output.log | tail -1 | grep -Eo '^[0-9]+') + : "${PASSED:=0}"; : "${FAILED:=0}" + echo "Result tally: passed=$PASSED failed=$FAILED (flutter test exit $FT_EXIT)" + if [ "$FAILED" -gt 0 ] || [ "$PASSED" -eq 0 ]; then + exit 1 + fi + if [ "$FT_EXIT" -ne 0 ]; then + echo "All $PASSED tests passed; tolerating flutter test exit $FT_EXIT (simulator foreground quirk)." + fi + exit 0 + EOF + ) + # `emulators:exec` boots the suite, runs the command and tears the suite + # down, exiting with the command's exit code - the laundering above + # decides whether this step passes. + firebase emulators:exec --project flutterfire-e2e-tests "$TEST_COMMAND" + - name: 'E2E Tests' + if: ${{ !inputs.use-firebase-emulators }} + working-directory: ${{ inputs.package-path }}/example + # Fits one cold (15 min) and two warm (10 min) alarm-bounded test + # attempts plus one simulator recycle. + timeout-minutes: 45 + env: + SIMULATOR: ${{ steps.simulator.outputs.udid }} + run: | + # Same "exit 1 despite all tests passing" laundering as the + # emulator-tier step above and the macOS job. + run_flutter_test() { + rm -f flutter_test_output.log + # File redirect, never capture (iOS spawns log streamers that hold + # a captured pipe open forever). The alarm ($1) bounds the known + # zero-output hang well below the step timeout. + perl -e 'alarm shift; exec @ARGV' "$1" \ + flutter test "$TEST_TARGET" -d "$SIMULATOR" --timeout 10x --dart-define=CI=true > flutter_test_output.log 2>&1 + FT_EXIT=$? + cat flutter_test_output.log + } + attempt_failed() { + { ! grep -Eq '[0-9]+ tests? passed' flutter_test_output.log && ! grep -Eq '[0-9]+ failed' flutter_test_output.log; } \ + || grep -q "Unable to start the app on the device" flutter_test_output.log + } + set +e + # Three bounded attempts. The zero-output hang is in the tool + # process, not the app or the device: the simulator log shows the + # app up and serving network traffic while `flutter test` waits + # forever for the VM service URI its log reader missed. Retries are + # therefore the effective lever; the first alarm covers the cold + # build, the later attempts run against a warm build cache. + run_flutter_test 900 + if attempt_failed; then + echo "Launch failure - retrying (attempt 2/3)." + echo "::group::Simulator log around the hang (Runner)" + perl -e 'alarm shift; exec @ARGV' 90 \ + xcrun simctl spawn "$SIMULATOR" log show --last 16m --style compact \ + --predicate 'process == "Runner" OR eventMessage CONTAINS "Runner"' 2>/dev/null | tail -80 || true + echo "::endgroup::" + run_flutter_test 600 + fi + if attempt_failed; then + # Last resort: recycle the simulator too, in case the device is + # the wedged part after all. + echo "Launch failure - recycling the simulator and retrying (attempt 3/3)." + xcrun simctl shutdown "$SIMULATOR" 2>/dev/null || true + xcrun simctl erase "$SIMULATOR" 2>/dev/null || true + xcrun simctl boot "$SIMULATOR" 2>/dev/null || true + "${GITHUB_WORKSPACE}/.github/workflows/scripts/ensure-simulator-ready.sh" || true + run_flutter_test 600 + fi + # Trust only the runner's own tally, parsed numerically. Substring + # checks are how '0 tests passed, 1 failed' once laundered into a + # green job. + PASSED=$(grep -Eo '[0-9]+ tests? passed' flutter_test_output.log | tail -1 | grep -Eo '^[0-9]+') + FAILED=$(grep -Eo '[0-9]+ failed' flutter_test_output.log | tail -1 | grep -Eo '^[0-9]+') + : "${PASSED:=0}"; : "${FAILED:=0}" + echo "Result tally: passed=$PASSED failed=$FAILED (flutter test exit $FT_EXIT)" + if [ "$FAILED" -gt 0 ] || [ "$PASSED" -eq 0 ]; then + exit 1 + fi + if [ "$FT_EXIT" -ne 0 ]; then + echo "All $PASSED tests passed; tolerating flutter test exit $FT_EXIT (simulator foreground quirk)." + fi + exit 0 + - name: Dump crash reports + # The recurring "Unable to start the app on the device" launch failures + # give the Flutter tool nothing to print; simulator app crashes land in + # the host's crash reporter with the real reason. Only runs on failure. + if: failure() + run: | + found=0 + while IFS= read -r report; do + found=1 + echo "::group::$(basename "$report")" + head -c 20000 "$report" + echo + echo "::endgroup::" + done < <(find "$HOME/Library/Logs/DiagnosticReports" -name '*.ips' -newermt '-2 hours' 2>/dev/null) + [ "$found" -eq 1 ] || echo "No crash reports in the last two hours." + - name: Save Firebase Emulator Cache + # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. + if: ${{ inputs.use-firebase-emulators && github.ref == 'refs/heads/main' }} + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + continue-on-error: true + with: + # The firebase emulators are pure javascript and java, OS-independent + enableCrossOsArchive: true + key: ${{ steps.firebase-emulator-cache.outputs.cache-primary-key }} + # Must match the restore path exactly + path: ~/.cache/firebase/emulators + - name: Save Pods Cache + continue-on-error: true + # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. + if: ${{ !inputs.ios-spm && github.ref == 'refs/heads/main' }} + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + with: + key: ${{ steps.pods-cache.outputs.cache-primary-key }} + # Must match the restore path exactly + path: ${{ inputs.package-path }}/example/ios/Pods diff --git a/.github/workflows/reusable_e2e_macos.yaml b/.github/workflows/reusable_e2e_macos.yaml new file mode 100644 index 000000000000..10d28ebe79c0 --- /dev/null +++ b/.github/workflows/reusable_e2e_macos.yaml @@ -0,0 +1,340 @@ +name: reusable-e2e-macos + +# The macOS half of the per-product e2e suite: build the example as a desktop +# app and run the integration tests against it. Called by every +# `e2e_tests_.yaml` whose product ships a macOS implementation +# (everything except firebase_performance). +# +# Always CocoaPods - the other half of the SPM/CocoaPods split with the iOS job. + +on: + workflow_call: + inputs: + package-path: + description: >- + Path of the package under test, e.g. + 'packages/firebase_storage/firebase_storage'. The example app is + `/example` by convention. + type: string + required: true + package-scope: + description: "Melos glob bootstrapped before the tests, e.g. 'firebase_storage*'." + type: string + required: true + test-target: + description: 'Integration test entrypoint, relative to the example directory.' + type: string + default: 'integration_test/e2e_test.dart' + native-config-args: + description: >- + Arguments for generate-dummy-firebase-configs.dart, e.g. + '--storage-native' (emulator tier, writes the native config files the + Android/Xcode builds require) or '--live-tier-plist=messaging' (live + tier, writes only the placeholder plist so it cannot clobber the + injected credentials). Empty = plain run. + type: string + default: '' + inject-config-secrets: + description: >- + true selects the live tier: the "Inject Firebase config" step writes + firebase_options.dart / google-services.json from secrets, no emulator + is started, and the job is fork/dependabot guarded. + type: boolean + default: false + use-firebase-emulators: + description: >- + true (default) runs the test command under `firebase emulators:exec` + with the emulator suite installed and cached. Independent of + inject-config-secrets since a product can need both: firebase_auth + tests run against the Auth emulator but its password-policy tests + call the live REST API, which rejects placeholder credentials. + type: boolean + default: true + storage-emulator-debug: + description: >- + Sets STORAGE_EMULATOR_DEBUG on the test steps. firebase_storage only. + type: boolean + default: false + flutter-version: + description: >- + Pinned Flutter version. Empty (the default) leaves the job unpinned + (stable channel), which is what every product uses today. + type: string + default: '' + firebase-tools-version: + description: 'firebase-tools version. Emulator tier only.' + type: string + default: '15.25.1' + cache-key-suffix: + description: >- + Short product token that makes the ccache/Pods cache keys unique, e.g. + 'storage'. Two products sharing a suffix would clobber each other's + caches, so this is required rather than defaulted. + type: string + required: true + nightly_test_mode: + description: >- + Passed down from nightly.yaml. Shortens the job timeout to 5 minutes. + type: boolean + default: false + secrets: + # `required: false` so the emulator-tier callers can omit the `secrets:` + # block entirely. The live-tier callers declare them as required and pass + # them through explicitly. + TESTS_E2E_FIREBASE_OPTIONS_DART: + required: false + TESTS_E2E_GOOGLE_SERVICES_JSON: + required: false + +permissions: + contents: read + +jobs: + macos: + # Live tier requires the live-project secrets, which fork and dependabot + # PRs do not receive. + if: >- + inputs.inject-config-secrets == false || + github.event_name != 'pull_request' || + (github.event.pull_request.head.repo.full_name == github.repository && + github.actor != 'dependabot[bot]') + permissions: + contents: read + runs-on: macos-15 + env: + # Inputs consumed inside run: scripts are routed through env so their + # values are data, never shell/template code (zizmor: template-injection). + PACKAGE_PATH: ${{ inputs.package-path }} + TEST_TARGET: ${{ inputs.test-target }} + NATIVE_CONFIG_ARGS: ${{ inputs.native-config-args }} + timeout-minutes: ${{ inputs.nightly_test_mode && 5 || 60 }} + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 + # No `xcode-select` pin: use the runner image's default Xcode. Firebase's + # minimum is 26.2 and every current macos-15 image is at or above it. See + # `reusable_e2e_ios.yaml` for why pinning 26.2 exactly was harmful. + - uses: ./.github/actions/setup-flutterfire + with: + node: ${{ inputs.use-firebase-emulators && 'true' || 'false' }} + java: ${{ inputs.use-firebase-emulators && 'true' || 'false' }} + flutter-version: ${{ inputs.flutter-version }} + firebase-tools-version: ${{ inputs.use-firebase-emulators && inputs.firebase-tools-version || '' }} + bootstrap-scope: ${{ inputs.package-scope }} + - name: Disable Swift Package Manager + # Recent stable Flutter enables SPM by default, which turns intended + # CocoaPods builds into hybrid ones. macOS is this workflow's CocoaPods + # coverage by design, so opt out explicitly. + run: flutter config --no-enable-swift-package-manager + - uses: hendrikmuhs/ccache-action@d62db5f07c26379fc4b4e0916f098a92573c3b03 + name: Xcode Compile Cache + with: + # Distinct from every other macos-15 job's key, otherwise the workflows + # clobber each other's cache. + key: xcode-ccache-${{ inputs.cache-key-suffix }}-macos + save: "${{ github.ref == 'refs/heads/main' }}" + max-size: 700M + - name: Pods Cache + # macOS is always CocoaPods - that is the other half of the SPM/CocoaPods + # split with the iOS job. + continue-on-error: true + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + id: pods-cache + with: + # Must match the save path exactly + path: ${{ inputs.package-path }}/example/macos/Pods + # Keyed on the Podfile and the pinned Firebase SDK version, not on a + # pubspec.lock: those are gitignored, so hashFiles() returns an empty + # string and the key could never be invalidated. + key: pods-v1-${{ runner.os }}-${{ inputs.cache-key-suffix }}-macos-${{ hashFiles(format('{0}/example/macos/Podfile', inputs.package-path), 'packages/firebase_core/firebase_core/ios/firebase_sdk_version.rb') }} + restore-keys: pods-v1-${{ runner.os }}-${{ inputs.cache-key-suffix }}-macos- + - name: Firebase Emulator Cache + if: inputs.use-firebase-emulators + id: firebase-emulator-cache + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + continue-on-error: true + with: + # The firebase emulators are pure javascript and java, OS-independent + enableCrossOsArchive: true + # Must match the save path exactly + path: ~/.cache/firebase/emulators + key: firebase-emulators-v5-${{ env.FIREBASE_TOOLS_VERSION }} + # Trailing dash so this never prefix-matches a future version's key + restore-keys: firebase-emulators-v5- + - name: Inject Firebase config + if: inputs.inject-config-secrets + # The `flutterfire-e2e-tests` project config lives in repository + # secrets rather than in the tree: this is a public repo and these are + # live backends. Same shape as `e2e_tests_pipeline.yaml`. + env: + FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} + run: | + if [ -z "$FIREBASE_OPTIONS_DART" ]; then + echo "::error::TESTS_E2E_FIREBASE_OPTIONS_DART is empty — secrets are unavailable (fork/dependabot PR?). Failing early instead of producing a broken build." + exit 1 + fi + echo "$FIREBASE_OPTIONS_DART" > "$PACKAGE_PATH/example/lib/firebase_options.dart" + echo "$GOOGLE_SERVICES_JSON" > "$PACKAGE_PATH/example/android/app/google-services.json" + - name: Generate Firebase config files + # See the iOS job: the macOS Xcode project has the same + # GoogleService-Info.plist resource requirement. + if: ${{ !inputs.inject-config-secrets || inputs.native-config-args != '' }} + run: | + # shellcheck disable=SC2086 # an argument list: word-splitting is the point + dart ./.github/workflows/scripts/generate-dummy-firebase-configs.dart $NATIVE_CONFIG_ARGS + - name: 'Build Application' + working-directory: ${{ inputs.package-path }}/example + timeout-minutes: 25 + run: | + export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH" + export CCACHE_SLOPPINESS=clang_index_store,file_stat_matches,include_file_ctime,include_file_mtime,ivfsoverlay,pch_defines,modules,system_headers,time_macros + export CCACHE_FILECLONE=true + export CCACHE_DEPEND=true + export CCACHE_INODECACHE=true + ccache -s + flutter build macos --debug --target="./$TEST_TARGET" --device-id=macos --dart-define=CI=true + ccache -s + - name: Install Cloud Functions dependencies + if: inputs.use-firebase-emulators + # `firebase emulators:exec` does not install these itself (the old + # start-firebase-emulator.sh wrapper did); without them the functions + # emulator fails to load the function definitions. + # Chown the npm cache directory to the runner user to avoid permission issues. + run: sudo chown -R 501:20 "/Users/runner/.npm" && npm ci --prefix .github/workflows/scripts/functions + - name: 'E2E Tests' + if: inputs.use-firebase-emulators + # firebase.json and the emulator rule files live here, so `emulators:exec` + # has to run from this directory; the test command cds back to the + # package under test. + working-directory: ./.github/workflows/scripts + # Covers booting the emulator suite as well as the tests themselves, since + # `emulators:exec` now owns both. + # Fits two alarm-bounded (15 min) test attempts: the retry the + # wrapper performs is pointless if the step dies before it finishes. + timeout-minutes: 40 + env: + STORAGE_EMULATOR_DEBUG: ${{ inputs.storage-emulator-debug && 'true' || '' }} + run: | + # The test command is handed to `emulators:exec` as a single string; a + # quoted heredoc keeps the script below verbatim instead of forcing a + # layer of quote escaping onto it. + TEST_COMMAND=$(cat <<'EOF' + cd "${GITHUB_WORKSPACE}/${PACKAGE_PATH}/example" + run_flutter_test() { + rm -f flutter_test_output.log + # File redirect, never capture (iOS spawns log streamers that hold + # a captured pipe open forever). perl-alarm bounds the known + # zero-output startup hang well below the step timeout. + perl -e 'alarm shift; exec @ARGV' 900 \ + flutter test "$TEST_TARGET" -d macos --timeout 10x --dart-define=CI=true > flutter_test_output.log 2>&1 + FT_EXIT=$? + cat flutter_test_output.log + } + set +e + run_flutter_test + # Retry once for the two known infra failure shapes: the zero-output + # startup hang (no tally at all), and the macOS/iOS launch race + # ("The log reader stopped unexpectedly" -> "Unable to start the app + # on the device"), which fails before a single test executes. + if { ! grep -Eq '[0-9]+ tests? passed' flutter_test_output.log && ! grep -Eq '[0-9]+ failed' flutter_test_output.log; } \ + || grep -q "Unable to start the app on the device" flutter_test_output.log; then + echo "Infrastructure launch failure detected - retrying once." + run_flutter_test + fi + # Trust only the runner's own tally, parsed numerically. Substring + # checks are how '0 tests passed, 1 failed' once laundered into a + # green job. + PASSED=$(grep -Eo '[0-9]+ tests? passed' flutter_test_output.log | tail -1 | grep -Eo '^[0-9]+') + FAILED=$(grep -Eo '[0-9]+ failed' flutter_test_output.log | tail -1 | grep -Eo '^[0-9]+') + : "${PASSED:=0}"; : "${FAILED:=0}" + echo "Result tally: passed=$PASSED failed=$FAILED (flutter test exit $FT_EXIT)" + if [ "$FAILED" -gt 0 ] || [ "$PASSED" -eq 0 ]; then + exit 1 + fi + if [ "$FT_EXIT" -ne 0 ]; then + echo "All $PASSED tests passed; tolerating flutter test exit $FT_EXIT (simulator foreground quirk)." + fi + exit 0 + EOF + ) + # `emulators:exec` boots the suite, runs the command and tears the suite + # down, exiting with the command's exit code - so the laundering above + # still decides whether this step passes. + firebase emulators:exec --project flutterfire-e2e-tests "$TEST_COMMAND" + - name: 'E2E Tests' + if: ${{ !inputs.use-firebase-emulators }} + working-directory: ${{ inputs.package-path }}/example + # Fits two alarm-bounded (15 min) test attempts: the retry the + # wrapper performs is pointless if the step dies before it finishes. + timeout-minutes: 40 + run: | + run_flutter_test() { + rm -f flutter_test_output.log + # File redirect, never capture (iOS spawns log streamers that hold + # a captured pipe open forever). perl-alarm bounds the known + # zero-output startup hang well below the step timeout. + perl -e 'alarm shift; exec @ARGV' 900 \ + flutter test "$TEST_TARGET" -d macos --timeout 10x --dart-define=CI=true > flutter_test_output.log 2>&1 + FT_EXIT=$? + cat flutter_test_output.log + } + set +e + run_flutter_test + # Retry once for the two known infra failure shapes: the zero-output + # startup hang (no tally at all), and the macOS/iOS launch race + # ("The log reader stopped unexpectedly" -> "Unable to start the app + # on the device"), which fails before a single test executes. + if { ! grep -Eq '[0-9]+ tests? passed' flutter_test_output.log && ! grep -Eq '[0-9]+ failed' flutter_test_output.log; } \ + || grep -q "Unable to start the app on the device" flutter_test_output.log; then + echo "Infrastructure launch failure detected - retrying once." + run_flutter_test + fi + # Trust only the runner's own tally, parsed numerically. Substring + # checks are how '0 tests passed, 1 failed' once laundered into a + # green job. + PASSED=$(grep -Eo '[0-9]+ tests? passed' flutter_test_output.log | tail -1 | grep -Eo '^[0-9]+') + FAILED=$(grep -Eo '[0-9]+ failed' flutter_test_output.log | tail -1 | grep -Eo '^[0-9]+') + : "${PASSED:=0}"; : "${FAILED:=0}" + echo "Result tally: passed=$PASSED failed=$FAILED (flutter test exit $FT_EXIT)" + if [ "$FAILED" -gt 0 ] || [ "$PASSED" -eq 0 ]; then + exit 1 + fi + if [ "$FT_EXIT" -ne 0 ]; then + echo "All $PASSED tests passed; tolerating flutter test exit $FT_EXIT (simulator foreground quirk)." + fi + exit 0 + - name: Dump crash reports + # The recurring "Unable to start the app on the device" launch failures + # give the Flutter tool nothing to print; the macOS crash reporter has + # the real reason. Only runs on failure, so green jobs pay nothing. + if: failure() + run: | + found=0 + while IFS= read -r report; do + found=1 + echo "::group::$(basename "$report")" + head -c 20000 "$report" + echo + echo "::endgroup::" + done < <(find "$HOME/Library/Logs/DiagnosticReports" -name '*.ips' -newermt '-2 hours' 2>/dev/null) + [ "$found" -eq 1 ] || echo "No crash reports in the last two hours." + - name: Save Firebase Emulator Cache + continue-on-error: true + # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. + if: ${{ inputs.use-firebase-emulators && github.ref == 'refs/heads/main' }} + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + with: + # The firebase emulators are pure javascript and java, OS-independent + enableCrossOsArchive: true + key: ${{ steps.firebase-emulator-cache.outputs.cache-primary-key }} + # Must match the restore path exactly + path: ~/.cache/firebase/emulators + - name: Save Pods Cache + continue-on-error: true + # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. + if: github.ref == 'refs/heads/main' + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + with: + key: ${{ steps.pods-cache.outputs.cache-primary-key }} + # Must match the restore path exactly + path: ${{ inputs.package-path }}/example/macos/Pods diff --git a/.github/workflows/reusable_e2e_web.yaml b/.github/workflows/reusable_e2e_web.yaml new file mode 100644 index 000000000000..752a41e75caf --- /dev/null +++ b/.github/workflows/reusable_e2e_web.yaml @@ -0,0 +1,238 @@ +name: reusable-e2e-web + +# The web half of the per-product e2e suite: `flutter drive` the example under +# chromedriver. Called by every `e2e_tests_.yaml` whose product ships +# a web implementation (everything except firebase_crashlytics and +# firebase_ml_model_downloader). +# +# `wasm: true` selects the WebAssembly variant instead of the JS one; a product +# whose example ships `web/wasm_index.html` calls this workflow twice, once per +# variant (only cloud_firestore does today). + +on: + workflow_call: + inputs: + package-path: + description: >- + Path of the package under test, e.g. + 'packages/firebase_storage/firebase_storage'. The example app is + `/example` by convention. + type: string + required: true + package-scope: + description: "Melos glob bootstrapped before the tests, e.g. 'firebase_storage*'." + type: string + required: true + test-target: + description: 'Integration test entrypoint, relative to the example directory.' + type: string + default: 'integration_test/e2e_test.dart' + native-config-args: + description: >- + Arguments for generate-dummy-firebase-configs.dart, e.g. + '--storage-native' (emulator tier, writes the native config files the + Android/Xcode builds require) or '--live-tier-plist=messaging' (live + tier, writes only the placeholder plist so it cannot clobber the + injected credentials). Empty = plain run. + type: string + default: '' + inject-config-secrets: + description: >- + true selects the live tier: the "Inject Firebase config" step writes + firebase_options.dart / google-services.json from secrets, no emulator + is started, and the job is fork/dependabot guarded. + type: boolean + default: false + use-firebase-emulators: + description: >- + true (default) runs the test command under `firebase emulators:exec` + with the emulator suite installed and cached. Independent of + inject-config-secrets since a product can need both: firebase_auth + tests run against the Auth emulator but its password-policy tests + call the live REST API, which rejects placeholder credentials. + type: boolean + default: true + storage-emulator-debug: + description: >- + Sets STORAGE_EMULATOR_DEBUG on the test steps. firebase_storage only. + type: boolean + default: false + flutter-version: + description: >- + Pinned Flutter version. Empty (the default) leaves the job unpinned + (stable channel), which is what every product uses today. + type: string + default: '' + firebase-tools-version: + description: 'firebase-tools version. Emulator tier only.' + type: string + default: '15.25.1' + wasm: + description: >- + true builds and drives the example as WebAssembly: the example's + `web/wasm_index.html` replaces `web/index.html` and `flutter drive` + gets `--wasm`. Needs an example that ships that file, which among the + product examples only cloud_firestore does. + type: boolean + default: false + drive-timeout: + description: >- + FLUTTER_DRIVE_TIMEOUT_SECONDS for the test step. Empty uses the retry + script default (180). + type: string + default: '' + max-attempts: + description: >- + FLUTTER_DRIVE_MAX_ATTEMPTS for the test step. Empty uses the retry + script default (4). + type: string + default: '' + nightly_test_mode: + description: >- + Passed down from nightly.yaml. Shortens the job timeout to 5 minutes. + type: boolean + default: false + secrets: + # `required: false` so the emulator-tier callers can omit the `secrets:` + # block entirely. The live-tier callers declare them as required and pass + # them through explicitly. + TESTS_E2E_FIREBASE_OPTIONS_DART: + required: false + TESTS_E2E_GOOGLE_SERVICES_JSON: + required: false + +permissions: + contents: read + +jobs: + web: + # Live tier requires the live-project secrets, which fork and dependabot + # PRs do not receive. + if: >- + inputs.inject-config-secrets == false || + github.event_name != 'pull_request' || + (github.event.pull_request.head.repo.full_name == github.repository && + github.actor != 'dependabot[bot]') + permissions: + contents: read + runs-on: ubuntu-latest + env: + # Inputs consumed inside run: scripts are routed through env so their + # values are data, never shell/template code (zizmor: template-injection). + PACKAGE_PATH: ${{ inputs.package-path }} + TEST_TARGET: ${{ inputs.test-target }} + NATIVE_CONFIG_ARGS: ${{ inputs.native-config-args }} + timeout-minutes: ${{ inputs.nightly_test_mode && 5 || 25 }} + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 + - uses: ./.github/actions/setup-flutterfire + timeout-minutes: 15 + with: + node: ${{ inputs.use-firebase-emulators && 'true' || 'false' }} + java: ${{ inputs.use-firebase-emulators && 'true' || 'false' }} + flutter-version: ${{ inputs.flutter-version }} + firebase-tools-version: ${{ inputs.use-firebase-emulators && inputs.firebase-tools-version || '' }} + bootstrap-scope: ${{ inputs.package-scope }} + # The ubuntu runner image ships Google Chrome and a chromedriver build + # that is matched to it — that pairing IS our pinning strategy, so we use + # the image binaries rather than downloading our own. + - name: 'Set up Chrome and chromedriver' + run: | + echo "$CHROMEWEBDRIVER" >> "$GITHUB_PATH" + google-chrome --version + "$CHROMEWEBDRIVER/chromedriver" --version + - name: Inject Firebase config + if: inputs.inject-config-secrets + # The `flutterfire-e2e-tests` project config lives in repository + # secrets rather than in the tree: this is a public repo and these are + # live backends. Same shape as `e2e_tests_pipeline.yaml`. + env: + FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} + run: | + if [ -z "$FIREBASE_OPTIONS_DART" ]; then + echo "::error::TESTS_E2E_FIREBASE_OPTIONS_DART is empty — secrets are unavailable (fork/dependabot PR?). Failing early instead of producing a broken build." + exit 1 + fi + echo "$FIREBASE_OPTIONS_DART" > "$PACKAGE_PATH/example/lib/firebase_options.dart" + echo "$GOOGLE_SERVICES_JSON" > "$PACKAGE_PATH/example/android/app/google-services.json" + - name: Generate dummy Firebase configs + # No native flag on web: there is no google-services.json or plist in a + # web build. + if: ${{ !inputs.inject-config-secrets }} + run: dart ./.github/workflows/scripts/generate-dummy-firebase-configs.dart + - name: Firebase Emulator Cache + if: inputs.use-firebase-emulators + id: firebase-emulator-cache + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + continue-on-error: true + with: + # The firebase emulators are pure javascript and java, OS-independent + enableCrossOsArchive: true + # Must match the save path exactly + path: ~/.cache/firebase/emulators + key: firebase-emulators-v5-${{ env.FIREBASE_TOOLS_VERSION }} + restore-keys: firebase-emulators-v5- + - name: Install Cloud Functions dependencies + if: inputs.use-firebase-emulators + # `firebase emulators:exec` does not install these itself (the old + # start-firebase-emulator.sh wrapper did); without them the functions + # emulator fails to load the function definitions. + run: npm ci --prefix .github/workflows/scripts/functions + - name: 'Use WASM index.html' + if: inputs.wasm + working-directory: ${{ inputs.package-path }}/example + run: mv ./web/wasm_index.html ./web/index.html + # Web devices are not supported for the `flutter test` command yet. As a + # workaround we can use the `flutter drive` command. Tracking issue: + # https://github.com/flutter/flutter/issues/66264 + # WASM runs can additionally hang after building but before the test + # harness connects; that is the same failure class. + # The retry script only retries infrastructure startup failures + # (timeouts, AppConnectionException, "Failed to exit Chromium"); real + # test/compile failures fail fast. It also owns the chromedriver + # lifecycle and defaults the device to web-server, so nothing here starts + # chromedriver. It reads its FLUTTER_DRIVE_* configuration from the + # environment, which `emulators:exec` passes through to the command. + - name: 'E2E Tests' + if: inputs.use-firebase-emulators + # Covers booting the emulator suite as well as the tests themselves, since + # `emulators:exec` now owns both. + timeout-minutes: 20 + # firebase.json and the emulator rule files live here, so `emulators:exec` + # has to run from this directory; the test command cds back to the + # package under test. + working-directory: ./.github/workflows/scripts + env: + FLUTTER_DRIVE_TARGET: './${{ inputs.test-target }}' + FLUTTER_DRIVE_DRIVER: './test_driver/integration_test.dart' + FLUTTER_DRIVE_EXTRA_ARGS: ${{ inputs.wasm && '--wasm --dart-define=CI=true' || '--dart-define=CI=true' }} + # Empty falls back to the retry script's own defaults (180s, 4 attempts). + FLUTTER_DRIVE_TIMEOUT_SECONDS: ${{ inputs.drive-timeout }} + FLUTTER_DRIVE_MAX_ATTEMPTS: ${{ inputs.max-attempts }} + STORAGE_EMULATOR_DEBUG: ${{ inputs.storage-emulator-debug && 'true' || '' }} + run: | + firebase emulators:exec --project flutterfire-e2e-tests "cd ${GITHUB_WORKSPACE}/${PACKAGE_PATH}/example && ${GITHUB_WORKSPACE}/.github/workflows/scripts/flutter-drive-web-retry.sh" + - name: 'E2E Tests' + if: ${{ !inputs.use-firebase-emulators }} + working-directory: ${{ inputs.package-path }}/example + timeout-minutes: 20 + env: + FLUTTER_DRIVE_TARGET: './${{ inputs.test-target }}' + FLUTTER_DRIVE_DRIVER: './test_driver/integration_test.dart' + FLUTTER_DRIVE_EXTRA_ARGS: ${{ inputs.wasm && '--wasm --dart-define=CI=true' || '--dart-define=CI=true' }} + # Empty falls back to the retry script's own defaults (180s, 4 attempts). + FLUTTER_DRIVE_TIMEOUT_SECONDS: ${{ inputs.drive-timeout }} + FLUTTER_DRIVE_MAX_ATTEMPTS: ${{ inputs.max-attempts }} + run: ${{ github.workspace }}/.github/workflows/scripts/flutter-drive-web-retry.sh + - name: Save Firebase Emulator Cache + # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. + if: ${{ inputs.use-firebase-emulators && github.ref == 'refs/heads/main' }} + continue-on-error: true + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + with: + # The firebase emulators are pure javascript and java, OS-independent + enableCrossOsArchive: true + key: ${{ steps.firebase-emulator-cache.outputs.cache-primary-key }} + # Must match the restore path exactly + path: ~/.cache/firebase/emulators diff --git a/.github/workflows/reusable_e2e_windows.yaml b/.github/workflows/reusable_e2e_windows.yaml new file mode 100644 index 000000000000..87592d48447e --- /dev/null +++ b/.github/workflows/reusable_e2e_windows.yaml @@ -0,0 +1,193 @@ +name: reusable-e2e-windows + +# The Windows half of the per-product e2e suite: `flutter drive` the example as +# a Windows desktop app. Called only by the products that ship a Windows +# implementation (auth, storage, app_check, remote_config, firestore). + +on: + workflow_call: + inputs: + package-path: + description: >- + Path of the package under test, e.g. + 'packages/firebase_storage/firebase_storage'. The example app is + `/example` by convention. + type: string + required: true + package-scope: + description: "Melos glob bootstrapped before the tests, e.g. 'firebase_storage*'." + type: string + required: true + test-target: + description: 'Integration test entrypoint, relative to the example directory.' + type: string + default: 'integration_test/e2e_test.dart' + native-config-args: + description: >- + Arguments for generate-dummy-firebase-configs.dart, e.g. + '--storage-native' (emulator tier, writes the native config files the + Android/Xcode builds require) or '--live-tier-plist=messaging' (live + tier, writes only the placeholder plist so it cannot clobber the + injected credentials). Empty = plain run. + type: string + default: '' + inject-config-secrets: + description: >- + true selects the live tier: the "Inject Firebase config" step writes + firebase_options.dart / google-services.json from secrets, no emulator + is started, and the job is fork/dependabot guarded. + type: boolean + default: false + use-firebase-emulators: + description: >- + true (default) runs the test command under `firebase emulators:exec` + with the emulator suite installed and cached. Independent of + inject-config-secrets since a product can need both: firebase_auth + tests run against the Auth emulator but its password-policy tests + call the live REST API, which rejects placeholder credentials. + type: boolean + default: true + storage-emulator-debug: + description: >- + Sets STORAGE_EMULATOR_DEBUG on the test steps. firebase_storage only. + type: boolean + default: false + flutter-version: + description: >- + Pinned Flutter version. Empty (the default) leaves the job unpinned + (stable channel), which is what every product uses today. + type: string + default: '' + firebase-tools-version: + description: 'firebase-tools version. Emulator tier only.' + type: string + default: '15.25.1' + nightly_test_mode: + description: >- + Passed down from nightly.yaml. Skips this job: a Windows build cannot + fit the 5 minute nightly budget. + type: boolean + default: false + secrets: + # `required: false` so the emulator-tier callers can omit the `secrets:` + # block entirely. The live-tier callers declare them as required and pass + # them through explicitly. + TESTS_E2E_FIREBASE_OPTIONS_DART: + required: false + TESTS_E2E_GOOGLE_SERVICES_JSON: + required: false + +permissions: + contents: read + +jobs: + windows: + # Skipped in nightly test mode, where the 5 minute budget cannot fit a + # Windows build. All conditions live in one `if:` - a second `if:` key + # would be a duplicate mapping key and only the last one would apply. + # The rest is the live tier's fork/dependabot guard: those PRs do not + # receive the live-project secrets. + if: >- + inputs.nightly_test_mode == false && + (inputs.inject-config-secrets == false || + github.event_name != 'pull_request' || + (github.event.pull_request.head.repo.full_name == github.repository && + github.actor != 'dependabot[bot]')) + permissions: + contents: read + runs-on: windows-latest + env: + # Inputs consumed inside run: scripts are routed through env so their + # values are data, never shell/template code (zizmor: template-injection). + PACKAGE_PATH: ${{ inputs.package-path }} + TEST_TARGET: ${{ inputs.test-target }} + NATIVE_CONFIG_ARGS: ${{ inputs.native-config-args }} + FIREBASE_TOOLS_VERSION_INPUT: ${{ inputs.firebase-tools-version }} + timeout-minutes: 45 + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 + - uses: ./.github/actions/setup-flutterfire + with: + node: ${{ inputs.use-firebase-emulators && 'true' || 'false' }} + java: ${{ inputs.use-firebase-emulators && 'true' || 'false' }} + # The npm cache action is not worth its restore cost here; the only + # npm work is the global firebase-tools install below. + npm-cache: 'false' + flutter-version: ${{ inputs.flutter-version }} + bootstrap-scope: ${{ inputs.package-scope }} + - name: Inject Firebase config + if: inputs.inject-config-secrets + # The `flutterfire-e2e-tests` project config lives in repository + # secrets rather than in the tree: this is a public repo and these are + # live backends. Same shape as `e2e_tests_pipeline.yaml`. + env: + FIREBASE_OPTIONS_DART: ${{ secrets.TESTS_E2E_FIREBASE_OPTIONS_DART }} + GOOGLE_SERVICES_JSON: ${{ secrets.TESTS_E2E_GOOGLE_SERVICES_JSON }} + shell: bash + run: | + if [ -z "$FIREBASE_OPTIONS_DART" ]; then + echo "::error::TESTS_E2E_FIREBASE_OPTIONS_DART is empty — secrets are unavailable (fork/dependabot PR?). Failing early instead of producing a broken build." + exit 1 + fi + echo "$FIREBASE_OPTIONS_DART" > "$PACKAGE_PATH/example/lib/firebase_options.dart" + echo "$GOOGLE_SERVICES_JSON" > "$PACKAGE_PATH/example/android/app/google-services.json" + - name: Generate dummy Firebase configs + if: ${{ !inputs.inject-config-secrets }} + run: dart ./.github/workflows/scripts/generate-dummy-firebase-configs.dart + - name: "Install Tools" + if: inputs.use-firebase-emulators + # Not the composite action's firebase-tools install: that one uses `sudo npm`, + # which does not exist on the Windows runners. + run: | + npm install -g firebase-tools@$env:FIREBASE_TOOLS_VERSION_INPUT + - name: Install Cloud Functions dependencies + if: inputs.use-firebase-emulators + # `firebase emulators:exec` does not install these itself; without them the + # functions emulator logs "Failed to load function definition" on every run. + run: npm ci --prefix .github/workflows/scripts/functions + - name: Start Firebase Emulator and run tests + if: inputs.use-firebase-emulators + timeout-minutes: 30 + env: + STORAGE_EMULATOR_DEBUG: ${{ inputs.storage-emulator-debug && 'true' || '' }} + run: | + cd ./.github/workflows/scripts + function Invoke-Drive { + firebase emulators:exec --project flutterfire-e2e-tests "cd ../../../$env:PACKAGE_PATH/example && flutter drive --target=.\$env:TEST_TARGET --driver=.\test_driver\integration_test.dart -d windows --verbose" 2>&1 | Tee-Object -FilePath output.log + $script:exitCode = $LASTEXITCODE + $script:output = Get-Content output.log -Raw + } + Invoke-Drive + # 'Service has disappeared' is the desktop app crashing mid-suite - + # an intermittent native crash, not a test failure. Retry that shape + # exactly once; everything else keeps its first verdict. + if ($exitCode -ne 0 -and $output -match 'Service has disappeared') { + Write-Host "App crashed mid-run (Service has disappeared) - retrying once." + Invoke-Drive + } + if ($output -match '\[E\]' -or $output -match 'Some tests failed') { + Write-Error "All tests did not pass. Please check the logs for more information." + exit 1 + } + exit $exitCode + - name: Run E2E tests + if: ${{ !inputs.use-firebase-emulators }} + timeout-minutes: 20 + run: | + cd $env:PACKAGE_PATH/example + function Invoke-Drive { + flutter drive --target=.\$env:TEST_TARGET --driver=.\test_driver\integration_test.dart -d windows --dart-define=CI=true --verbose 2>&1 | Tee-Object -FilePath output.log + $script:exitCode = $LASTEXITCODE + $script:output = Get-Content output.log -Raw + } + Invoke-Drive + # Same crash-shape retry as the emulator-tier step above. + if ($exitCode -ne 0 -and $output -match 'Service has disappeared') { + Write-Host "App crashed mid-run (Service has disappeared) - retrying once." + Invoke-Drive + } + if ($output -match '\[E\]' -or $output -match 'Some tests failed') { + Write-Error "All tests did not pass. Please check the logs for more information." + exit 1 + } + exit $exitCode diff --git a/.github/workflows/scripts/flutter-drive-web-retry.sh b/.github/workflows/scripts/flutter-drive-web-retry.sh index bf2563d60358..d94f8969caef 100755 --- a/.github/workflows/scripts/flutter-drive-web-retry.sh +++ b/.github/workflows/scripts/flutter-drive-web-retry.sh @@ -4,7 +4,11 @@ set -euo pipefail : "${FLUTTER_DRIVE_TARGET:?FLUTTER_DRIVE_TARGET is required}" : "${FLUTTER_DRIVE_DRIVER:?FLUTTER_DRIVE_DRIVER is required}" -FLUTTER_DRIVE_DEVICE="${FLUTTER_DRIVE_DEVICE:-chrome}" +# web-server, not chrome: with `-d chrome` the flutter tool launches the +# browser itself, which requires a display server and silently exits 0 when +# none exists (headless CI). With web-server the browser is launched through +# chromedriver, which is headless-capable. +FLUTTER_DRIVE_DEVICE="${FLUTTER_DRIVE_DEVICE:-web-server}" FLUTTER_DRIVE_TIMEOUT_SECONDS="${FLUTTER_DRIVE_TIMEOUT_SECONDS:-180}" FLUTTER_DRIVE_MAX_ATTEMPTS="${FLUTTER_DRIVE_MAX_ATTEMPTS:-4}" FLUTTER_DRIVE_EXTRA_ARGS="${FLUTTER_DRIVE_EXTRA_ARGS:-}" @@ -110,6 +114,13 @@ PY return 2 fi + # Never trust a bare exit 0: `flutter drive` exits 0 even when the browser + # failed to launch and no test ever ran. Require positive evidence. + if [[ "$exit_code" == "0" && "$output" != *"All tests passed"* ]]; then + echo "flutter drive exited 0 without reporting test success; treating as an infrastructure failure." + return 3 + fi + if [[ "$exit_code" == "124" ]] || [[ "$output" == *"AppConnectionException"* ]] || [[ "$output" == *"Failed to exit Chromium"* ]]; then @@ -120,14 +131,19 @@ PY } for attempt in $(seq 1 "$FLUTTER_DRIVE_MAX_ATTEMPTS"); do - if run_tests; then + # NOT `if run_tests; then ...; fi` + `$?`: a completed `if` with a false + # condition and no else sets $? to 0, so every failure collapsed to exit 0. + exit_code=0 + run_tests || exit_code=$? + + if [[ "$exit_code" == "0" ]]; then exit 0 fi - - exit_code=$? if [[ "$exit_code" != "3" || "$attempt" == "$FLUTTER_DRIVE_MAX_ATTEMPTS" ]]; then exit "$exit_code" fi echo "Attempt $attempt failed before tests completed. Retrying with clean browser processes..." done +# Unreachable, but never fall off the end with an implicit 0. +exit 1 diff --git a/.github/workflows/scripts/flutter-test-android-retry.sh b/.github/workflows/scripts/flutter-test-android-retry.sh new file mode 100755 index 000000000000..5fb70e2613a9 --- /dev/null +++ b/.github/workflows/scripts/flutter-test-android-retry.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# Android twin of the Apple wrapper embedded in reusable_e2e_ios/macos.yaml: +# `flutter test` against the AVD occasionally hangs after `assembleDebug` +# without printing a single line (the app never launches), which used to burn +# the whole step timeout and kill the job. Bound each attempt with an alarm, +# retry the zero-output shape once inside the same booted AVD, and trust only +# the runner's numeric tally for the verdict. +set -euo pipefail + +: "${TEST_TARGET:?TEST_TARGET is required}" + +run_flutter_test() { + rm -f flutter_test_output.log + # File redirect, never capture, so a stray child holding the pipe cannot + # block us. The alarm bounds the known zero-output launch hang well below + # the step timeout, leaving room for the retry. + perl -e 'alarm shift; exec @ARGV' 900 \ + flutter test "$TEST_TARGET" --timeout 10x --dart-define=CI=true -d emulator-5554 \ + > flutter_test_output.log 2>&1 + FT_EXIT=$? + cat flutter_test_output.log +} + +set +e +run_flutter_test +# Retry once when no tally exists at all: the launch hang (alarm kill) and +# "Error connecting to the service protocol" both fail before any test runs. +if ! grep -Eq '[0-9]+ tests? passed' flutter_test_output.log \ + && ! grep -Eq '[0-9]+ failed' flutter_test_output.log; then + echo "Infrastructure launch failure detected - retrying once." + run_flutter_test +fi +# Trust only the runner's own tally, parsed numerically. Substring checks are +# how '0 tests passed, 1 failed' once laundered into a green job. +PASSED=$(grep -Eo '[0-9]+ tests? passed' flutter_test_output.log | tail -1 | grep -Eo '^[0-9]+') +FAILED=$(grep -Eo '[0-9]+ failed' flutter_test_output.log | tail -1 | grep -Eo '^[0-9]+') +: "${PASSED:=0}"; : "${FAILED:=0}" +echo "Result tally: passed=$PASSED failed=$FAILED (flutter test exit $FT_EXIT)" +if [ "$FAILED" -gt 0 ] || [ "$PASSED" -eq 0 ]; then + exit 1 +fi +exit 0 diff --git a/.github/workflows/scripts/generate-dummy-firebase-configs.dart b/.github/workflows/scripts/generate-dummy-firebase-configs.dart index 855e03d8d983..f64cf4f154a3 100644 --- a/.github/workflows/scripts/generate-dummy-firebase-configs.dart +++ b/.github/workflows/scripts/generate-dummy-firebase-configs.dart @@ -7,10 +7,25 @@ import 'dart:convert'; import 'dart:io'; +/// Examples whose `lib/firebase_options.dart` this script fills in with dummy +/// credentials. +/// +/// The nine live-tier examples (ai, analytics, app_check, app_installations, +/// crashlytics, messaging, ml_model_downloader, performance, remote_config) +/// are still listed: their `lib/main.dart` imports `firebase_options.dart`, so +/// without a file here `melos analyze-ci` and every example build in +/// `all_plugins.yaml` fail on an unresolved import. +/// +/// Their e2e workflows never take this path. `e2e_tests_.yaml` gets +/// the real values from repository secrets ("Inject Firebase config") and only +/// ever invokes this script through `--live-tier-plist=`, which writes +/// the Apple placeholder plist and nothing else - so a dummy can never clobber +/// an injected credential regardless of step order. const _firebaseOptionsPaths = [ 'packages/cloud_firestore/cloud_firestore/example/integration_test/firebase_options.dart', 'packages/cloud_firestore/cloud_firestore/example/lib/firebase_options.dart', 'packages/cloud_functions/cloud_functions/example/lib/firebase_options.dart', + 'packages/firebase_ai/firebase_ai/example/lib/firebase_options.dart', 'packages/firebase_analytics/firebase_analytics/example/lib/firebase_options.dart', 'packages/firebase_app_check/firebase_app_check/example/lib/firebase_options.dart', 'packages/firebase_app_installations/firebase_app_installations/example/lib/firebase_options.dart', @@ -27,6 +42,24 @@ const _firebaseOptionsPaths = [ 'packages/firebase_storage/firebase_storage/example/lib/firebase_options.dart', ]; +/// Placeholder API key, shaped so the Firebase SDKs accept it. +/// +/// It is not enough for this to be obviously fake. `firebase_core`'s iOS/macOS +/// plugin calls `[FIRApp configureWithOptions:[FIROptions defaultOptions]]` the +/// moment it is registered, whenever a GoogleService-Info.plist is present in +/// the bundle - the plugin registrant runs it before any Dart code, so the Dart +/// `FirebaseOptions` never get a say. `FIRApp` then eagerly instantiates +/// `FIRInstallations`, which is a hard dependency of Analytics, App Check, +/// App Installations, Crashlytics, In-App Messaging, ML Model Downloader, +/// Messaging, Performance and Remote Config. `+[FIRInstallations +/// validateAPIKey:]` raises an ObjC exception - i.e. SIGABRT at launch, before +/// the VM service is up, which `flutter test` only ever sees as +/// "WebSocketChannelException: Connection refused" - unless the key is exactly +/// 39 characters, starts with `A` and contains only base64url characters. +/// +/// So: keep the shape, keep the value obviously fake. +const _dummyApiKey = 'AIzaSyDUMMYKEYFORFLUTTERFIRECITESTS0000'; + // The project ID must match the emulator fixtures and Firestore bundles. const _firebaseOptions = ''' // Copyright 2026, the Chromium project authors. Please see the AUTHORS file @@ -55,44 +88,126 @@ class DefaultFirebaseOptions { } static const web = FirebaseOptions( - apiKey: 'dummy-api-key', + apiKey: '$_dummyApiKey', appId: '1:123456789012:web:0000000000000000000000', messagingSenderId: '123456789012', projectId: 'flutterfire-e2e-tests', + // The database e2e suite asserts refFromURL() mismatch behavior, which + // only triggers when the instance has a configured databaseURL. + databaseURL: + 'https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app', authDomain: 'flutterfire-e2e-tests.firebaseapp.com', storageBucket: 'flutterfire-e2e-tests.appspot.com', ); static const android = FirebaseOptions( - apiKey: 'dummy-api-key', + apiKey: '$_dummyApiKey', appId: '1:123456789012:android:0000000000000000000000', messagingSenderId: '123456789012', projectId: 'flutterfire-e2e-tests', + // The database e2e suite asserts refFromURL() mismatch behavior, which + // only triggers when the instance has a configured databaseURL. + databaseURL: + 'https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app', storageBucket: 'flutterfire-e2e-tests.appspot.com', ); static const ios = FirebaseOptions( - apiKey: 'dummy-api-key', + apiKey: '$_dummyApiKey', appId: '1:123456789012:ios:0000000000000000000000', messagingSenderId: '123456789012', projectId: 'flutterfire-e2e-tests', + // The database e2e suite asserts refFromURL() mismatch behavior, which + // only triggers when the instance has a configured databaseURL. + databaseURL: + 'https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app', storageBucket: 'flutterfire-e2e-tests.appspot.com', ); static const macos = ios; static const windows = FirebaseOptions( - apiKey: 'dummy-api-key', + apiKey: '$_dummyApiKey', appId: '1:123456789012:web:0000000000000000000000', messagingSenderId: '123456789012', projectId: 'flutterfire-e2e-tests', + // The database e2e suite asserts refFromURL() mismatch behavior, which + // only triggers when the instance has a configured databaseURL. + databaseURL: + 'https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app', authDomain: 'flutterfire-e2e-tests.firebaseapp.com', storageBucket: 'flutterfire-e2e-tests.appspot.com', ); } '''; +/// Example roots of the nine live-tier products, keyed by the name used in +/// `--live-tier-plist=`. +/// +/// Every one of these Xcode projects lists `GoogleService-Info.plist` in a +/// Resources build phase, so `flutter build ios/macos` fails outright when the +/// file is absent. The plist is only there to satisfy the build: these products +/// initialise Firebase from the Dart `FirebaseOptions` injected from secrets, +/// not from the plist, and the bundle id below matches the app identity every +/// one of these examples now uses. +/// +/// The one caveat found while migrating: the crashlytics example's macOS target +/// runs `upload-symbols --flutter-project firebase_app_id_file.json`, so on that +/// target it is the committed `firebase_app_id_file.json` - not the plist - that +/// is load-bearing at build time. +const _liveTierExampleRoots = { + 'ai': 'packages/firebase_ai/firebase_ai/example', + // Not a live-tier product (its suite runs against the Auth emulator), but + // the same plist mechanics apply: real options are injected from secrets + // (the password-policy tests call the live REST API), the Xcode projects + // list GoogleService-Info.plist in a Resources build phase, and the native + // [DEFAULT] app configured from that plist must agree with the injected + // options. The old committed plist carried `dummy-api-key`, whose shape + // FIRInstallations rejects with an abort at launch - the "Unable to start + // the app on the device" failures. + 'auth': 'packages/firebase_auth/firebase_auth/example', + 'analytics': 'packages/firebase_analytics/firebase_analytics/example', + 'app_check': 'packages/firebase_app_check/firebase_app_check/example', + 'app_installations': + 'packages/firebase_app_installations/firebase_app_installations/example', + 'crashlytics': 'packages/firebase_crashlytics/firebase_crashlytics/example', + 'messaging': 'packages/firebase_messaging/firebase_messaging/example', + 'ml_model_downloader': + 'packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example', + 'performance': 'packages/firebase_performance/firebase_performance/example', + 'remote_config': + 'packages/firebase_remote_config/firebase_remote_config/example', +}; + +/// The app identity shared by the mega `tests` app and the nine live-tier +/// examples: live Firebase backends only accept app ids registered in the +/// `flutterfire-e2e-tests` project. +const _liveTierBundleId = 'io.flutter.plugins.firebase.tests'; + void main(List arguments) { + const plistFlag = '--live-tier-plist='; + final plistProducts = arguments + .where((argument) => argument.startsWith(plistFlag)) + .map((argument) => argument.substring(plistFlag.length)) + .toList(); + + if (plistProducts.isNotEmpty) { + // Deliberately exclusive: a live-tier e2e job must never write a Dart + // options file, because the real one arrives from a repository secret. + for (final product in plistProducts) { + final exampleRoot = _liveTierExampleRoots[product]; + if (exampleRoot == null) { + stderr.writeln( + 'Unknown --live-tier-plist target "$product". ' + 'Known: ${_liveTierExampleRoots.keys.join(', ')}.', + ); + exit(1); + } + _writeApplePlists(exampleRoot: exampleRoot, bundleId: _liveTierBundleId); + } + return; + } + for (final path in _firebaseOptionsPaths) { _write(path, _firebaseOptions); } @@ -164,23 +279,32 @@ void _writeNativeConfigs({ String? macosBundleId, }) { final androidPath = '$exampleRoot/android/app/google-services.json'; + // Same reason as the plist: the google-services Gradle plugin turns this file + // into the string resources `FirebaseApp.initializeApp` reads, so anything + // missing here is missing from the natively-configured `[DEFAULT]` app that + // Dart then has to agree with. `firebase_url` is what the database suite's + // refFromURL assertions need. + final options = _optionsFromDart(exampleRoot, 'android'); final androidConfig = { 'project_info': { - 'project_number': '123456789012', - 'project_id': 'flutterfire-e2e-tests', - 'storage_bucket': 'flutterfire-e2e-tests.appspot.com', + 'project_number': options['messagingSenderId']!, + 'project_id': options['projectId']!, + if (options['storageBucket'] != null) + 'storage_bucket': options['storageBucket']!, + if (options['databaseURL'] != null) + 'firebase_url': options['databaseURL']!, }, 'client': [ { 'client_info': { - 'mobilesdk_app_id': '1:123456789012:android:0000000000000000000000', + 'mobilesdk_app_id': options['appId']!, 'android_client_info': { 'package_name': androidPackageName, }, }, 'api_key': [ - {'current_key': 'dummy-api-key'}, + {'current_key': options['apiKey']!}, ], }, ], @@ -191,36 +315,155 @@ void _writeNativeConfigs({ '${const JsonEncoder.withIndent(' ').convert(androidConfig)}\n', ); - String applePlist(String bundleId) => - ''' + _writeApplePlists( + exampleRoot: exampleRoot, + bundleId: appleBundleId, + macosBundleId: macosBundleId, + ); +} + +/// Writes `GoogleService-Info.plist` for whichever Apple targets the example +/// has, with values taken from that example's `lib/firebase_options.dart`. +/// +/// Deriving rather than hardcoding is the point. Once a plist is in the bundle, +/// `firebase_core`'s plugin registrant configures the native `[DEFAULT]` app +/// from it before any Dart runs; the Dart `Firebase.initializeApp(options: ...)` +/// that follows then only succeeds if its apiKey, databaseURL and storageBucket +/// agree with what the plist already installed - otherwise +/// `MethodChannelFirebase.initializeApp` throws `[core/duplicate-app]`. Two +/// hand-maintained copies of the same credentials drift the moment one side +/// gains a field (which is exactly how `databaseURL` broke the Apple suites), +/// so there is only one copy here and the plist is projected out of it. +/// +/// This works for both tiers because both leave the truth in the same file: +/// the emulator tier because [_firebaseOptions] was just written to it, the +/// live tier because CI's "Inject Firebase config" step writes the real +/// credentials there before invoking `--live-tier-plist`. +void _writeApplePlists({ + required String exampleRoot, + required String bundleId, + // Defaults to [bundleId]; only the examples whose macOS target carries a + // different bundle id need to pass this. + String? macosBundleId, +}) { + final options = _optionsFromDart(exampleRoot, 'ios'); + + String applePlist(String bundleId) { + final entries = { + 'API_KEY': options['apiKey']!, + 'GCM_SENDER_ID': options['messagingSenderId']!, + 'PLIST_VERSION': '1', + 'BUNDLE_ID': bundleId, + 'PROJECT_ID': options['projectId']!, + 'GOOGLE_APP_ID': options['appId']!, + // Optional in a real plist too: only projects with the product enabled + // carry them. + if (options['storageBucket'] != null) + 'STORAGE_BUCKET': options['storageBucket']!, + if (options['databaseURL'] != null) + 'DATABASE_URL': options['databaseURL']!, + if (options['iosClientId'] != null) 'CLIENT_ID': options['iosClientId']!, + }; + final body = entries.entries + .map( + (entry) => + '\t${entry.key}\n\t${entry.value}', + ) + .join('\n'); + return ''' -\tAPI_KEY -\tdummy-api-key -\tGCM_SENDER_ID -\t123456789012 -\tPLIST_VERSION -\t1 -\tBUNDLE_ID -\t$bundleId -\tPROJECT_ID -\tflutterfire-e2e-tests -\tSTORAGE_BUCKET -\tflutterfire-e2e-tests.appspot.com -\tGOOGLE_APP_ID -\t1:123456789012:ios:0000000000000000000000 +$body '''; - _write( - '$exampleRoot/ios/Runner/GoogleService-Info.plist', - applePlist(appleBundleId), - ); - _write( - '$exampleRoot/macos/Runner/GoogleService-Info.plist', - applePlist(macosBundleId ?? appleBundleId), - ); + } + + // Not every example has both Apple targets (the performance example has no + // `macos/`), and creating one would leave a stray directory behind. + for (final platform in const ['ios', 'macos']) { + if (!Directory('$exampleRoot/$platform').existsSync()) continue; + final target = '$exampleRoot/$platform/Runner/GoogleService-Info.plist'; + // A committed plist wins: crashlytics, app_installations and messaging + // check in real plists for their registered Firebase apps, and the native + // SDKs configure from the bundled plist before any Dart code runs - + // overwriting one with a placeholder made crashlytics hang at launch. + if (File(target).existsSync()) { + stdout.writeln('Keeping existing $target'); + continue; + } + _write( + target, + applePlist(platform == 'macos' ? (macosBundleId ?? bundleId) : bundleId), + ); + } +} + +/// Pulls one platform's `static const = FirebaseOptions(...)` values +/// out of an example's `lib/firebase_options.dart`. +/// +/// `macos` is never asked for: every generator (this script and the +/// `flutterfire` CLI) emits the same credentials for both Apple platforms, and +/// the two targets only differ by bundle id, which the caller supplies. +/// +/// Exits non-zero rather than falling back to a placeholder: a native config +/// that disagrees with the Dart options fails at runtime, inside the app, as +/// `[core/duplicate-app]` - which is a far worse thing to debug than a build +/// step that says what it could not read. +Map _optionsFromDart(String exampleRoot, String platform) { + final path = '$exampleRoot/lib/firebase_options.dart'; + final file = File(path); + if (!file.existsSync()) { + stderr.writeln( + 'Cannot write the $platform Firebase config: $path is missing.', + ); + exit(1); + } + + // Comments can contain apostrophes ("doesn't"), which would otherwise be + // picked up as string delimiters by the field pattern below. + final source = file + .readAsStringSync() + .replaceAll(RegExp(r'^\s*//.*$', multiLine: true), ''); + + // Both declaration shapes exist: this script's own template writes + // `static const ios = ...`, while the injected live config (and flutterfire + // configure output) writes the typed `static const FirebaseOptions ios = ...`. + final block = RegExp( + 'static\\s+const\\s+(?:FirebaseOptions\\s+)?$platform\\s*=\\s*FirebaseOptions\\(([\\s\\S]*?)\\);', + ).firstMatch(source); + if (block == null) { + stderr.writeln( + 'Cannot write the $platform Firebase config: no ' + '"static const [FirebaseOptions] $platform = FirebaseOptions(...)" ' + 'found in $path.', + ); + exit(1); + } + + final fields = { + for (final field + in RegExp(r"(\w+)\s*:\s*'([^']*)'").allMatches(block.group(1)!)) + field.group(1)!: field.group(2)!, + }; + + for (final required in const [ + 'apiKey', + 'appId', + 'messagingSenderId', + 'projectId', + ]) { + if (fields[required] == null) { + stderr.writeln( + 'Cannot write the $platform Firebase config: `$required` is missing ' + 'from the $platform FirebaseOptions in $path.', + ); + exit(1); + } + } + + return fields; } void _write(String path, String contents) { diff --git a/.github/workflows/scripts/nightly_issue_dashboard.dart b/.github/workflows/scripts/nightly_issue_dashboard.dart index 02ac14bb858c..c49d9e459fe0 100644 --- a/.github/workflows/scripts/nightly_issue_dashboard.dart +++ b/.github/workflows/scripts/nightly_issue_dashboard.dart @@ -15,21 +15,49 @@ import 'dart:convert'; import 'dart:io'; +/// One column per nightly workflow, in table order: the environment variable +/// `nightly.yaml` sets, and the header the issue shows. +/// +/// The table used to be built by hand in three places (create, rewrite, and the +/// row itself); with seventeen workflows reporting that triplication was the whole +/// maintenance cost of adding one. Everything below derives from this list, so +/// a new workflow is a single entry here plus a `_STATUS` in +/// `nightly.yaml`. +/// +/// Rows stay one-per-date: the issue is a 30-day history, so products have to +/// be columns. +const _columns = <({String env, String header})>[ + (env: 'SMOKE_STATUS', header: 'Smoke'), + (env: 'FIRESTORE_STATUS', header: 'Firestore'), + (env: 'FDC_STATUS', header: 'FDC'), + (env: 'STORAGE_STATUS', header: 'Storage'), + (env: 'AUTH_STATUS', header: 'Auth'), + (env: 'DATABASE_STATUS', header: 'Database'), + (env: 'FUNCTIONS_STATUS', header: 'Functions'), + (env: 'PIPELINE_STATUS', header: 'Pipeline'), + (env: 'AI_STATUS', header: 'AI'), + (env: 'ANALYTICS_STATUS', header: 'Analytics'), + (env: 'APP_CHECK_STATUS', header: 'App Check'), + (env: 'APP_INSTALLATIONS_STATUS', header: 'Installations'), + (env: 'CRASHLYTICS_STATUS', header: 'Crashlytics'), + (env: 'MESSAGING_STATUS', header: 'Messaging'), + (env: 'ML_MODEL_DOWNLOADER_STATUS', header: 'ML Model'), + (env: 'PERFORMANCE_STATUS', header: 'Performance'), + (env: 'REMOTE_CONFIG_STATUS', header: 'Remote Config'), +]; + +/// `| Date | Smoke | ... | Notes |` +String get _headerRow => + '| Date | ${_columns.map((c) => c.header).join(' | ')} | Notes |'; + +/// The `| :--- | :--- | ... |` separator, one cell per header cell. +String get _separatorRow => + '| ${List.filled(_columns.length + 2, ':---').join(' | ')} |'; + void main() async { final env = Platform.environment; final token = env['GITHUB_TOKEN']; final repo = env['REPO']; - final androidStatus = env['ANDROID_STATUS'] ?? 'skipped'; - final webStatus = env['WEB_STATUS'] ?? 'skipped'; - final iosStatus = env['IOS_STATUS'] ?? 'skipped'; - final macosStatus = env['MACOS_STATUS'] ?? 'skipped'; - final windowsStatus = env['WINDOWS_STATUS'] ?? 'skipped'; - final fdcStatus = env['FDC_STATUS'] ?? 'skipped'; - final storageStatus = env['STORAGE_STATUS'] ?? 'skipped'; - final authStatus = env['AUTH_STATUS'] ?? 'skipped'; - final databaseStatus = env['DATABASE_STATUS'] ?? 'skipped'; - final functionsStatus = env['FUNCTIONS_STATUS'] ?? 'skipped'; - final pipelineStatus = env['PIPELINE_STATUS'] ?? 'skipped'; final runId = env['GITHUB_RUN_ID']; final serverUrl = env['GITHUB_SERVER_URL'] ?? 'https://github.com'; @@ -44,20 +72,10 @@ void main() async { final runUrl = '$serverUrl/$repo/actions/runs/$runId'; final notes = '[View Run]($runUrl)'; - final androidIcon = _getIcon(androidStatus); - final webIcon = _getIcon(webStatus); - final iosIcon = _getIcon(iosStatus); - final macosIcon = _getIcon(macosStatus); - final windowsIcon = _getIcon(windowsStatus); - final fdcIcon = _getIcon(fdcStatus); - final storageIcon = _getIcon(storageStatus); - final authIcon = _getIcon(authStatus); - final databaseIcon = _getIcon(databaseStatus); - final functionsIcon = _getIcon(functionsStatus); - final pipelineIcon = _getIcon(pipelineStatus); - - final newRow = - '| $date | $androidIcon | $iosIcon | $webIcon | $macosIcon | $windowsIcon | $fdcIcon | $storageIcon | $authIcon | $databaseIcon | $functionsIcon | $pipelineIcon | $notes |'; + final icons = + _columns.map((c) => _getIcon(env[c.env] ?? 'skipped')).join(' | '); + + final newRow = '| $date | $icons | $notes |'; print('New Row: $newRow'); @@ -136,12 +154,11 @@ Future _createIssue( final body = { 'title': '[FlutterFire] Nightly Integration Testing Report', 'labels': ['nightly-testing'], - 'body': - ''' + 'body': ''' ## Testing History (last 30 days) -| Date | Android | iOS | Web | MacOS | Windows | FDC | Storage | Auth | Database | Functions | Pipeline | Notes | -| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | +$_headerRow +$_separatorRow $newRow ''', }; @@ -235,12 +252,8 @@ String _appendRow(String currentBody, String newRow) { for (final line in lines) { if (line.startsWith('| Date |')) { if (!processedTable) { - newBodyLines.add( - '| Date | Android | iOS | Web | MacOS | Windows | FDC | Storage | Auth | Database | Functions | Pipeline | Notes |', - ); - newBodyLines.add( - '| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |', - ); + newBodyLines.add(_headerRow); + newBodyLines.add(_separatorRow); newBodyLines.addAll(tableRows); processedTable = true; } @@ -257,12 +270,8 @@ String _appendRow(String currentBody, String newRow) { if (!processedTable) { newBodyLines.add('## Testing History (last 30 days)'); newBodyLines.add(''); - newBodyLines.add( - '| Date | Android | iOS | Web | MacOS | Windows | FDC | Storage | Auth | Database | Functions | Pipeline | Notes |', - ); - newBodyLines.add( - '| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |', - ); + newBodyLines.add(_headerRow); + newBodyLines.add(_separatorRow); newBodyLines.add(newRow); } @@ -283,4 +292,3 @@ String _getDateFromRow(String row) { } return ''; } - diff --git a/.github/workflows/scripts/swift-integration.dart b/.github/workflows/scripts/swift-integration.dart index 1c4e3f75cdd2..7a097cd452ed 100644 --- a/.github/workflows/scripts/swift-integration.dart +++ b/.github/workflows/scripts/swift-integration.dart @@ -7,6 +7,8 @@ import 'dart:convert'; final debugMode = false; +const _allPlatforms = ['ios', 'macos']; + void main(List arguments) async { if (debugMode) { print('[DEBUG] main: Starting swift-integration script'); @@ -14,10 +16,36 @@ void main(List arguments) async { print('[DEBUG] Number of arguments: ${arguments.length}'); } - if (arguments.isEmpty) { + // `--platform=` narrows the run to one platform so CI can build + // the two in parallel jobs. Omitting it builds both, in order, as before. + final platforms = []; + final packages = []; + for (final argument in arguments) { + if (argument.startsWith('--platform=')) { + final platform = argument.substring('--platform='.length); + if (!_allPlatforms.contains(platform)) { + throw Exception( + 'Unknown --platform value "$platform". ' + 'Expected one of: ${_allPlatforms.join(', ')}.', + ); + } + platforms.add(platform); + } else { + packages.add(argument); + } + } + if (platforms.isEmpty) { + platforms.addAll(_allPlatforms); + } + + if (packages.isEmpty) { throw Exception('No FlutterFire dependency arguments provided.'); } + if (debugMode) { + print('[DEBUG] Platforms to build: ${platforms.join(', ')}'); + } + // Get the current git branch from GitHub Actions environment or fallback to git command final currentBranch = await getCurrentBranch(); print('Current branch: $currentBranch'); @@ -29,23 +57,20 @@ void main(List arguments) async { } // Update all Package.swift files to use branch dependencies - await updatePackageSwiftFiles(currentBranch, arguments); + await updatePackageSwiftFiles(currentBranch, packages); if (debugMode) { print('[DEBUG] Package.swift files updated, starting builds'); } - final plugins = arguments.join(','); - - if (debugMode) { - print('[DEBUG] Building iOS first...'); - } - await buildSwiftExampleApp('ios', plugins); + final plugins = packages.join(','); - if (debugMode) { - print('[DEBUG] iOS build completed, now building macOS...'); + for (final platform in platforms) { + if (debugMode) { + print('[DEBUG] Building $platform...'); + } + await buildSwiftExampleApp(platform, plugins); } - await buildSwiftExampleApp('macos', plugins); if (debugMode) { print('[DEBUG] main: All builds completed successfully'); diff --git a/.github/workflows/web.yaml b/.github/workflows/web.yaml deleted file mode 100644 index e37ccefd3e9f..000000000000 --- a/.github/workflows/web.yaml +++ /dev/null @@ -1,311 +0,0 @@ -name: e2e-web - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }}-web - cancel-in-progress: true - -on: - pull_request: - paths-ignore: - - 'docs/**' - - 'website/**' - - '**/example/**' - - '!**/example/integration_test/**' - - '**/flutterfire_ui/**' - - '**.md' - push: - branches: - - main - paths-ignore: - - 'docs/**' - - 'website/**' - - '**/example/**' - - '!**/example/integration_test/**' - - '**/flutterfire_ui/**' - - '**.md' - workflow_call: - inputs: - nightly_test_mode: - type: boolean - default: false - -permissions: - contents: read - -jobs: - web: - name: web (${{ matrix.suite.name }}) - permissions: - contents: read - runs-on: ubuntu-latest - timeout-minutes: ${{ inputs.nightly_test_mode && 5 || 25 }} - strategy: - fail-fast: false - matrix: - # The `tests` app is sharded by product suite so a hang or flake costs - # one small job instead of the whole run. `integration_test/e2e_test.dart` - # still aggregates every suite for Windows and local runs. App Check is - # not in any shard on web: `web-app-check` below keeps running it alone, - # because it is throttled when it shares a run with the other suites. - suite: - - name: core_misc - working_directory: tests - target: integration_test/shards/core_misc_shard_test.dart - scope: tests - - name: firestore - working_directory: packages/cloud_firestore/cloud_firestore/example - target: integration_test/e2e_test.dart - scope: 'cloud_firestore*' - exclude: - # Nightly drops the firestore example leg. The entry is repeated in - # full because matrix `exclude` compares the whole object; only `name` - # is switched, so outside nightly it matches nothing. - - suite: - name: ${{ inputs.nightly_test_mode && 'firestore' || 'none' }} - working_directory: packages/cloud_firestore/cloud_firestore/example - target: integration_test/e2e_test.dart - scope: 'cloud_firestore*' - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - - uses: ./.github/actions/setup-flutterfire - timeout-minutes: 15 - with: - firebase-tools-version: '15.25.1' - # Each matrix leg bootstraps only the packages it exercises. - bootstrap-scope: ${{ matrix.suite.scope }} - # The ubuntu runner image ships Google Chrome and a chromedriver build - # that is matched to it — that pairing IS our pinning strategy, so we use - # the image binaries rather than downloading our own. - - name: 'Set up Chrome and chromedriver' - run: | - echo "$CHROMEWEBDRIVER" >> "$GITHUB_PATH" - google-chrome --version - "$CHROMEWEBDRIVER/chromedriver" --version - - name: Generate dummy Firebase configs - run: dart ./.github/workflows/scripts/generate-dummy-firebase-configs.dart - - name: Firebase Emulator Cache - id: firebase-emulator-cache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - # Must match the save path exactly - path: ~/.cache/firebase/emulators - key: firebase-emulators-v5-${{ env.FIREBASE_TOOLS_VERSION }} - restore-keys: firebase-emulators-v5- - - name: Install Cloud Functions dependencies - # `firebase emulators:exec` does not install these itself (the old - # start-firebase-emulator.sh wrapper did); without them the functions - # emulator fails to load the function definitions. - run: npm ci --prefix .github/workflows/scripts/functions - - name: 'E2E Tests' - # Covers booting the emulator suite as well as the tests themselves, since - # `emulators:exec` now owns both. - timeout-minutes: 20 - # firebase.json and the emulator rule files live here, so `emulators:exec` - # has to run from this directory; the test command cds back to the package - # under test. - working-directory: ./.github/workflows/scripts - # Web devices are not supported for the `flutter test` command yet. As a - # workaround we can use the `flutter drive` command. Tracking issue: - # https://github.com/flutter/flutter/issues/66264 - # The retry script only retries infrastructure startup failures - # (timeouts, AppConnectionException, "Failed to exit Chromium"); real - # test/compile failures fail fast. It also owns the chromedriver - # lifecycle, so nothing here starts chromedriver. - # The retry script reads its FLUTTER_DRIVE_* configuration from the - # environment, which `emulators:exec` passes through to the command. - env: - FLUTTER_DRIVE_TARGET: './${{ matrix.suite.target }}' - FLUTTER_DRIVE_DRIVER: './test_driver/integration_test.dart' - FLUTTER_DRIVE_EXTRA_ARGS: '--dart-define=CI=true' - STORAGE_EMULATOR_DEBUG: 'true' - run: | - firebase emulators:exec --project flutterfire-e2e-tests "cd ${GITHUB_WORKSPACE}/${{ matrix.suite.working_directory }} && ${GITHUB_WORKSPACE}/.github/workflows/scripts/flutter-drive-web-retry.sh" - - name: Save Firestore Emulator Cache - # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. - if: github.ref == 'refs/heads/main' - continue-on-error: true - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - key: ${{ steps.firebase-emulator-cache.outputs.cache-primary-key }} - # Must match the restore path exactly - path: ~/.cache/firebase/emulators - - web-app-check: - permissions: - contents: read - runs-on: ubuntu-latest - timeout-minutes: 25 - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - - uses: ./.github/actions/setup-flutterfire - timeout-minutes: 15 - with: - firebase-tools-version: '15.25.1' - bootstrap-scope: 'tests' - # The ubuntu runner image ships Google Chrome and a chromedriver build - # that is matched to it — that pairing IS our pinning strategy, so we use - # the image binaries rather than downloading our own. - - name: 'Set up Chrome and chromedriver' - run: | - echo "$CHROMEWEBDRIVER" >> "$GITHUB_PATH" - google-chrome --version - "$CHROMEWEBDRIVER/chromedriver" --version - - name: Firebase Emulator Cache - id: firebase-emulator-cache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - # Must match the save path exactly - path: ~/.cache/firebase/emulators - key: firebase-emulators-v5-${{ env.FIREBASE_TOOLS_VERSION }} - restore-keys: firebase-emulators-v5- - - name: Install Cloud Functions dependencies - # `firebase emulators:exec` does not install these itself (the old - # start-firebase-emulator.sh wrapper did); without them the functions - # emulator fails to load the function definitions. - run: npm ci --prefix .github/workflows/scripts/functions - - name: 'E2E Tests' - # Covers booting the emulator suite as well as the tests themselves, since - # `emulators:exec` now owns both. - timeout-minutes: 20 - # firebase.json and the emulator rule files live here, so `emulators:exec` - # has to run from this directory; the test command cds back to the package - # under test. - working-directory: ./.github/workflows/scripts - # Web devices are not supported for the `flutter test` command yet. As a - # workaround we can use the `flutter drive` command. Tracking issue: - # https://github.com/flutter/flutter/issues/66264 - # The retry script only retries infrastructure startup failures - # (timeouts, AppConnectionException, "Failed to exit Chromium"); real - # test/compile failures fail fast. It also owns the chromedriver - # lifecycle, so nothing here starts chromedriver. - # The retry script reads its FLUTTER_DRIVE_* configuration from the - # environment, which `emulators:exec` passes through to the command. - env: - FLUTTER_DRIVE_TARGET: './integration_test/e2e_test.dart' - FLUTTER_DRIVE_DRIVER: './test_driver/integration_test.dart' - FLUTTER_DRIVE_EXTRA_ARGS: '--dart-define=CI=true --dart-define=APP_CHECK_E2E=true' - STORAGE_EMULATOR_DEBUG: 'true' - run: | - firebase emulators:exec --project flutterfire-e2e-tests "cd ${GITHUB_WORKSPACE}/tests && ${GITHUB_WORKSPACE}/.github/workflows/scripts/flutter-drive-web-retry.sh" - - name: Save Firestore Emulator Cache - # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. - if: github.ref == 'refs/heads/main' - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - key: ${{ steps.firebase-emulator-cache.outputs.cache-primary-key }} - # Must match the restore path exactly - path: ~/.cache/firebase/emulators - - web-wasm: - name: web-wasm (${{ matrix.suite.name }}) - permissions: - contents: read - runs-on: ubuntu-latest - timeout-minutes: ${{ inputs.nightly_test_mode && 5 || 25 }} - strategy: - fail-fast: false - matrix: - # Same sharding as the `web` job above; see the comment there. - suite: - - name: core_misc - working_directory: tests - target: integration_test/shards/core_misc_shard_test.dart - scope: tests - - name: firestore - working_directory: packages/cloud_firestore/cloud_firestore/example - target: integration_test/e2e_test.dart - scope: 'cloud_firestore*' - exclude: - # Nightly drops the firestore example leg. The entry is repeated in - # full because matrix `exclude` compares the whole object; only `name` - # is switched, so outside nightly it matches nothing. - - suite: - name: ${{ inputs.nightly_test_mode && 'firestore' || 'none' }} - working_directory: packages/cloud_firestore/cloud_firestore/example - target: integration_test/e2e_test.dart - scope: 'cloud_firestore*' - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - - uses: ./.github/actions/setup-flutterfire - timeout-minutes: 15 - with: - firebase-tools-version: '15.25.1' - # Each matrix leg bootstraps only the packages it exercises. - bootstrap-scope: ${{ matrix.suite.scope }} - # The ubuntu runner image ships Google Chrome and a chromedriver build - # that is matched to it — that pairing IS our pinning strategy, so we use - # the image binaries rather than downloading our own. - - name: 'Set up Chrome and chromedriver' - run: | - echo "$CHROMEWEBDRIVER" >> "$GITHUB_PATH" - google-chrome --version - "$CHROMEWEBDRIVER/chromedriver" --version - - name: Generate dummy Firebase configs - run: dart ./.github/workflows/scripts/generate-dummy-firebase-configs.dart - - name: Firebase Emulator Cache - id: firebase-emulator-cache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - continue-on-error: true - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - # Must match the save path exactly - path: ~/.cache/firebase/emulators - key: firebase-emulators-v5-${{ env.FIREBASE_TOOLS_VERSION }} - restore-keys: firebase-emulators-v5- - - name: Install Cloud Functions dependencies - # `firebase emulators:exec` does not install these itself (the old - # start-firebase-emulator.sh wrapper did); without them the functions - # emulator fails to load the function definitions. - run: npm ci --prefix .github/workflows/scripts/functions - - name: 'Use WASM index.html' - working-directory: ${{ matrix.suite.working_directory }} - run: mv ./web/wasm_index.html ./web/index.html - - name: 'E2E Tests' - # Covers booting the emulator suite as well as the tests themselves, since - # `emulators:exec` now owns both. - timeout-minutes: 20 - # firebase.json and the emulator rule files live here, so `emulators:exec` - # has to run from this directory; the test command cds back to the package - # under test. - working-directory: ./.github/workflows/scripts - # Web devices are not supported for the `flutter test` command yet. As a - # workaround we can use the `flutter drive` command. Tracking issue: - # https://github.com/flutter/flutter/issues/66264 - # WASM web runs can hang after building but before the test harness - # connects; the retry script retries only those infrastructure startup - # failures. It also owns the chromedriver lifecycle, so nothing here - # starts chromedriver. - # The retry script reads its FLUTTER_DRIVE_* configuration from the - # environment, which `emulators:exec` passes through to the command. - env: - FLUTTER_DRIVE_TARGET: './${{ matrix.suite.target }}' - FLUTTER_DRIVE_DRIVER: './test_driver/integration_test.dart' - FLUTTER_DRIVE_EXTRA_ARGS: '--wasm --dart-define=CI=true' - FLUTTER_DRIVE_TIMEOUT_SECONDS: '300' - FLUTTER_DRIVE_MAX_ATTEMPTS: '2' - STORAGE_EMULATOR_DEBUG: 'true' - run: | - firebase emulators:exec --project flutterfire-e2e-tests "cd ${GITHUB_WORKSPACE}/${{ matrix.suite.working_directory }} && ${GITHUB_WORKSPACE}/.github/workflows/scripts/flutter-drive-web-retry.sh" - - name: Save Firestore Emulator Cache - # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. - if: github.ref == 'refs/heads/main' - continue-on-error: true - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - with: - # The firebase emulators are pure javascript and java, OS-independent - enableCrossOsArchive: true - key: ${{ steps.firebase-emulator-cache.outputs.cache-primary-key }} - # Must match the restore path exactly - path: ~/.cache/firebase/emulators diff --git a/.github/workflows/windows.yaml b/.github/workflows/windows.yaml deleted file mode 100644 index d5b85be8efa8..000000000000 --- a/.github/workflows/windows.yaml +++ /dev/null @@ -1,100 +0,0 @@ -name: e2e-windows - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }}-windows - cancel-in-progress: true - -on: - pull_request: - paths-ignore: - - 'docs/**' - - 'website/**' - - '**/example/**' - - '!**/example/integration_test/**' - - '**/flutterfire_ui/**' - - '**.md' - push: - branches: - - main - paths-ignore: - - 'docs/**' - - 'website/**' - - '**/example/**' - - '!**/example/integration_test/**' - - '**/flutterfire_ui/**' - - '**.md' - workflow_call: - inputs: - nightly_test_mode: - type: boolean - default: false - -permissions: - contents: read - -jobs: - windows: - runs-on: windows-latest - timeout-minutes: ${{ inputs.nightly_test_mode && 5 || 45 }} - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - - uses: ./.github/actions/setup-flutterfire - with: - npm-cache: 'false' - bootstrap-scope: 'tests cloud_firestore*' - - name: "Install Tools" - # Not the composite action's firebase-tools install: that one uses `sudo npm`, - # which does not exist on the Windows runners. - run: | - npm install -g firebase-tools@15.25.1 - - name: "Build Windows (Release)" - timeout-minutes: 25 - run: cd tests && flutter build windows --release - - name: "Build Windows (Profile)" - timeout-minutes: 25 - run: cd tests && flutter build windows --profile - - name: Install Cloud Functions dependencies - # `firebase emulators:exec` does not install these itself; without them the - # functions emulator logs "Failed to load function definition" on every run. - run: npm ci --prefix .github/workflows/scripts/functions - - name: Start Firebase Emulator and run tests - timeout-minutes: 30 - env: - STORAGE_EMULATOR_DEBUG: 'true' - run: cd ./.github/workflows/scripts && firebase emulators:exec --project flutterfire-e2e-tests "cd ../../../tests && flutter test .\integration_test\e2e_test.dart -d windows --verbose" - - windows-firestore: - runs-on: windows-latest - if: ${{ !inputs.nightly_test_mode }} - timeout-minutes: ${{ inputs.nightly_test_mode && 5 || 45 }} - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - - uses: ./.github/actions/setup-flutterfire - with: - npm-cache: 'false' - bootstrap-scope: 'tests cloud_firestore*' - - name: Generate dummy Firebase configs - run: dart ./.github/workflows/scripts/generate-dummy-firebase-configs.dart - - name: "Install Tools" - # Not the composite action's firebase-tools install: that one uses `sudo npm`, - # which does not exist on the Windows runners. - run: | - npm install -g firebase-tools@15.25.1 - - name: Install Cloud Functions dependencies - # `firebase emulators:exec` does not install these itself; without them the - # functions emulator logs "Failed to load function definition" on every run. - run: npm ci --prefix .github/workflows/scripts/functions - - name: Start Firebase Emulator and run tests - timeout-minutes: 30 - env: - STORAGE_EMULATOR_DEBUG: 'true' - run: | - cd ./.github/workflows/scripts - firebase emulators:exec --project flutterfire-e2e-tests "cd ../../../packages/cloud_firestore/cloud_firestore/example && flutter drive --target=.\integration_test\e2e_test.dart --driver=.\test_driver\integration_test.dart -d windows --verbose" 2>&1 | Tee-Object -FilePath output.log - $exitCode = $LASTEXITCODE - $output = Get-Content output.log -Raw - if ($output -match '\[E\]' -or $output -match 'Some tests failed') { - Write-Error "All tests did not pass. Please check the logs for more information." - exit 1 - } - exit $exitCode diff --git a/packages/cloud_firestore/cloud_firestore/example/android/app/src/main/AndroidManifest.xml b/packages/cloud_firestore/cloud_firestore/example/android/app/src/main/AndroidManifest.xml index 74a78b939e5e..637f57c840d1 100644 --- a/packages/cloud_firestore/cloud_firestore/example/android/app/src/main/AndroidManifest.xml +++ b/packages/cloud_firestore/cloud_firestore/example/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ diff --git a/packages/cloud_firestore/cloud_firestore/example/integration_test/e2e_test.dart b/packages/cloud_firestore/cloud_firestore/example/integration_test/e2e_test.dart index 44d4742cbe35..6001093c1a07 100644 --- a/packages/cloud_firestore/cloud_firestore/example/integration_test/e2e_test.dart +++ b/packages/cloud_firestore/cloud_firestore/example/integration_test/e2e_test.dart @@ -17,6 +17,7 @@ import 'geo_point_e2e.dart'; import 'instance_e2e.dart'; import 'load_bundle_e2e.dart'; import 'query_e2e.dart'; +import 'report_test_results.dart'; import 'second_database.dart'; import 'settings_e2e.dart'; import 'snapshot_metadata_e2e.dart'; @@ -29,13 +30,25 @@ import 'write_batch_e2e.dart'; bool kUseFirestoreEmulator = true; void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + reportTestResultsToDriver(binding); group('cloud_firestore', () { setUpAll(() async { - await Firebase.initializeApp( - options: DefaultFirebaseOptions.currentPlatform, - ); + // The native SDK may already have configured [DEFAULT] from a bundled + // GoogleService-Info.plist (the plugin registrant does this before any + // Dart runs). Dart's Firebase.apps cannot see that app until the first + // platform-channel call, so the only reliable guard is catching the + // duplicate-app error and keeping the natively configured instance. + try { + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + } on FirebaseException catch (e) { + if (e.code != 'duplicate-app') { + rethrow; + } + } // Web by default doesn't have persistence enabled FirebaseFirestore.instance.settings = const Settings( persistenceEnabled: true, diff --git a/packages/cloud_firestore/cloud_firestore/example/integration_test/firebase_options.dart b/packages/cloud_firestore/cloud_firestore/example/integration_test/firebase_options.dart new file mode 100644 index 000000000000..a5a1360f6620 --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore/example/integration_test/firebase_options.dart @@ -0,0 +1,61 @@ +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// Generated for CI builds and emulator tests. These are not real credentials. +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + return switch (defaultTargetPlatform) { + TargetPlatform.android => android, + TargetPlatform.iOS => ios, + TargetPlatform.macOS => macos, + TargetPlatform.windows => windows, + _ => throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ), + }; + } + + static const web = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:web:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + authDomain: 'flutterfire-e2e-tests.firebaseapp.com', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const android = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:android:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const ios = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:ios:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const macos = ios; + + static const windows = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:web:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + authDomain: 'flutterfire-e2e-tests.firebaseapp.com', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); +} diff --git a/packages/cloud_firestore/cloud_firestore/example/integration_test/report_test_results.dart b/packages/cloud_firestore/cloud_firestore/example/integration_test/report_test_results.dart new file mode 100644 index 000000000000..f08ddf1af020 --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore/example/integration_test/report_test_results.dart @@ -0,0 +1,40 @@ +// Copyright 2020, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/common.dart' show Failure; +import 'package:integration_test/integration_test.dart'; +import 'package:test_api/hooks.dart' show TestHandle; + +/// Records every executed test via the package:test lifecycle (plain `test()` +/// declarations never reach the binding's `results` map - only `testWidgets` +/// does) and publishes them through +/// [IntegrationTestWidgetsFlutterBinding.reportData], the only channel that +/// crosses the wire on web. +void reportTestResultsToDriver(IntegrationTestWidgetsFlutterBinding binding) { + final executed = []; + setUp(() { + executed.add(TestHandle.current.name); + }); + tearDownAll(() { + // `TestHandle.current.name` is the full name, prefixed with the enclosing + // group names, while `binding.results` is keyed by the bare `testWidgets` + // description - so match a failed entry on the suffix as well as on + // equality, otherwise no grouped test could ever be marked failed. + final failures = [ + for (final entry in binding.results.entries) + if (entry.value is Failure) entry.key, + ]; + bool didFail(String name) => failures + .any((failure) => name == failure || name.endsWith(' $failure')); + + binding.reportData ??= {}; + binding.reportData!['testResults'] = { + // Plain test() failures are not individually attributable here, but they + // fail the run as a whole via allTestsPassed; testWidgets failures are + // attributed from the binding's results map. + for (final name in executed) name: didFail(name) ? 'failed' : 'success', + }; + }); +} diff --git a/packages/cloud_firestore/cloud_firestore/example/lib/firebase_options.dart b/packages/cloud_firestore/cloud_firestore/example/lib/firebase_options.dart new file mode 100644 index 000000000000..a5a1360f6620 --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore/example/lib/firebase_options.dart @@ -0,0 +1,61 @@ +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// Generated for CI builds and emulator tests. These are not real credentials. +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + return switch (defaultTargetPlatform) { + TargetPlatform.android => android, + TargetPlatform.iOS => ios, + TargetPlatform.macOS => macos, + TargetPlatform.windows => windows, + _ => throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ), + }; + } + + static const web = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:web:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + authDomain: 'flutterfire-e2e-tests.firebaseapp.com', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const android = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:android:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const ios = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:ios:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const macos = ios; + + static const windows = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:web:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + authDomain: 'flutterfire-e2e-tests.firebaseapp.com', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); +} diff --git a/packages/cloud_firestore/cloud_firestore/example/pubspec.yaml b/packages/cloud_firestore/cloud_firestore/example/pubspec.yaml index 03b1bf9c1e05..db02eb81c7b5 100755 --- a/packages/cloud_firestore/cloud_firestore/example/pubspec.yaml +++ b/packages/cloud_firestore/cloud_firestore/example/pubspec.yaml @@ -18,6 +18,9 @@ dev_dependencies: sdk: flutter integration_test: sdk: flutter + # `integration_test/report_test_results.dart` names each executed test via + # `package:test_api/hooks.dart`; `any` defers to the version flutter_test pins. + test_api: any flutter: uses-material-design: true diff --git a/packages/cloud_firestore/cloud_firestore/example/test_driver/integration_test.dart b/packages/cloud_firestore/cloud_firestore/example/test_driver/integration_test.dart index f1ac26f27b88..691723cbbfe1 100644 --- a/packages/cloud_firestore/cloud_firestore/example/test_driver/integration_test.dart +++ b/packages/cloud_firestore/cloud_firestore/example/test_driver/integration_test.dart @@ -2,6 +2,33 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'dart:io'; + import 'package:integration_test/integration_test_driver.dart'; -Future main() => integrationDriver(); +Future main() => integrationDriver( + writeResponseOnFailure: true, + responseDataCallback: (Map? data) async { + final results = + (data?['testResults'] as Map?)?.cast() ?? + const {}; + var passed = 0; + var failed = 0; + for (final entry in results.entries) { + final ok = entry.value == 'success'; + ok ? passed++ : failed++; + // ignore: avoid_print + print('${ok ? '✅' : '❌'} ${entry.key}'); + } + // ignore: avoid_print + print('Web e2e summary: $passed passed, $failed failed, ' + '${results.length} total'); + if (results.isEmpty) { + // ignore: avoid_print + print('[E] No tests reported by the app - treating as ' + 'infrastructure failure.'); + exit(1); + } + await writeResponseData(data); + }, + ); diff --git a/packages/cloud_firestore/cloud_firestore/pipeline_example/integration_test/pipeline/pipeline_live_test.dart b/packages/cloud_firestore/cloud_firestore/pipeline_example/integration_test/pipeline/pipeline_live_test.dart index d3f679955d58..b361d17bbb45 100644 --- a/packages/cloud_firestore/cloud_firestore/pipeline_example/integration_test/pipeline/pipeline_live_test.dart +++ b/packages/cloud_firestore/cloud_firestore/pipeline_example/integration_test/pipeline/pipeline_live_test.dart @@ -10,6 +10,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:pipeline_example/firebase_options.dart'; +import 'report_test_results.dart'; import 'pipeline_add_fields_e2e.dart'; import 'pipeline_aggregate_e2e.dart'; import 'pipeline_expressions_e2e.dart'; @@ -24,13 +25,25 @@ import 'pipeline_select_e2e.dart'; import 'pipeline_unnest_union_e2e.dart'; void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + reportTestResultsToDriver(binding); group('pipeline (live)', () { setUpAll(() async { - await Firebase.initializeApp( - options: DefaultFirebaseOptions.currentPlatform, - ); + // The native SDK may already have configured [DEFAULT] from a bundled + // GoogleService-Info.plist (the plugin registrant does this before any + // Dart runs). Dart's Firebase.apps cannot see that app until the first + // platform-channel call, so the only reliable guard is catching the + // duplicate-app error and keeping the natively configured instance. + try { + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + } on FirebaseException catch (e) { + if (e.code != 'duplicate-app') { + rethrow; + } + } final firestore = FirebaseFirestore.instanceFor( app: Firebase.app(), databaseId: 'firestore-pipeline-test', diff --git a/packages/cloud_firestore/cloud_firestore/pipeline_example/integration_test/pipeline/report_test_results.dart b/packages/cloud_firestore/cloud_firestore/pipeline_example/integration_test/pipeline/report_test_results.dart new file mode 100644 index 000000000000..9de87f8dd08b --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore/pipeline_example/integration_test/pipeline/report_test_results.dart @@ -0,0 +1,41 @@ +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/common.dart' show Failure; +import 'package:integration_test/integration_test.dart'; +import 'package:test_api/hooks.dart' show TestHandle; + +/// Records every executed test via the package:test lifecycle (plain `test()` +/// declarations never reach the binding's `results` map - only `testWidgets` +/// does) and publishes them through +/// [IntegrationTestWidgetsFlutterBinding.reportData], the only channel that +/// crosses the wire on web. +void reportTestResultsToDriver(IntegrationTestWidgetsFlutterBinding binding) { + final executed = []; + setUp(() { + executed.add(TestHandle.current.name); + }); + tearDownAll(() { + // `TestHandle.current.name` is the full name, prefixed with the enclosing + // group names, while `binding.results` is keyed by the bare `testWidgets` + // description - so match a failed entry on the suffix as well as on + // equality, otherwise no grouped test could ever be marked failed. + final failures = [ + for (final entry in binding.results.entries) + if (entry.value is Failure) entry.key, + ]; + bool didFail(String name) => failures.any( + (failure) => name == failure || name.endsWith(' $failure'), + ); + + binding.reportData ??= {}; + binding.reportData!['testResults'] = { + // Plain test() failures are not individually attributable here, but they + // fail the run as a whole via allTestsPassed; testWidgets failures are + // attributed from the binding's results map. + for (final name in executed) name: didFail(name) ? 'failed' : 'success', + }; + }); +} diff --git a/packages/cloud_firestore/cloud_firestore/pipeline_example/ios/Flutter/AppFrameworkInfo.plist b/packages/cloud_firestore/cloud_firestore/pipeline_example/ios/Flutter/AppFrameworkInfo.plist index 1dc6cf7652ba..391a902b2beb 100644 --- a/packages/cloud_firestore/cloud_firestore/pipeline_example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/cloud_firestore/cloud_firestore/pipeline_example/ios/Flutter/AppFrameworkInfo.plist @@ -20,7 +20,5 @@ ???? CFBundleVersion 1.0 - MinimumOSVersion - 13.0 diff --git a/packages/cloud_firestore/cloud_firestore/pipeline_example/ios/Runner.xcodeproj/project.pbxproj b/packages/cloud_firestore/cloud_firestore/pipeline_example/ios/Runner.xcodeproj/project.pbxproj index 238fb056204e..5d60858b5449 100644 --- a/packages/cloud_firestore/cloud_firestore/pipeline_example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/cloud_firestore/cloud_firestore/pipeline_example/ios/Runner.xcodeproj/project.pbxproj @@ -8,7 +8,6 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 2B45BBBA04F26970F9DC2428 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D9D2411717BE352C92EC6263 /* Pods_RunnerTests.framework */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; @@ -16,7 +15,7 @@ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; D3B189D93111F21B593E07CE /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = FC9AF0E7C3463CF0E820724D /* GoogleService-Info.plist */; }; - ED8175A9DDB4B6F569867736 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 344F3B3BBCDBB7DF16FCD695 /* Pods_Runner.framework */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -43,14 +42,10 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 060D24293863437D08A9EF6D /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; - 0A1494B03E7E4E5E8FCD37E4 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 24A69F3F28EEBABDA3E3C6E5 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 344F3B3BBCDBB7DF16FCD695 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -62,10 +57,6 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 9A6E43430478085B3F4B8179 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; - 9B52183BF5A24CC0FD358BC5 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - D44D6373BC6CC1B25B52520C /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - D9D2411717BE352C92EC6263 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; FC9AF0E7C3463CF0E820724D /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -74,7 +65,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 2B45BBBA04F26970F9DC2428 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -82,7 +72,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - ED8175A9DDB4B6F569867736 /* Pods_Runner.framework in Frameworks */, + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -97,24 +87,9 @@ path = RunnerTests; sourceTree = ""; }; - 33AEEB36D1DBFD083FE19F82 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 344F3B3BBCDBB7DF16FCD695 /* Pods_Runner.framework */, - D9D2411717BE352C92EC6263 /* Pods_RunnerTests.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; 726BAFFB79E9610C171E5B05 /* Pods */ = { isa = PBXGroup; children = ( - 9B52183BF5A24CC0FD358BC5 /* Pods-Runner.debug.xcconfig */, - D44D6373BC6CC1B25B52520C /* Pods-Runner.release.xcconfig */, - 0A1494B03E7E4E5E8FCD37E4 /* Pods-Runner.profile.xcconfig */, - 060D24293863437D08A9EF6D /* Pods-RunnerTests.debug.xcconfig */, - 24A69F3F28EEBABDA3E3C6E5 /* Pods-RunnerTests.release.xcconfig */, - 9A6E43430478085B3F4B8179 /* Pods-RunnerTests.profile.xcconfig */, ); path = Pods; sourceTree = ""; @@ -139,7 +114,6 @@ 331C8082294A63A400263BE5 /* RunnerTests */, FC9AF0E7C3463CF0E820724D /* GoogleService-Info.plist */, 726BAFFB79E9610C171E5B05 /* Pods */, - 33AEEB36D1DBFD083FE19F82 /* Frameworks */, ); sourceTree = ""; }; @@ -174,7 +148,6 @@ isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - 11FC74FA85B59C7E267B4550 /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, 7A5921923875F445DD42E396 /* Frameworks */, @@ -190,17 +163,18 @@ productType = "com.apple.product-type.bundle.unit-test"; }; 97C146ED1CF9000F007C117D /* Runner */ = { + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 6F498101593ECA79F30C31E4 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - B4380A12E6068F3FEA627E84 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -215,6 +189,9 @@ /* Begin PBXProject section */ 97C146E61CF9000F007C117D /* Project object */ = { + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + ); isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; @@ -273,28 +250,6 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 11FC74FA85B59C7E267B4550 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -311,28 +266,6 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 6F498101593ECA79F30C31E4 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -348,27 +281,6 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - B4380A12E6068F3FEA627E84 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - inputPaths = ( - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -496,7 +408,6 @@ }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 060D24293863437D08A9EF6D /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -514,7 +425,6 @@ }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 24A69F3F28EEBABDA3E3C6E5 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -530,7 +440,6 @@ }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9A6E43430478085B3F4B8179 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -736,6 +645,18 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } diff --git a/packages/cloud_firestore/cloud_firestore/pipeline_example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/cloud_firestore/cloud_firestore/pipeline_example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index e3773d42e24c..c3fedb29c990 100644 --- a/packages/cloud_firestore/cloud_firestore/pipeline_example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/cloud_firestore/cloud_firestore/pipeline_example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -5,6 +5,24 @@ + + + + + + + + + + main() => integrationDriver(); +Future main() => integrationDriver( + writeResponseOnFailure: true, + responseDataCallback: (Map? data) async { + final results = + (data?['testResults'] as Map?)?.cast() ?? + const {}; + var passed = 0; + var failed = 0; + for (final entry in results.entries) { + final ok = entry.value == 'success'; + ok ? passed++ : failed++; + // ignore: avoid_print + print('${ok ? '✅' : '❌'} ${entry.key}'); + } + // ignore: avoid_print + print( + 'Web e2e summary: $passed passed, $failed failed, ' + '${results.length} total', + ); + if (results.isEmpty) { + // ignore: avoid_print + print( + '[E] No tests reported by the app - treating as ' + 'infrastructure failure.', + ); + exit(1); + } + await writeResponseData(data); + }, +); diff --git a/packages/cloud_functions/cloud_functions/example/integration_test/e2e_test.dart b/packages/cloud_functions/cloud_functions/example/integration_test/e2e_test.dart index 12cfaf437070..b2f088f9cd01 100644 --- a/packages/cloud_functions/cloud_functions/example/integration_test/e2e_test.dart +++ b/packages/cloud_functions/cloud_functions/example/integration_test/e2e_test.dart @@ -12,6 +12,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:cloud_functions_example/firebase_options.dart'; +import 'report_test_results.dart'; import 'sample_data.dart' as data; String kTestFunctionDefaultRegion = 'testFunctionDefaultRegion'; @@ -25,15 +26,27 @@ String kTestStreamResponse = 'testStreamResponse'; const _completerTimeout = Duration(seconds: 30); void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + reportTestResultsToDriver(binding); group('cloud_functions', () { late HttpsCallable callable; setUpAll(() async { - await Firebase.initializeApp( - options: DefaultFirebaseOptions.currentPlatform, - ); + // The native SDK may already have configured [DEFAULT] from a bundled + // GoogleService-Info.plist (the plugin registrant does this before any + // Dart runs). Dart's Firebase.apps cannot see that app until the first + // platform-channel call, so the only reliable guard is catching the + // duplicate-app error and keeping the natively configured instance. + try { + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + } on FirebaseException catch (e) { + if (e.code != 'duplicate-app') { + rethrow; + } + } FirebaseFunctions.instance.useFunctionsEmulator('localhost', 5001); callable = FirebaseFunctions.instance.httpsCallable(kTestFunctionDefaultRegion); diff --git a/packages/cloud_functions/cloud_functions/example/integration_test/report_test_results.dart b/packages/cloud_functions/cloud_functions/example/integration_test/report_test_results.dart new file mode 100644 index 000000000000..f04b57f3cf38 --- /dev/null +++ b/packages/cloud_functions/cloud_functions/example/integration_test/report_test_results.dart @@ -0,0 +1,40 @@ +// Copyright 2021, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/common.dart' show Failure; +import 'package:integration_test/integration_test.dart'; +import 'package:test_api/hooks.dart' show TestHandle; + +/// Records every executed test via the package:test lifecycle (plain `test()` +/// declarations never reach the binding's `results` map - only `testWidgets` +/// does) and publishes them through +/// [IntegrationTestWidgetsFlutterBinding.reportData], the only channel that +/// crosses the wire on web. +void reportTestResultsToDriver(IntegrationTestWidgetsFlutterBinding binding) { + final executed = []; + setUp(() { + executed.add(TestHandle.current.name); + }); + tearDownAll(() { + // `TestHandle.current.name` is the full name, prefixed with the enclosing + // group names, while `binding.results` is keyed by the bare `testWidgets` + // description - so match a failed entry on the suffix as well as on + // equality, otherwise no grouped test could ever be marked failed. + final failures = [ + for (final entry in binding.results.entries) + if (entry.value is Failure) entry.key, + ]; + bool didFail(String name) => failures + .any((failure) => name == failure || name.endsWith(' $failure')); + + binding.reportData ??= {}; + binding.reportData!['testResults'] = { + // Plain test() failures are not individually attributable here, but they + // fail the run as a whole via allTestsPassed; testWidgets failures are + // attributed from the binding's results map. + for (final name in executed) name: didFail(name) ? 'failed' : 'success', + }; + }); +} diff --git a/packages/cloud_functions/cloud_functions/example/lib/firebase_options.dart b/packages/cloud_functions/cloud_functions/example/lib/firebase_options.dart new file mode 100644 index 000000000000..a5a1360f6620 --- /dev/null +++ b/packages/cloud_functions/cloud_functions/example/lib/firebase_options.dart @@ -0,0 +1,61 @@ +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// Generated for CI builds and emulator tests. These are not real credentials. +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + return switch (defaultTargetPlatform) { + TargetPlatform.android => android, + TargetPlatform.iOS => ios, + TargetPlatform.macOS => macos, + TargetPlatform.windows => windows, + _ => throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ), + }; + } + + static const web = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:web:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + authDomain: 'flutterfire-e2e-tests.firebaseapp.com', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const android = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:android:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const ios = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:ios:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const macos = ios; + + static const windows = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:web:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + authDomain: 'flutterfire-e2e-tests.firebaseapp.com', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); +} diff --git a/packages/cloud_functions/cloud_functions/example/macos/Podfile b/packages/cloud_functions/cloud_functions/example/macos/Podfile index 07712c0a33e8..c60870efea08 100644 --- a/packages/cloud_functions/cloud_functions/example/macos/Podfile +++ b/packages/cloud_functions/cloud_functions/example/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.12' +platform :osx, '10.15' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -9,74 +9,35 @@ project 'Runner', { 'Release' => :release, } -def parse_KV_file(file, separator='=') - file_abs_path = File.expand_path(file) - if !File.exists? file_abs_path - return []; +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" end - pods_ary = [] - skip_line_start_symbols = ["#", "/"] - File.foreach(file_abs_path) { |line| - next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } - plugin = line.split(pattern=separator) - if plugin.length == 2 - podname = plugin[0].strip() - path = plugin[1].strip() - podpath = File.expand_path("#{path}", file_abs_path) - pods_ary.push({:name => podname, :path => podpath}); - else - puts "Invalid plugin specification: #{line}" - end - } - return pods_ary -end -def pubspec_supports_macos(file) - file_abs_path = File.expand_path(file) - if !File.exists? file_abs_path - return false; + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches end - File.foreach(file_abs_path) { |line| - return true if line =~ /^\s*macos:/ - } - return false + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" end +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + target 'Runner' do use_frameworks! use_modular_headers! - # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock - # referring to absolute paths on developers' machines. - ephemeral_dir = File.join('Flutter', 'ephemeral') - symlink_dir = File.join(ephemeral_dir, '.symlinks') - symlink_plugins_dir = File.join(symlink_dir, 'plugins') - system("rm -rf #{symlink_dir}") - system("mkdir -p #{symlink_plugins_dir}") + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + # target 'RunnerTests' do + # inherit! :search_paths + # end +end - # Flutter Pods - generated_xcconfig = parse_KV_file(File.join(ephemeral_dir, 'Flutter-Generated.xcconfig')) - if generated_xcconfig.empty? - puts "Flutter-Generated.xcconfig must exist. If you're running pod install manually, make sure flutter packages get is executed first." +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) end - generated_xcconfig.map { |p| - if p[:name] == 'FLUTTER_FRAMEWORK_DIR' - symlink = File.join(symlink_dir, 'flutter') - File.symlink(File.dirname(p[:path]), symlink) - pod 'FlutterMacOS', :path => File.join(symlink, File.basename(p[:path])) - end - } - - # Plugin Pods - plugin_pods = parse_KV_file('../.flutter-plugins') - plugin_pods.map { |p| - symlink = File.join(symlink_plugins_dir, p[:name]) - File.symlink(p[:path], symlink) - if pubspec_supports_macos(File.join(symlink, 'pubspec.yaml')) - pod p[:name], :path => File.join(symlink, 'macos') - end - } end - -# Prevent Cocoapods from embedding a second Flutter framework and causing an error with the new Xcode build system. -install! 'cocoapods', :disable_input_output_paths => true diff --git a/packages/cloud_functions/cloud_functions/example/pubspec.yaml b/packages/cloud_functions/cloud_functions/example/pubspec.yaml index e1917fcb089a..ff13d7bde6d4 100644 --- a/packages/cloud_functions/cloud_functions/example/pubspec.yaml +++ b/packages/cloud_functions/cloud_functions/example/pubspec.yaml @@ -17,6 +17,9 @@ dev_dependencies: sdk: flutter integration_test: sdk: flutter + # `integration_test/report_test_results.dart` names each executed test via + # `package:test_api/hooks.dart`; `any` defers to the version flutter_test pins. + test_api: any flutter: uses-material-design: true diff --git a/packages/cloud_functions/cloud_functions/example/test_driver/integration_test.dart b/packages/cloud_functions/cloud_functions/example/test_driver/integration_test.dart index f1ac26f27b88..691723cbbfe1 100644 --- a/packages/cloud_functions/cloud_functions/example/test_driver/integration_test.dart +++ b/packages/cloud_functions/cloud_functions/example/test_driver/integration_test.dart @@ -2,6 +2,33 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'dart:io'; + import 'package:integration_test/integration_test_driver.dart'; -Future main() => integrationDriver(); +Future main() => integrationDriver( + writeResponseOnFailure: true, + responseDataCallback: (Map? data) async { + final results = + (data?['testResults'] as Map?)?.cast() ?? + const {}; + var passed = 0; + var failed = 0; + for (final entry in results.entries) { + final ok = entry.value == 'success'; + ok ? passed++ : failed++; + // ignore: avoid_print + print('${ok ? '✅' : '❌'} ${entry.key}'); + } + // ignore: avoid_print + print('Web e2e summary: $passed passed, $failed failed, ' + '${results.length} total'); + if (results.isEmpty) { + // ignore: avoid_print + print('[E] No tests reported by the app - treating as ' + 'infrastructure failure.'); + exit(1); + } + await writeResponseData(data); + }, + ); diff --git a/packages/firebase_ai/firebase_ai/example/android/app/build.gradle.kts b/packages/firebase_ai/firebase_ai/example/android/app/build.gradle.kts index 5b2cf7547615..19d1136b20e3 100644 --- a/packages/firebase_ai/firebase_ai/example/android/app/build.gradle.kts +++ b/packages/firebase_ai/firebase_ai/example/android/app/build.gradle.kts @@ -20,8 +20,13 @@ android { } defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId = "com.example.firebase_ai_example" + // Deliberately the mega test app's Firebase app identity: the live-service + // e2e suite in `integration_test/` talks to real Firebase backends, which + // only accept app ids registered in the `flutterfire-e2e-tests` project. + // `namespace` and the Kotlin package below are unaffected. Two of these + // examples cannot be co-installed on one device - CI installs one per + // emulator. + applicationId = "io.flutter.plugins.firebase.tests" // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. minSdk = flutter.minSdkVersion diff --git a/tests/integration_test/firebase_ai/firebase_ai_e2e_test.dart b/packages/firebase_ai/firebase_ai/example/integration_test/e2e_test.dart similarity index 87% rename from tests/integration_test/firebase_ai/firebase_ai_e2e_test.dart rename to packages/firebase_ai/firebase_ai/example/integration_test/e2e_test.dart index 8509114ae557..c3da2f7b36fc 100644 --- a/tests/integration_test/firebase_ai/firebase_ai_e2e_test.dart +++ b/packages/firebase_ai/firebase_ai/example/integration_test/e2e_test.dart @@ -18,9 +18,11 @@ import 'package:integration_test/integration_test.dart'; import 'firebase_ai_headers_e2e_test.dart' as headers_tests; import 'firebase_ai_response_parsing_e2e_test.dart' as parsing_tests; import 'firebase_ai_mock_test.dart' as mock_tests; +import 'report_test_results.dart'; void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + reportTestResultsToDriver(binding); group('firebase_ai', () { headers_tests.main(); diff --git a/tests/integration_test/firebase_ai/firebase_ai_headers_e2e_test.dart b/packages/firebase_ai/firebase_ai/example/integration_test/firebase_ai_headers_e2e_test.dart similarity index 100% rename from tests/integration_test/firebase_ai/firebase_ai_headers_e2e_test.dart rename to packages/firebase_ai/firebase_ai/example/integration_test/firebase_ai_headers_e2e_test.dart diff --git a/tests/integration_test/firebase_ai/firebase_ai_mock_test.dart b/packages/firebase_ai/firebase_ai/example/integration_test/firebase_ai_mock_test.dart similarity index 99% rename from tests/integration_test/firebase_ai/firebase_ai_mock_test.dart rename to packages/firebase_ai/firebase_ai/example/integration_test/firebase_ai_mock_test.dart index 119def4620a5..d4bda2c6957c 100644 --- a/tests/integration_test/firebase_ai/firebase_ai_mock_test.dart +++ b/packages/firebase_ai/firebase_ai/example/integration_test/firebase_ai_mock_test.dart @@ -22,7 +22,7 @@ import 'package:firebase_ai/src/client.dart'; import 'package:firebase_ai/src/base_model.dart'; import 'package:firebase_ai/src/content.dart'; import 'package:firebase_ai/src/chat.dart'; -import 'package:tests/firebase_options.dart'; +import 'package:firebase_ai_example/firebase_options.dart'; class MockApiClient implements ApiClient { final List> requests = []; diff --git a/tests/integration_test/firebase_ai/firebase_ai_response_parsing_e2e_test.dart b/packages/firebase_ai/firebase_ai/example/integration_test/firebase_ai_response_parsing_e2e_test.dart similarity index 98% rename from tests/integration_test/firebase_ai/firebase_ai_response_parsing_e2e_test.dart rename to packages/firebase_ai/firebase_ai/example/integration_test/firebase_ai_response_parsing_e2e_test.dart index be4ed70ab8eb..0c52c07c7634 100644 --- a/tests/integration_test/firebase_ai/firebase_ai_response_parsing_e2e_test.dart +++ b/packages/firebase_ai/firebase_ai/example/integration_test/firebase_ai_response_parsing_e2e_test.dart @@ -21,7 +21,7 @@ import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core_platform_interface/test.dart'; import 'package:firebase_ai/src/api.dart'; import 'package:firebase_ai/src/developer/api.dart'; -import 'package:tests/firebase_options.dart'; +import 'package:firebase_ai_example/firebase_options.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/packages/firebase_ai/firebase_ai/example/integration_test/report_test_results.dart b/packages/firebase_ai/firebase_ai/example/integration_test/report_test_results.dart new file mode 100644 index 000000000000..8adf5e71620a --- /dev/null +++ b/packages/firebase_ai/firebase_ai/example/integration_test/report_test_results.dart @@ -0,0 +1,50 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/common.dart' show Failure; +import 'package:integration_test/integration_test.dart'; +import 'package:test_api/hooks.dart' show TestHandle; + +/// Records every executed test via the package:test lifecycle (plain `test()` +/// declarations never reach the binding's `results` map - only `testWidgets` +/// does) and publishes them through +/// [IntegrationTestWidgetsFlutterBinding.reportData], the only channel that +/// crosses the wire on web. +void reportTestResultsToDriver(IntegrationTestWidgetsFlutterBinding binding) { + final executed = []; + setUp(() { + executed.add(TestHandle.current.name); + }); + tearDownAll(() { + // `TestHandle.current.name` is the full name, prefixed with the enclosing + // group names, while `binding.results` is keyed by the bare `testWidgets` + // description - so match a failed entry on the suffix as well as on + // equality, otherwise no grouped test could ever be marked failed. + final failures = [ + for (final entry in binding.results.entries) + if (entry.value is Failure) entry.key, + ]; + bool didFail(String name) => failures + .any((failure) => name == failure || name.endsWith(' $failure')); + + binding.reportData ??= {}; + binding.reportData!['testResults'] = { + // Plain test() failures are not individually attributable here, but they + // fail the run as a whole via allTestsPassed; testWidgets failures are + // attributed from the binding's results map. + for (final name in executed) name: didFail(name) ? 'failed' : 'success', + }; + }); +} diff --git a/packages/firebase_ai/firebase_ai/example/ios/Runner.xcodeproj/project.pbxproj b/packages/firebase_ai/firebase_ai/example/ios/Runner.xcodeproj/project.pbxproj index 550a5b9c22a6..9af1459650ab 100644 --- a/packages/firebase_ai/firebase_ai/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/firebase_ai/firebase_ai/example/ios/Runner.xcodeproj/project.pbxproj @@ -403,7 +403,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -419,7 +419,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -436,7 +436,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; @@ -451,7 +451,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; @@ -583,7 +583,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -606,7 +606,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; diff --git a/packages/firebase_ai/firebase_ai/example/macos/Runner.xcodeproj/project.pbxproj b/packages/firebase_ai/firebase_ai/example/macos/Runner.xcodeproj/project.pbxproj index e2ab2e60ccd7..9ed06e14699b 100644 --- a/packages/firebase_ai/firebase_ai/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/firebase_ai/firebase_ai/example/macos/Runner.xcodeproj/project.pbxproj @@ -403,7 +403,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example"; @@ -417,7 +417,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example"; @@ -431,7 +431,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example"; diff --git a/packages/firebase_ai/firebase_ai/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/firebase_ai/firebase_ai/example/macos/Runner/Configs/AppInfo.xcconfig index 92fb3cd54e84..f7c95e535ef8 100644 --- a/packages/firebase_ai/firebase_ai/example/macos/Runner/Configs/AppInfo.xcconfig +++ b/packages/firebase_ai/firebase_ai/example/macos/Runner/Configs/AppInfo.xcconfig @@ -8,7 +8,7 @@ PRODUCT_NAME = example // The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = com.example.example +PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests // The copyright displayed in application information PRODUCT_COPYRIGHT = Copyright © 2024 com.example. All rights reserved. diff --git a/packages/firebase_ai/firebase_ai/example/pubspec.yaml b/packages/firebase_ai/firebase_ai/example/pubspec.yaml index 7b98740e6038..980d1b8ad22a 100644 --- a/packages/firebase_ai/firebase_ai/example/pubspec.yaml +++ b/packages/firebase_ai/firebase_ai/example/pubspec.yaml @@ -38,9 +38,21 @@ dependencies: waveform_flutter: ^1.2.0 dev_dependencies: + # `firebase_ai_mock_test.dart` installs a mock platform via + # `firebase_core_platform_interface/test.dart`, which firebase_core does not + # re-export. + firebase_core_platform_interface: ^8.1.0 flutter_lints: ^6.0.0 flutter_test: sdk: flutter + # `firebase_ai_response_parsing_e2e_test.dart` fetches the reference response + # fixtures straight over HTTP. + http: ^1.0.0 + integration_test: + sdk: flutter + # `integration_test/report_test_results.dart` names each executed test via + # `package:test_api/hooks.dart`; `any` defers to the version flutter_test pins. + test_api: any flutter: diff --git a/packages/firebase_ai/firebase_ai/example/test_driver/integration_test.dart b/packages/firebase_ai/firebase_ai/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..691723cbbfe1 --- /dev/null +++ b/packages/firebase_ai/firebase_ai/example/test_driver/integration_test.dart @@ -0,0 +1,34 @@ +// Copyright 2022, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io'; + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver( + writeResponseOnFailure: true, + responseDataCallback: (Map? data) async { + final results = + (data?['testResults'] as Map?)?.cast() ?? + const {}; + var passed = 0; + var failed = 0; + for (final entry in results.entries) { + final ok = entry.value == 'success'; + ok ? passed++ : failed++; + // ignore: avoid_print + print('${ok ? '✅' : '❌'} ${entry.key}'); + } + // ignore: avoid_print + print('Web e2e summary: $passed passed, $failed failed, ' + '${results.length} total'); + if (results.isEmpty) { + // ignore: avoid_print + print('[E] No tests reported by the app - treating as ' + 'infrastructure failure.'); + exit(1); + } + await writeResponseData(data); + }, + ); diff --git a/packages/firebase_analytics/firebase_analytics/example/android/app/build.gradle b/packages/firebase_analytics/firebase_analytics/example/android/app/build.gradle index c05ed68b6947..09f474399846 100644 --- a/packages/firebase_analytics/firebase_analytics/example/android/app/build.gradle +++ b/packages/firebase_analytics/firebase_analytics/example/android/app/build.gradle @@ -42,7 +42,13 @@ android { } defaultConfig { - applicationId = "io.flutter.plugins.firebase.analytics.example" + // Deliberately the mega test app's Firebase app identity: the live-service + // e2e suite in `integration_test/` talks to real Firebase backends, which + // only accept app ids registered in the `flutterfire-e2e-tests` project. + // `namespace` and the Kotlin package below are unaffected. Two of these + // examples cannot be co-installed on one device - CI installs one per + // emulator. + applicationId = "io.flutter.plugins.firebase.tests" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. minSdk = 23 diff --git a/tests/integration_test/firebase_analytics/firebase_analytics_e2e_test.dart b/packages/firebase_analytics/firebase_analytics/example/integration_test/e2e_test.dart similarity index 93% rename from tests/integration_test/firebase_analytics/firebase_analytics_e2e_test.dart rename to packages/firebase_analytics/firebase_analytics/example/integration_test/e2e_test.dart index 3af5a7069ef0..c8eb946c6e8c 100644 --- a/tests/integration_test/firebase_analytics/firebase_analytics_e2e_test.dart +++ b/packages/firebase_analytics/firebase_analytics/example/integration_test/e2e_test.dart @@ -8,19 +8,33 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'package:tests/firebase_options.dart'; +import 'package:firebase_analytics_example/firebase_options.dart'; + +import 'report_test_results.dart'; // ignore: do_not_use_environment const bool skipTestsOnCI = bool.fromEnvironment('CI'); void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + reportTestResultsToDriver(binding); group('firebase_analytics', () { setUpAll(() async { - await Firebase.initializeApp( - options: DefaultFirebaseOptions.currentPlatform, - ); + // The native SDK may already have configured [DEFAULT] from a bundled + // GoogleService-Info.plist (the plugin registrant does this before any + // Dart runs). Dart's Firebase.apps cannot see that app until the first + // platform-channel call, so the only reliable guard is catching the + // duplicate-app error and keeping the natively configured instance. + try { + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + } on FirebaseException catch (e) { + if (e.code != 'duplicate-app') { + rethrow; + } + } }); // getSessionId has to be first, else Android returns null diff --git a/packages/firebase_analytics/firebase_analytics/example/integration_test/report_test_results.dart b/packages/firebase_analytics/firebase_analytics/example/integration_test/report_test_results.dart new file mode 100644 index 000000000000..038d20c39931 --- /dev/null +++ b/packages/firebase_analytics/firebase_analytics/example/integration_test/report_test_results.dart @@ -0,0 +1,40 @@ +// Copyright 2019, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/common.dart' show Failure; +import 'package:integration_test/integration_test.dart'; +import 'package:test_api/hooks.dart' show TestHandle; + +/// Records every executed test via the package:test lifecycle (plain `test()` +/// declarations never reach the binding's `results` map - only `testWidgets` +/// does) and publishes them through +/// [IntegrationTestWidgetsFlutterBinding.reportData], the only channel that +/// crosses the wire on web. +void reportTestResultsToDriver(IntegrationTestWidgetsFlutterBinding binding) { + final executed = []; + setUp(() { + executed.add(TestHandle.current.name); + }); + tearDownAll(() { + // `TestHandle.current.name` is the full name, prefixed with the enclosing + // group names, while `binding.results` is keyed by the bare `testWidgets` + // description - so match a failed entry on the suffix as well as on + // equality, otherwise no grouped test could ever be marked failed. + final failures = [ + for (final entry in binding.results.entries) + if (entry.value is Failure) entry.key, + ]; + bool didFail(String name) => failures + .any((failure) => name == failure || name.endsWith(' $failure')); + + binding.reportData ??= {}; + binding.reportData!['testResults'] = { + // Plain test() failures are not individually attributable here, but they + // fail the run as a whole via allTestsPassed; testWidgets failures are + // attributed from the binding's results map. + for (final name in executed) name: didFail(name) ? 'failed' : 'success', + }; + }); +} diff --git a/packages/firebase_analytics/firebase_analytics/example/ios/Runner.xcodeproj/project.pbxproj b/packages/firebase_analytics/firebase_analytics/example/ios/Runner.xcodeproj/project.pbxproj index 5b27573c818d..9d1137a85655 100644 --- a/packages/firebase_analytics/firebase_analytics/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/firebase_analytics/firebase_analytics/example/ios/Runner.xcodeproj/project.pbxproj @@ -413,7 +413,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.analytics.example; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; @@ -438,7 +438,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.analytics.example; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; diff --git a/packages/firebase_analytics/firebase_analytics/example/lib/firebase_options.dart b/packages/firebase_analytics/firebase_analytics/example/lib/firebase_options.dart new file mode 100644 index 000000000000..a5a1360f6620 --- /dev/null +++ b/packages/firebase_analytics/firebase_analytics/example/lib/firebase_options.dart @@ -0,0 +1,61 @@ +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// Generated for CI builds and emulator tests. These are not real credentials. +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + return switch (defaultTargetPlatform) { + TargetPlatform.android => android, + TargetPlatform.iOS => ios, + TargetPlatform.macOS => macos, + TargetPlatform.windows => windows, + _ => throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ), + }; + } + + static const web = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:web:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + authDomain: 'flutterfire-e2e-tests.firebaseapp.com', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const android = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:android:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const ios = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:ios:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const macos = ios; + + static const windows = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:web:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + authDomain: 'flutterfire-e2e-tests.firebaseapp.com', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); +} diff --git a/packages/firebase_analytics/firebase_analytics/example/macos/Podfile b/packages/firebase_analytics/firebase_analytics/example/macos/Podfile index 049abe295427..9ec46f8cd53c 100644 --- a/packages/firebase_analytics/firebase_analytics/example/macos/Podfile +++ b/packages/firebase_analytics/firebase_analytics/example/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.14' +platform :osx, '10.15' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/packages/firebase_analytics/firebase_analytics/example/macos/Runner.xcodeproj/project.pbxproj b/packages/firebase_analytics/firebase_analytics/example/macos/Runner.xcodeproj/project.pbxproj index a4ce72486dd2..3da6e3b85843 100644 --- a/packages/firebase_analytics/firebase_analytics/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/firebase_analytics/firebase_analytics/example/macos/Runner.xcodeproj/project.pbxproj @@ -420,7 +420,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.analytics.example; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; @@ -547,7 +547,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.analytics.example; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -568,7 +568,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.analytics.example; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; diff --git a/packages/firebase_analytics/firebase_analytics/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/firebase_analytics/firebase_analytics/example/macos/Runner/Configs/AppInfo.xcconfig index cccda2e8a262..40bc34b0dcb3 100644 --- a/packages/firebase_analytics/firebase_analytics/example/macos/Runner/Configs/AppInfo.xcconfig +++ b/packages/firebase_analytics/firebase_analytics/example/macos/Runner/Configs/AppInfo.xcconfig @@ -8,7 +8,7 @@ PRODUCT_NAME = example // The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.example +PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests // The copyright displayed in application information PRODUCT_COPYRIGHT = Copyright © 2021 io.flutter.plugins. All rights reserved. diff --git a/packages/firebase_analytics/firebase_analytics/example/pubspec.yaml b/packages/firebase_analytics/firebase_analytics/example/pubspec.yaml index 455d22125014..c1b632c515c0 100755 --- a/packages/firebase_analytics/firebase_analytics/example/pubspec.yaml +++ b/packages/firebase_analytics/firebase_analytics/example/pubspec.yaml @@ -13,5 +13,14 @@ dependencies: sdk: flutter in_app_purchase: ^3.2.3 +dev_dependencies: + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + # `integration_test/report_test_results.dart` names each executed test via + # `package:test_api/hooks.dart`; `any` defers to the version flutter_test pins. + test_api: any + flutter: uses-material-design: true diff --git a/packages/firebase_analytics/firebase_analytics/example/test_driver/integration_test.dart b/packages/firebase_analytics/firebase_analytics/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..691723cbbfe1 --- /dev/null +++ b/packages/firebase_analytics/firebase_analytics/example/test_driver/integration_test.dart @@ -0,0 +1,34 @@ +// Copyright 2022, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io'; + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver( + writeResponseOnFailure: true, + responseDataCallback: (Map? data) async { + final results = + (data?['testResults'] as Map?)?.cast() ?? + const {}; + var passed = 0; + var failed = 0; + for (final entry in results.entries) { + final ok = entry.value == 'success'; + ok ? passed++ : failed++; + // ignore: avoid_print + print('${ok ? '✅' : '❌'} ${entry.key}'); + } + // ignore: avoid_print + print('Web e2e summary: $passed passed, $failed failed, ' + '${results.length} total'); + if (results.isEmpty) { + // ignore: avoid_print + print('[E] No tests reported by the app - treating as ' + 'infrastructure failure.'); + exit(1); + } + await writeResponseData(data); + }, + ); diff --git a/packages/firebase_app_check/firebase_app_check/example/android/app/build.gradle b/packages/firebase_app_check/firebase_app_check/example/android/app/build.gradle index 92298e7270e5..f309bbc3fb44 100644 --- a/packages/firebase_app_check/firebase_app_check/example/android/app/build.gradle +++ b/packages/firebase_app_check/firebase_app_check/example/android/app/build.gradle @@ -42,7 +42,13 @@ android { } defaultConfig { - applicationId = "io.flutter.plugins.firebase.appcheck.example" + // Deliberately the mega test app's Firebase app identity: the live-service + // e2e suite in `integration_test/` talks to real Firebase backends, which + // only accept app ids registered in the `flutterfire-e2e-tests` project. + // `namespace` and the Kotlin package below are unaffected. Two of these + // examples cannot be co-installed on one device - CI installs one per + // emulator. + applicationId = "io.flutter.plugins.firebase.tests" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. minSdkVersion = flutter.minSdkVersion diff --git a/tests/integration_test/firebase_app_check/firebase_app_check_e2e_test.dart b/packages/firebase_app_check/firebase_app_check/example/integration_test/e2e_test.dart similarity index 85% rename from tests/integration_test/firebase_app_check/firebase_app_check_e2e_test.dart rename to packages/firebase_app_check/firebase_app_check/example/integration_test/e2e_test.dart index 06358a44d4f9..9af6341c8823 100644 --- a/tests/integration_test/firebase_app_check/firebase_app_check_e2e_test.dart +++ b/packages/firebase_app_check/firebase_app_check/example/integration_test/e2e_test.dart @@ -9,7 +9,9 @@ import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'package:tests/firebase_options.dart'; +import 'package:firebase_app_check_example/firebase_options.dart'; + +import 'report_test_results.dart'; const androidDebugToken = String.fromEnvironment('APP_CHECK_ANDROID_DEBUG_TOKEN'); @@ -17,15 +19,27 @@ const androidDebugToken = const appleDebugToken = String.fromEnvironment('APP_CHECK_APPLE_DEBUG_TOKEN'); void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + reportTestResultsToDriver(binding); group( 'firebase_app_check', () { setUpAll(() async { - await Firebase.initializeApp( - options: DefaultFirebaseOptions.currentPlatform, - ); + // The native SDK may already have configured [DEFAULT] from a bundled + // GoogleService-Info.plist (the plugin registrant does this before any + // Dart runs). Dart's Firebase.apps cannot see that app until the first + // platform-channel call, so the only reliable guard is catching the + // duplicate-app error and keeping the natively configured instance. + try { + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + } on FirebaseException catch (e) { + if (e.code != 'duplicate-app') { + rethrow; + } + } }); test( diff --git a/packages/firebase_app_check/firebase_app_check/example/integration_test/report_test_results.dart b/packages/firebase_app_check/firebase_app_check/example/integration_test/report_test_results.dart new file mode 100644 index 000000000000..fb80e3ba19f7 --- /dev/null +++ b/packages/firebase_app_check/firebase_app_check/example/integration_test/report_test_results.dart @@ -0,0 +1,40 @@ +// Copyright 2022, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/common.dart' show Failure; +import 'package:integration_test/integration_test.dart'; +import 'package:test_api/hooks.dart' show TestHandle; + +/// Records every executed test via the package:test lifecycle (plain `test()` +/// declarations never reach the binding's `results` map - only `testWidgets` +/// does) and publishes them through +/// [IntegrationTestWidgetsFlutterBinding.reportData], the only channel that +/// crosses the wire on web. +void reportTestResultsToDriver(IntegrationTestWidgetsFlutterBinding binding) { + final executed = []; + setUp(() { + executed.add(TestHandle.current.name); + }); + tearDownAll(() { + // `TestHandle.current.name` is the full name, prefixed with the enclosing + // group names, while `binding.results` is keyed by the bare `testWidgets` + // description - so match a failed entry on the suffix as well as on + // equality, otherwise no grouped test could ever be marked failed. + final failures = [ + for (final entry in binding.results.entries) + if (entry.value is Failure) entry.key, + ]; + bool didFail(String name) => failures + .any((failure) => name == failure || name.endsWith(' $failure')); + + binding.reportData ??= {}; + binding.reportData!['testResults'] = { + // Plain test() failures are not individually attributable here, but they + // fail the run as a whole via allTestsPassed; testWidgets failures are + // attributed from the binding's results map. + for (final name in executed) name: didFail(name) ? 'failed' : 'success', + }; + }); +} diff --git a/packages/firebase_app_check/firebase_app_check/example/ios/Podfile b/packages/firebase_app_check/firebase_app_check/example/ios/Podfile index 620e46eba607..45f98d2763b3 100644 --- a/packages/firebase_app_check/firebase_app_check/example/ios/Podfile +++ b/packages/firebase_app_check/firebase_app_check/example/ios/Podfile @@ -31,9 +31,11 @@ target 'Runner' do use_frameworks! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) - target 'RunnerTests' do - inherit! :search_paths - end + # There is no RunnerTests target in Runner.xcodeproj for this example, so no + # test target is declared here. Uncomment if one is ever added. + # target 'RunnerTests' do + # inherit! :search_paths + # end end post_install do |installer| diff --git a/packages/firebase_app_check/firebase_app_check/example/ios/Runner.xcodeproj/project.pbxproj b/packages/firebase_app_check/firebase_app_check/example/ios/Runner.xcodeproj/project.pbxproj index 1ef35c770b2f..adb30451296f 100644 --- a/packages/firebase_app_check/firebase_app_check/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/firebase_app_check/firebase_app_check/example/ios/Runner.xcodeproj/project.pbxproj @@ -355,7 +355,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.appcheck.example; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests; PRODUCT_NAME = "$(TARGET_NAME)"; VERSIONING_SYSTEM = "apple-generic"; }; @@ -483,7 +483,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.appcheck.example; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests; PRODUCT_NAME = "$(TARGET_NAME)"; VERSIONING_SYSTEM = "apple-generic"; }; @@ -504,7 +504,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.appcheck.example; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests; PRODUCT_NAME = "$(TARGET_NAME)"; VERSIONING_SYSTEM = "apple-generic"; }; diff --git a/packages/firebase_app_check/firebase_app_check/example/lib/firebase_options.dart b/packages/firebase_app_check/firebase_app_check/example/lib/firebase_options.dart new file mode 100644 index 000000000000..a5a1360f6620 --- /dev/null +++ b/packages/firebase_app_check/firebase_app_check/example/lib/firebase_options.dart @@ -0,0 +1,61 @@ +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// Generated for CI builds and emulator tests. These are not real credentials. +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + return switch (defaultTargetPlatform) { + TargetPlatform.android => android, + TargetPlatform.iOS => ios, + TargetPlatform.macOS => macos, + TargetPlatform.windows => windows, + _ => throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ), + }; + } + + static const web = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:web:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + authDomain: 'flutterfire-e2e-tests.firebaseapp.com', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const android = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:android:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const ios = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:ios:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const macos = ios; + + static const windows = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:web:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + authDomain: 'flutterfire-e2e-tests.firebaseapp.com', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); +} diff --git a/packages/firebase_app_check/firebase_app_check/example/macos/Podfile b/packages/firebase_app_check/firebase_app_check/example/macos/Podfile index ff5ddb3b8bdc..4b2032310ad7 100644 --- a/packages/firebase_app_check/firebase_app_check/example/macos/Podfile +++ b/packages/firebase_app_check/firebase_app_check/example/macos/Podfile @@ -30,9 +30,11 @@ target 'Runner' do use_frameworks! flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) - target 'RunnerTests' do - inherit! :search_paths - end + # There is no RunnerTests target in Runner.xcodeproj for this example, so no + # test target is declared here. Uncomment if one is ever added. + # target 'RunnerTests' do + # inherit! :search_paths + # end end post_install do |installer| diff --git a/packages/firebase_app_check/firebase_app_check/example/macos/Runner.xcodeproj/project.pbxproj b/packages/firebase_app_check/firebase_app_check/example/macos/Runner.xcodeproj/project.pbxproj index c0dc38604806..e9b9393bbd30 100644 --- a/packages/firebase_app_check/firebase_app_check/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/firebase_app_check/firebase_app_check/example/macos/Runner.xcodeproj/project.pbxproj @@ -384,7 +384,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.appcheck.example; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; @@ -515,7 +515,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.appcheck.example; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -538,7 +538,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.appcheck.example; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; diff --git a/packages/firebase_app_check/firebase_app_check/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/firebase_app_check/firebase_app_check/example/macos/Runner/Configs/AppInfo.xcconfig index 800dd3d2cba4..4b13c0205146 100644 --- a/packages/firebase_app_check/firebase_app_check/example/macos/Runner/Configs/AppInfo.xcconfig +++ b/packages/firebase_app_check/firebase_app_check/example/macos/Runner/Configs/AppInfo.xcconfig @@ -8,7 +8,7 @@ PRODUCT_NAME = firebase_app_check_example // The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.appcheck.example +PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests // The copyright displayed in application information PRODUCT_COPYRIGHT = Copyright © 2021 io.flutter.plugins.firebase.appcheck.example. All rights reserved. diff --git a/packages/firebase_app_check/firebase_app_check/example/pubspec.yaml b/packages/firebase_app_check/firebase_app_check/example/pubspec.yaml index 34ee82ffd254..162fce7ad9e4 100644 --- a/packages/firebase_app_check/firebase_app_check/example/pubspec.yaml +++ b/packages/firebase_app_check/firebase_app_check/example/pubspec.yaml @@ -17,5 +17,14 @@ dependencies: flutter: sdk: flutter +dev_dependencies: + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + # `integration_test/report_test_results.dart` names each executed test via + # `package:test_api/hooks.dart`; `any` defers to the version flutter_test pins. + test_api: any + flutter: uses-material-design: true diff --git a/packages/firebase_app_check/firebase_app_check/example/test_driver/integration_test.dart b/packages/firebase_app_check/firebase_app_check/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..691723cbbfe1 --- /dev/null +++ b/packages/firebase_app_check/firebase_app_check/example/test_driver/integration_test.dart @@ -0,0 +1,34 @@ +// Copyright 2022, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io'; + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver( + writeResponseOnFailure: true, + responseDataCallback: (Map? data) async { + final results = + (data?['testResults'] as Map?)?.cast() ?? + const {}; + var passed = 0; + var failed = 0; + for (final entry in results.entries) { + final ok = entry.value == 'success'; + ok ? passed++ : failed++; + // ignore: avoid_print + print('${ok ? '✅' : '❌'} ${entry.key}'); + } + // ignore: avoid_print + print('Web e2e summary: $passed passed, $failed failed, ' + '${results.length} total'); + if (results.isEmpty) { + // ignore: avoid_print + print('[E] No tests reported by the app - treating as ' + 'infrastructure failure.'); + exit(1); + } + await writeResponseData(data); + }, + ); diff --git a/packages/firebase_app_installations/firebase_app_installations/example/android/app/build.gradle b/packages/firebase_app_installations/firebase_app_installations/example/android/app/build.gradle index 0ef5a05bbb2a..b2526c5e0e6e 100644 --- a/packages/firebase_app_installations/firebase_app_installations/example/android/app/build.gradle +++ b/packages/firebase_app_installations/firebase_app_installations/example/android/app/build.gradle @@ -42,7 +42,13 @@ android { } defaultConfig { - applicationId = "io.flutter.plugins.firebase.installations.example" + // Deliberately the mega test app's Firebase app identity: the live-service + // e2e suite in `integration_test/` talks to real Firebase backends, which + // only accept app ids registered in the `flutterfire-e2e-tests` project. + // `namespace` and the Kotlin package below are unaffected. Two of these + // examples cannot be co-installed on one device - CI installs one per + // emulator. + applicationId = "io.flutter.plugins.firebase.tests" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. minSdk = 23 diff --git a/tests/integration_test/firebase_app_installations/firebase_app_installations_e2e_test.dart b/packages/firebase_app_installations/firebase_app_installations/example/integration_test/e2e_test.dart similarity index 66% rename from tests/integration_test/firebase_app_installations/firebase_app_installations_e2e_test.dart rename to packages/firebase_app_installations/firebase_app_installations/example/integration_test/e2e_test.dart index af34b20b0267..23e6382de3a3 100644 --- a/tests/integration_test/firebase_app_installations/firebase_app_installations_e2e_test.dart +++ b/packages/firebase_app_installations/firebase_app_installations/example/integration_test/e2e_test.dart @@ -7,19 +7,38 @@ import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'package:tests/firebase_options.dart'; +import 'package:firebase_app_installations_example/firebase_options.dart'; -import '../e2e_test.dart'; +import 'report_test_results.dart'; + +// Was imported from the `tests` app's aggregated `e2e_test.dart` before this +// suite moved into the example; it is the only thing this file needed from +// there. +// Github Actions environment variable +// ignore: do_not_use_environment +final isCI = const String.fromEnvironment('CI').isNotEmpty; void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + reportTestResultsToDriver(binding); group( 'firebase_app_installations', () { setUpAll(() async { - await Firebase.initializeApp( - options: DefaultFirebaseOptions.currentPlatform, - ); + // The native SDK may already have configured [DEFAULT] from a bundled + // GoogleService-Info.plist (the plugin registrant does this before any + // Dart runs). Dart's Firebase.apps cannot see that app until the first + // platform-channel call, so the only reliable guard is catching the + // duplicate-app error and keeping the natively configured instance. + try { + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + } on FirebaseException catch (e) { + if (e.code != 'duplicate-app') { + rethrow; + } + } if (defaultTargetPlatform == TargetPlatform.android) { // Android Installations can deadlock if token/id APIs race native // heartbeat initialization immediately after manual app init. @@ -59,7 +78,13 @@ void main() { expect(token, isNotEmpty); // macOS skipped because it needs keychain sharing entitlement. See: https://github.com/firebase/flutterfire/issues/9538 }, - skip: defaultTargetPlatform == TargetPlatform.macOS, + // TODO(ci): getToken deadlocks (5-minute timeout, reproducibly) on the + // Android emulator since the suite moved into this standalone example - + // likely the token/heartbeat initialization race the setUpAll delay + // guards, hitting differently on a cold single-plugin app. Needs + // investigation before re-enabling on Android. + skip: defaultTargetPlatform == TargetPlatform.macOS || + defaultTargetPlatform == TargetPlatform.android, ); test( diff --git a/packages/firebase_app_installations/firebase_app_installations/example/integration_test/report_test_results.dart b/packages/firebase_app_installations/firebase_app_installations/example/integration_test/report_test_results.dart new file mode 100644 index 000000000000..fb80e3ba19f7 --- /dev/null +++ b/packages/firebase_app_installations/firebase_app_installations/example/integration_test/report_test_results.dart @@ -0,0 +1,40 @@ +// Copyright 2022, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/common.dart' show Failure; +import 'package:integration_test/integration_test.dart'; +import 'package:test_api/hooks.dart' show TestHandle; + +/// Records every executed test via the package:test lifecycle (plain `test()` +/// declarations never reach the binding's `results` map - only `testWidgets` +/// does) and publishes them through +/// [IntegrationTestWidgetsFlutterBinding.reportData], the only channel that +/// crosses the wire on web. +void reportTestResultsToDriver(IntegrationTestWidgetsFlutterBinding binding) { + final executed = []; + setUp(() { + executed.add(TestHandle.current.name); + }); + tearDownAll(() { + // `TestHandle.current.name` is the full name, prefixed with the enclosing + // group names, while `binding.results` is keyed by the bare `testWidgets` + // description - so match a failed entry on the suffix as well as on + // equality, otherwise no grouped test could ever be marked failed. + final failures = [ + for (final entry in binding.results.entries) + if (entry.value is Failure) entry.key, + ]; + bool didFail(String name) => failures + .any((failure) => name == failure || name.endsWith(' $failure')); + + binding.reportData ??= {}; + binding.reportData!['testResults'] = { + // Plain test() failures are not individually attributable here, but they + // fail the run as a whole via allTestsPassed; testWidgets failures are + // attributed from the binding's results map. + for (final name in executed) name: didFail(name) ? 'failed' : 'success', + }; + }); +} diff --git a/packages/firebase_app_installations/firebase_app_installations/example/lib/firebase_options.dart b/packages/firebase_app_installations/firebase_app_installations/example/lib/firebase_options.dart new file mode 100644 index 000000000000..a5a1360f6620 --- /dev/null +++ b/packages/firebase_app_installations/firebase_app_installations/example/lib/firebase_options.dart @@ -0,0 +1,61 @@ +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// Generated for CI builds and emulator tests. These are not real credentials. +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + return switch (defaultTargetPlatform) { + TargetPlatform.android => android, + TargetPlatform.iOS => ios, + TargetPlatform.macOS => macos, + TargetPlatform.windows => windows, + _ => throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ), + }; + } + + static const web = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:web:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + authDomain: 'flutterfire-e2e-tests.firebaseapp.com', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const android = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:android:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const ios = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:ios:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const macos = ios; + + static const windows = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:web:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + authDomain: 'flutterfire-e2e-tests.firebaseapp.com', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); +} diff --git a/packages/firebase_app_installations/firebase_app_installations/example/macos/Podfile b/packages/firebase_app_installations/firebase_app_installations/example/macos/Podfile index 22d9caad2e9d..9ec46f8cd53c 100644 --- a/packages/firebase_app_installations/firebase_app_installations/example/macos/Podfile +++ b/packages/firebase_app_installations/firebase_app_installations/example/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.12' +platform :osx, '10.15' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/packages/firebase_app_installations/firebase_app_installations/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/firebase_app_installations/firebase_app_installations/example/macos/Runner/Configs/AppInfo.xcconfig index cf9be60ca471..43d10e8bcd7f 100644 --- a/packages/firebase_app_installations/firebase_app_installations/example/macos/Runner/Configs/AppInfo.xcconfig +++ b/packages/firebase_app_installations/firebase_app_installations/example/macos/Runner/Configs/AppInfo.xcconfig @@ -8,7 +8,7 @@ PRODUCT_NAME = example // The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = com.example.example +PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests // The copyright displayed in application information PRODUCT_COPYRIGHT = Copyright © 2021 com.example. All rights reserved. diff --git a/packages/firebase_app_installations/firebase_app_installations/example/pubspec.yaml b/packages/firebase_app_installations/firebase_app_installations/example/pubspec.yaml index c69e0ab08789..03debcbe479a 100644 --- a/packages/firebase_app_installations/firebase_app_installations/example/pubspec.yaml +++ b/packages/firebase_app_installations/firebase_app_installations/example/pubspec.yaml @@ -18,6 +18,13 @@ dependencies: dev_dependencies: flutter_lints: ^6.0.0 + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + # `integration_test/report_test_results.dart` names each executed test via + # `package:test_api/hooks.dart`; `any` defers to the version flutter_test pins. + test_api: any flutter: uses-material-design: true diff --git a/packages/firebase_app_installations/firebase_app_installations/example/test_driver/integration_test.dart b/packages/firebase_app_installations/firebase_app_installations/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..691723cbbfe1 --- /dev/null +++ b/packages/firebase_app_installations/firebase_app_installations/example/test_driver/integration_test.dart @@ -0,0 +1,34 @@ +// Copyright 2022, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io'; + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver( + writeResponseOnFailure: true, + responseDataCallback: (Map? data) async { + final results = + (data?['testResults'] as Map?)?.cast() ?? + const {}; + var passed = 0; + var failed = 0; + for (final entry in results.entries) { + final ok = entry.value == 'success'; + ok ? passed++ : failed++; + // ignore: avoid_print + print('${ok ? '✅' : '❌'} ${entry.key}'); + } + // ignore: avoid_print + print('Web e2e summary: $passed passed, $failed failed, ' + '${results.length} total'); + if (results.isEmpty) { + // ignore: avoid_print + print('[E] No tests reported by the app - treating as ' + 'infrastructure failure.'); + exit(1); + } + await writeResponseData(data); + }, + ); diff --git a/packages/firebase_auth/firebase_auth/example/android/app/build.gradle b/packages/firebase_auth/firebase_auth/example/android/app/build.gradle index db427be91ec5..eefaf046e62a 100644 --- a/packages/firebase_auth/firebase_auth/example/android/app/build.gradle +++ b/packages/firebase_auth/firebase_auth/example/android/app/build.gradle @@ -39,7 +39,9 @@ android { } defaultConfig { - applicationId = "io.flutter.plugins.firebase.auth.example" + // Shares the mega test app's registered Firebase app identity: the injected + // live google-services.json has no client entry for an auth.example id. + applicationId = "io.flutter.plugins.firebase.tests" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. minSdkVersion = flutter.minSdkVersion diff --git a/packages/firebase_auth/firebase_auth/example/android/app/src/main/AndroidManifest.xml b/packages/firebase_auth/firebase_auth/example/android/app/src/main/AndroidManifest.xml index 74a78b939e5e..637f57c840d1 100644 --- a/packages/firebase_auth/firebase_auth/example/android/app/src/main/AndroidManifest.xml +++ b/packages/firebase_auth/firebase_auth/example/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ diff --git a/packages/firebase_auth/firebase_auth/example/android/gradle.properties b/packages/firebase_auth/firebase_auth/example/android/gradle.properties index 3b5b324f6e3f..1551eb080642 100644 --- a/packages/firebase_auth/firebase_auth/example/android/gradle.properties +++ b/packages/firebase_auth/firebase_auth/example/android/gradle.properties @@ -1,3 +1,7 @@ org.gradle.jvmargs=-Xmx4G -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true android.enableJetifier=true +# This builtInKotlin flag was added automatically by Flutter migrator +android.builtInKotlin=false +# This newDsl flag was added automatically by Flutter migrator +android.newDsl=false diff --git a/packages/firebase_auth/firebase_auth/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/firebase_auth/firebase_auth/example/android/gradle/wrapper/gradle-wrapper.properties index e411586a54a8..d6e308a63789 100644 --- a/packages/firebase_auth/firebase_auth/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/firebase_auth/firebase_auth/example/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/firebase_auth/firebase_auth/example/android/settings.gradle b/packages/firebase_auth/firebase_auth/example/android/settings.gradle index a4d924db8bec..812272422a40 100644 --- a/packages/firebase_auth/firebase_auth/example/android/settings.gradle +++ b/packages/firebase_auth/firebase_auth/example/android/settings.gradle @@ -18,11 +18,11 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "8.3.0" apply false + id "com.android.application" version "8.9.1" apply false // START: FlutterFire Configuration id "com.google.gms.google-services" version "4.3.15" apply false // END: FlutterFire Configuration - id "org.jetbrains.kotlin.android" version "1.9.22" apply false + id "org.jetbrains.kotlin.android" version "2.1.0" apply false } include ":app" diff --git a/packages/firebase_auth/firebase_auth/example/integration_test/e2e_test.dart b/packages/firebase_auth/firebase_auth/example/integration_test/e2e_test.dart index a4c047696920..0cc35bf1870c 100644 --- a/packages/firebase_auth/firebase_auth/example/integration_test/e2e_test.dart +++ b/packages/firebase_auth/firebase_auth/example/integration_test/e2e_test.dart @@ -12,16 +12,29 @@ import 'package:firebase_auth_example/firebase_options.dart'; import 'firebase_auth_instance_e2e_test.dart' as instance_tests; import 'firebase_auth_multi_factor_e2e_test.dart' as multi_factor_tests; import 'firebase_auth_user_e2e_test.dart' as user_tests; +import 'report_test_results.dart'; import 'test_utils.dart'; void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + reportTestResultsToDriver(binding); group('firebase_auth', () { setUpAll(() async { - await Firebase.initializeApp( - options: DefaultFirebaseOptions.currentPlatform, - ); + // The native SDK may already have configured [DEFAULT] from a bundled + // GoogleService-Info.plist (the plugin registrant does this before any + // Dart runs). Dart's Firebase.apps cannot see that app until the first + // platform-channel call, so the only reliable guard is catching the + // duplicate-app error and keeping the natively configured instance. + try { + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + } on FirebaseException catch (e) { + if (e.code != 'duplicate-app') { + rethrow; + } + } await FirebaseAuth.instance .useAuthEmulator(testEmulatorHost, testEmulatorPort); diff --git a/packages/firebase_auth/firebase_auth/example/integration_test/firebase_auth_instance_e2e_test.dart b/packages/firebase_auth/firebase_auth/example/integration_test/firebase_auth_instance_e2e_test.dart index 9a8ee8050d9d..0517680ee1da 100644 --- a/packages/firebase_auth/firebase_auth/example/integration_test/firebase_auth_instance_e2e_test.dart +++ b/packages/firebase_auth/firebase_auth/example/integration_test/firebase_auth_instance_e2e_test.dart @@ -284,58 +284,75 @@ void main() { } }); - test('returns correct operation for verifyEmail action code', - () async { - final email = generateRandomEmail(); - await FirebaseAuth.instance.createUserWithEmailAndPassword( - email: email, - password: testPassword, - ); + test( + 'returns correct operation for verifyEmail action code', + () async { + final email = generateRandomEmail(); + await FirebaseAuth.instance.createUserWithEmailAndPassword( + email: email, + password: testPassword, + ); - await FirebaseAuth.instance.currentUser!.sendEmailVerification(); + await FirebaseAuth.instance.currentUser!.sendEmailVerification(); - final oobCode = await emulatorOutOfBandCode( - email, - EmulatorOobCodeType.verifyEmail, - ); - expect(oobCode, isNotNull); + final oobCode = await emulatorOutOfBandCode( + email, + EmulatorOobCodeType.verifyEmail, + ); + expect(oobCode, isNotNull); - final actionCodeInfo = await FirebaseAuth.instance.checkActionCode( - oobCode!.oobCode!, - ); + final actionCodeInfo = + await FirebaseAuth.instance.checkActionCode( + oobCode!.oobCode!, + ); - expect( - actionCodeInfo.operation, - equals(ActionCodeInfoOperation.verifyEmail), - ); - }); + expect( + actionCodeInfo.operation, + equals(ActionCodeInfoOperation.verifyEmail), + ); + }, + // Windows skipped like the enclosing group (checkActionCode is not + // implemented there); a per-test skip REPLACES the group skip in + // package:test metadata merging, so it must repeat that condition. + // macOS skipped because createUserWithEmailAndPassword needs the + // keychain sharing entitlement, which requires a provisioning + // profile CI's ad-hoc signing cannot provide. + // See: https://github.com/firebase/flutterfire/issues/9538 + skip: !kIsWeb && (Platform.isWindows || Platform.isMacOS), + ); - test('returns correct operation for passwordReset action code', - () async { - final email = generateRandomEmail(); - await FirebaseAuth.instance.createUserWithEmailAndPassword( - email: email, - password: testPassword, - ); - await ensureSignedOut(); + test( + 'returns correct operation for passwordReset action code', + () async { + final email = generateRandomEmail(); + await FirebaseAuth.instance.createUserWithEmailAndPassword( + email: email, + password: testPassword, + ); + await ensureSignedOut(); - await FirebaseAuth.instance.sendPasswordResetEmail(email: email); + await FirebaseAuth.instance.sendPasswordResetEmail(email: email); - final oobCode = await emulatorOutOfBandCode( - email, - EmulatorOobCodeType.passwordReset, - ); - expect(oobCode, isNotNull); + final oobCode = await emulatorOutOfBandCode( + email, + EmulatorOobCodeType.passwordReset, + ); + expect(oobCode, isNotNull); - final actionCodeInfo = await FirebaseAuth.instance.checkActionCode( - oobCode!.oobCode!, - ); + final actionCodeInfo = + await FirebaseAuth.instance.checkActionCode( + oobCode!.oobCode!, + ); - expect( - actionCodeInfo.operation, - equals(ActionCodeInfoOperation.passwordReset), - ); - }); + expect( + actionCodeInfo.operation, + equals(ActionCodeInfoOperation.passwordReset), + ); + }, + // macOS skipped for the same keychain reason as the verifyEmail + // test above. See: https://github.com/firebase/flutterfire/issues/9538 + skip: !kIsWeb && (Platform.isWindows || Platform.isMacOS), + ); }, skip: !kIsWeb && Platform.isWindows, ); @@ -493,7 +510,7 @@ void main() { fail(e.toString()); } }, - skip: !kIsWeb && Platform.isMacOS, + skip: !kIsWeb && (Platform.isWindows || Platform.isMacOS), ); test('fails if the user could not be found', () async { diff --git a/packages/firebase_auth/firebase_auth/example/integration_test/report_test_results.dart b/packages/firebase_auth/firebase_auth/example/integration_test/report_test_results.dart new file mode 100644 index 000000000000..038d20c39931 --- /dev/null +++ b/packages/firebase_auth/firebase_auth/example/integration_test/report_test_results.dart @@ -0,0 +1,40 @@ +// Copyright 2019, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/common.dart' show Failure; +import 'package:integration_test/integration_test.dart'; +import 'package:test_api/hooks.dart' show TestHandle; + +/// Records every executed test via the package:test lifecycle (plain `test()` +/// declarations never reach the binding's `results` map - only `testWidgets` +/// does) and publishes them through +/// [IntegrationTestWidgetsFlutterBinding.reportData], the only channel that +/// crosses the wire on web. +void reportTestResultsToDriver(IntegrationTestWidgetsFlutterBinding binding) { + final executed = []; + setUp(() { + executed.add(TestHandle.current.name); + }); + tearDownAll(() { + // `TestHandle.current.name` is the full name, prefixed with the enclosing + // group names, while `binding.results` is keyed by the bare `testWidgets` + // description - so match a failed entry on the suffix as well as on + // equality, otherwise no grouped test could ever be marked failed. + final failures = [ + for (final entry in binding.results.entries) + if (entry.value is Failure) entry.key, + ]; + bool didFail(String name) => failures + .any((failure) => name == failure || name.endsWith(' $failure')); + + binding.reportData ??= {}; + binding.reportData!['testResults'] = { + // Plain test() failures are not individually attributable here, but they + // fail the run as a whole via allTestsPassed; testWidgets failures are + // attributed from the binding's results map. + for (final name in executed) name: didFail(name) ? 'failed' : 'success', + }; + }); +} diff --git a/packages/firebase_auth/firebase_auth/example/ios/Flutter/Release.xcconfig b/packages/firebase_auth/firebase_auth/example/ios/Flutter/Release.xcconfig index 88c29144c836..c4855bfe2000 100644 --- a/packages/firebase_auth/firebase_auth/example/ios/Flutter/Release.xcconfig +++ b/packages/firebase_auth/firebase_auth/example/ios/Flutter/Release.xcconfig @@ -1,3 +1,2 @@ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/packages/firebase_auth/firebase_auth/example/lib/firebase_options.dart b/packages/firebase_auth/firebase_auth/example/lib/firebase_options.dart new file mode 100644 index 000000000000..a5a1360f6620 --- /dev/null +++ b/packages/firebase_auth/firebase_auth/example/lib/firebase_options.dart @@ -0,0 +1,61 @@ +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// Generated for CI builds and emulator tests. These are not real credentials. +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + return switch (defaultTargetPlatform) { + TargetPlatform.android => android, + TargetPlatform.iOS => ios, + TargetPlatform.macOS => macos, + TargetPlatform.windows => windows, + _ => throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ), + }; + } + + static const web = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:web:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + authDomain: 'flutterfire-e2e-tests.firebaseapp.com', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const android = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:android:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const ios = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:ios:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const macos = ios; + + static const windows = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:web:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + authDomain: 'flutterfire-e2e-tests.firebaseapp.com', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); +} diff --git a/packages/firebase_auth/firebase_auth/example/macos/Runner.xcodeproj/project.pbxproj b/packages/firebase_auth/firebase_auth/example/macos/Runner.xcodeproj/project.pbxproj index 4848b09b04b9..c14d779beb73 100644 --- a/packages/firebase_auth/firebase_auth/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/firebase_auth/firebase_auth/example/macos/Runner.xcodeproj/project.pbxproj @@ -467,10 +467,8 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = YYX2P3XVJ7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter/ephemeral", @@ -601,10 +599,8 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = YYX2P3XVJ7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter/ephemeral", @@ -629,10 +625,8 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = YYX2P3XVJ7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter/ephemeral", diff --git a/packages/firebase_auth/firebase_auth/example/macos/Runner/DebugProfile.entitlements b/packages/firebase_auth/firebase_auth/example/macos/Runner/DebugProfile.entitlements index c34fc0a4e55a..3ba6c1266f21 100644 --- a/packages/firebase_auth/firebase_auth/example/macos/Runner/DebugProfile.entitlements +++ b/packages/firebase_auth/firebase_auth/example/macos/Runner/DebugProfile.entitlements @@ -2,10 +2,6 @@ - com.apple.developer.applesignin - - Default - com.apple.security.app-sandbox com.apple.security.cs.allow-jit diff --git a/packages/firebase_auth/firebase_auth/example/macos/Runner/Release.entitlements b/packages/firebase_auth/firebase_auth/example/macos/Runner/Release.entitlements index cd2171f4278f..ee95ab7e582d 100644 --- a/packages/firebase_auth/firebase_auth/example/macos/Runner/Release.entitlements +++ b/packages/firebase_auth/firebase_auth/example/macos/Runner/Release.entitlements @@ -2,10 +2,6 @@ - com.apple.developer.applesignin - - Default - com.apple.security.app-sandbox com.apple.security.network.client diff --git a/packages/firebase_auth/firebase_auth/example/pubspec.yaml b/packages/firebase_auth/firebase_auth/example/pubspec.yaml index 18f5fdf378e1..a471e047c8ae 100644 --- a/packages/firebase_auth/firebase_auth/example/pubspec.yaml +++ b/packages/firebase_auth/firebase_auth/example/pubspec.yaml @@ -30,6 +30,9 @@ dev_dependencies: http: ^1.0.0 integration_test: sdk: flutter + # `integration_test/report_test_results.dart` names each executed test via + # `package:test_api/hooks.dart`; `any` defers to the version flutter_test pins. + test_api: any flutter: uses-material-design: true diff --git a/packages/firebase_auth/firebase_auth/example/test_driver/integration_test.dart b/packages/firebase_auth/firebase_auth/example/test_driver/integration_test.dart index f1ac26f27b88..691723cbbfe1 100644 --- a/packages/firebase_auth/firebase_auth/example/test_driver/integration_test.dart +++ b/packages/firebase_auth/firebase_auth/example/test_driver/integration_test.dart @@ -2,6 +2,33 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'dart:io'; + import 'package:integration_test/integration_test_driver.dart'; -Future main() => integrationDriver(); +Future main() => integrationDriver( + writeResponseOnFailure: true, + responseDataCallback: (Map? data) async { + final results = + (data?['testResults'] as Map?)?.cast() ?? + const {}; + var passed = 0; + var failed = 0; + for (final entry in results.entries) { + final ok = entry.value == 'success'; + ok ? passed++ : failed++; + // ignore: avoid_print + print('${ok ? '✅' : '❌'} ${entry.key}'); + } + // ignore: avoid_print + print('Web e2e summary: $passed passed, $failed failed, ' + '${results.length} total'); + if (results.isEmpty) { + // ignore: avoid_print + print('[E] No tests reported by the app - treating as ' + 'infrastructure failure.'); + exit(1); + } + await writeResponseData(data); + }, + ); diff --git a/packages/firebase_core/firebase_core/example/lib/firebase_options.dart b/packages/firebase_core/firebase_core/example/lib/firebase_options.dart new file mode 100644 index 000000000000..a5a1360f6620 --- /dev/null +++ b/packages/firebase_core/firebase_core/example/lib/firebase_options.dart @@ -0,0 +1,61 @@ +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// Generated for CI builds and emulator tests. These are not real credentials. +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + return switch (defaultTargetPlatform) { + TargetPlatform.android => android, + TargetPlatform.iOS => ios, + TargetPlatform.macOS => macos, + TargetPlatform.windows => windows, + _ => throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ), + }; + } + + static const web = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:web:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + authDomain: 'flutterfire-e2e-tests.firebaseapp.com', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const android = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:android:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const ios = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:ios:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const macos = ios; + + static const windows = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:web:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + authDomain: 'flutterfire-e2e-tests.firebaseapp.com', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); +} diff --git a/packages/firebase_core/firebase_core/example/macos/Podfile b/packages/firebase_core/firebase_core/example/macos/Podfile index 5bf4307c0570..08795ec4ab62 100644 --- a/packages/firebase_core/firebase_core/example/macos/Podfile +++ b/packages/firebase_core/firebase_core/example/macos/Podfile @@ -11,7 +11,7 @@ project 'Runner', { def parse_KV_file(file, separator='=') file_abs_path = File.expand_path(file) - if !File.exists? file_abs_path + if !File.exist? file_abs_path return []; end pods_ary = [] @@ -33,7 +33,7 @@ end def pubspec_supports_macos(file) file_abs_path = File.expand_path(file) - if !File.exists? file_abs_path + if !File.exist? file_abs_path return false; end File.foreach(file_abs_path) { |line| diff --git a/packages/firebase_crashlytics/firebase_crashlytics/example/android/app/build.gradle b/packages/firebase_crashlytics/firebase_crashlytics/example/android/app/build.gradle index dbca60bd988e..a5735eeb6351 100644 --- a/packages/firebase_crashlytics/firebase_crashlytics/example/android/app/build.gradle +++ b/packages/firebase_crashlytics/firebase_crashlytics/example/android/app/build.gradle @@ -43,7 +43,13 @@ android { } defaultConfig { - applicationId = "io.flutter.plugins.firebasecrashlyticsexample" + // Deliberately the mega test app's Firebase app identity: the live-service + // e2e suite in `integration_test/` talks to real Firebase backends, which + // only accept app ids registered in the `flutterfire-e2e-tests` project. + // `namespace` and the Kotlin package below are unaffected. Two of these + // examples cannot be co-installed on one device - CI installs one per + // emulator. + applicationId = "io.flutter.plugins.firebase.tests" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. minSdk = 23 diff --git a/packages/firebase_crashlytics/firebase_crashlytics/example/android/app/src/main/AndroidManifest.xml b/packages/firebase_crashlytics/firebase_crashlytics/example/android/app/src/main/AndroidManifest.xml index f8619527de06..5ae822d1212b 100644 --- a/packages/firebase_crashlytics/firebase_crashlytics/example/android/app/src/main/AndroidManifest.xml +++ b/packages/firebase_crashlytics/firebase_crashlytics/example/android/app/src/main/AndroidManifest.xml @@ -3,6 +3,13 @@ android:label="firebasecrashlyticsexample" android:name="${applicationName}" android:icon="@mipmap/ic_launcher"> + + []; + setUp(() { + executed.add(TestHandle.current.name); + }); + tearDownAll(() { + // `TestHandle.current.name` is the full name, prefixed with the enclosing + // group names, while `binding.results` is keyed by the bare `testWidgets` + // description - so match a failed entry on the suffix as well as on + // equality, otherwise no grouped test could ever be marked failed. + final failures = [ + for (final entry in binding.results.entries) + if (entry.value is Failure) entry.key, + ]; + bool didFail(String name) => failures + .any((failure) => name == failure || name.endsWith(' $failure')); + + binding.reportData ??= {}; + binding.reportData!['testResults'] = { + // Plain test() failures are not individually attributable here, but they + // fail the run as a whole via allTestsPassed; testWidgets failures are + // attributed from the binding's results map. + for (final name in executed) name: didFail(name) ? 'failed' : 'success', + }; + }); +} diff --git a/packages/firebase_crashlytics/firebase_crashlytics/example/ios/Runner.xcodeproj/project.pbxproj b/packages/firebase_crashlytics/firebase_crashlytics/example/ios/Runner.xcodeproj/project.pbxproj index d0afda96fb21..50c2c3a0176c 100644 --- a/packages/firebase_crashlytics/firebase_crashlytics/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/firebase_crashlytics/firebase_crashlytics/example/ios/Runner.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 4A1C7E2B2C90118400B7F3A1 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 864CBEC4F3EDA362F4B5B76D /* GoogleService-Info.plist */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; @@ -16,7 +17,6 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - AAE9D7BFA8AAA8783C2860B2 /* GoogleService-Info.plist in Sources */ = {isa = PBXBuildFile; fileRef = 864CBEC4F3EDA362F4B5B76D /* GoogleService-Info.plist */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -192,6 +192,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4A1C7E2B2C90118400B7F3A1 /* GoogleService-Info.plist in Resources */, 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, @@ -262,7 +263,6 @@ 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, 97C146F31CF9000F007C117D /* main.m in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - AAE9D7BFA8AAA8783C2860B2 /* GoogleService-Info.plist in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -356,7 +356,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.crashlytics.example; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests; PRODUCT_NAME = "$(TARGET_NAME)"; VERSIONING_SYSTEM = "apple-generic"; }; @@ -484,7 +484,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.crashlytics.example; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests; PRODUCT_NAME = "$(TARGET_NAME)"; VERSIONING_SYSTEM = "apple-generic"; }; @@ -511,7 +511,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.crashlytics.example; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests; PRODUCT_NAME = "$(TARGET_NAME)"; VERSIONING_SYSTEM = "apple-generic"; }; diff --git a/packages/firebase_crashlytics/firebase_crashlytics/example/lib/firebase_options.dart b/packages/firebase_crashlytics/firebase_crashlytics/example/lib/firebase_options.dart new file mode 100644 index 000000000000..a5a1360f6620 --- /dev/null +++ b/packages/firebase_crashlytics/firebase_crashlytics/example/lib/firebase_options.dart @@ -0,0 +1,61 @@ +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// Generated for CI builds and emulator tests. These are not real credentials. +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + return switch (defaultTargetPlatform) { + TargetPlatform.android => android, + TargetPlatform.iOS => ios, + TargetPlatform.macOS => macos, + TargetPlatform.windows => windows, + _ => throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ), + }; + } + + static const web = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:web:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + authDomain: 'flutterfire-e2e-tests.firebaseapp.com', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const android = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:android:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const ios = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:ios:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const macos = ios; + + static const windows = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:web:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + authDomain: 'flutterfire-e2e-tests.firebaseapp.com', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); +} diff --git a/packages/firebase_crashlytics/firebase_crashlytics/example/macos/Podfile b/packages/firebase_crashlytics/firebase_crashlytics/example/macos/Podfile index fe733905db65..9ec46f8cd53c 100644 --- a/packages/firebase_crashlytics/firebase_crashlytics/example/macos/Podfile +++ b/packages/firebase_crashlytics/firebase_crashlytics/example/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.13' +platform :osx, '10.15' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/packages/firebase_crashlytics/firebase_crashlytics/example/macos/Runner.xcodeproj/project.pbxproj b/packages/firebase_crashlytics/firebase_crashlytics/example/macos/Runner.xcodeproj/project.pbxproj index 5645f070c799..fa69b1e7014c 100644 --- a/packages/firebase_crashlytics/firebase_crashlytics/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/firebase_crashlytics/firebase_crashlytics/example/macos/Runner.xcodeproj/project.pbxproj @@ -473,7 +473,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.crashlytics.example; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; @@ -606,7 +606,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.crashlytics.example; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -633,7 +633,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.crashlytics.example; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; diff --git a/packages/firebase_crashlytics/firebase_crashlytics/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/firebase_crashlytics/firebase_crashlytics/example/macos/Runner/Configs/AppInfo.xcconfig index 0fd315dfc2fc..447e00741817 100644 --- a/packages/firebase_crashlytics/firebase_crashlytics/example/macos/Runner/Configs/AppInfo.xcconfig +++ b/packages/firebase_crashlytics/firebase_crashlytics/example/macos/Runner/Configs/AppInfo.xcconfig @@ -8,7 +8,7 @@ PRODUCT_NAME = example // The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.crashlytics.example +PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests // The copyright displayed in application information PRODUCT_COPYRIGHT = Copyright © 2020 io.flutter.plugins.firebase.crashlytics. All rights reserved. diff --git a/packages/firebase_crashlytics/firebase_crashlytics/example/pubspec.yaml b/packages/firebase_crashlytics/firebase_crashlytics/example/pubspec.yaml index 389347f48d6e..e090bb2a247e 100644 --- a/packages/firebase_crashlytics/firebase_crashlytics/example/pubspec.yaml +++ b/packages/firebase_crashlytics/firebase_crashlytics/example/pubspec.yaml @@ -13,5 +13,14 @@ dependencies: flutter: sdk: flutter +dev_dependencies: + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + # `integration_test/report_test_results.dart` names each executed test via + # `package:test_api/hooks.dart`; `any` defers to the version flutter_test pins. + test_api: any + flutter: uses-material-design: true diff --git a/packages/firebase_crashlytics/firebase_crashlytics/example/test_driver/integration_test.dart b/packages/firebase_crashlytics/firebase_crashlytics/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..691723cbbfe1 --- /dev/null +++ b/packages/firebase_crashlytics/firebase_crashlytics/example/test_driver/integration_test.dart @@ -0,0 +1,34 @@ +// Copyright 2022, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io'; + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver( + writeResponseOnFailure: true, + responseDataCallback: (Map? data) async { + final results = + (data?['testResults'] as Map?)?.cast() ?? + const {}; + var passed = 0; + var failed = 0; + for (final entry in results.entries) { + final ok = entry.value == 'success'; + ok ? passed++ : failed++; + // ignore: avoid_print + print('${ok ? '✅' : '❌'} ${entry.key}'); + } + // ignore: avoid_print + print('Web e2e summary: $passed passed, $failed failed, ' + '${results.length} total'); + if (results.isEmpty) { + // ignore: avoid_print + print('[E] No tests reported by the app - treating as ' + 'infrastructure failure.'); + exit(1); + } + await writeResponseData(data); + }, + ); diff --git a/packages/firebase_data_connect/firebase_data_connect/example/integration_test/e2e_test.dart b/packages/firebase_data_connect/firebase_data_connect/example/integration_test/e2e_test.dart index 14feb969aa95..4e99c608a398 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/integration_test/e2e_test.dart +++ b/packages/firebase_data_connect/firebase_data_connect/example/integration_test/e2e_test.dart @@ -15,6 +15,7 @@ import 'generation_e2e.dart'; import 'instance_e2e.dart'; import 'listen_e2e.dart'; import 'query_e2e.dart'; +import 'report_test_results.dart'; import 'websocket_e2e.dart'; Future _signInTestUser() async { @@ -48,13 +49,25 @@ Future _signInTestUser() async { } void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + reportTestResultsToDriver(binding); group('firebase_data_connect', () { setUpAll(() async { - await Firebase.initializeApp( - options: DefaultFirebaseOptions.currentPlatform, - ); + // The native SDK may already have configured [DEFAULT] from a bundled + // GoogleService-Info.plist (the plugin registrant does this before any + // Dart runs). Dart's Firebase.apps cannot see that app until the first + // platform-channel call, so the only reliable guard is catching the + // duplicate-app error and keeping the natively configured instance. + try { + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + } on FirebaseException catch (e) { + if (e.code != 'duplicate-app') { + rethrow; + } + } final connector = MoviesConnector.connectorConfig; diff --git a/packages/firebase_data_connect/firebase_data_connect/example/integration_test/report_test_results.dart b/packages/firebase_data_connect/firebase_data_connect/example/integration_test/report_test_results.dart new file mode 100644 index 000000000000..f08ddf1af020 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/integration_test/report_test_results.dart @@ -0,0 +1,40 @@ +// Copyright 2020, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/common.dart' show Failure; +import 'package:integration_test/integration_test.dart'; +import 'package:test_api/hooks.dart' show TestHandle; + +/// Records every executed test via the package:test lifecycle (plain `test()` +/// declarations never reach the binding's `results` map - only `testWidgets` +/// does) and publishes them through +/// [IntegrationTestWidgetsFlutterBinding.reportData], the only channel that +/// crosses the wire on web. +void reportTestResultsToDriver(IntegrationTestWidgetsFlutterBinding binding) { + final executed = []; + setUp(() { + executed.add(TestHandle.current.name); + }); + tearDownAll(() { + // `TestHandle.current.name` is the full name, prefixed with the enclosing + // group names, while `binding.results` is keyed by the bare `testWidgets` + // description - so match a failed entry on the suffix as well as on + // equality, otherwise no grouped test could ever be marked failed. + final failures = [ + for (final entry in binding.results.entries) + if (entry.value is Failure) entry.key, + ]; + bool didFail(String name) => failures + .any((failure) => name == failure || name.endsWith(' $failure')); + + binding.reportData ??= {}; + binding.reportData!['testResults'] = { + // Plain test() failures are not individually attributable here, but they + // fail the run as a whole via allTestsPassed; testWidgets failures are + // attributed from the binding's results map. + for (final name in executed) name: didFail(name) ? 'failed' : 'success', + }; + }); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/lib/firebase_options.dart b/packages/firebase_data_connect/firebase_data_connect/example/lib/firebase_options.dart new file mode 100644 index 000000000000..a5a1360f6620 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/lib/firebase_options.dart @@ -0,0 +1,61 @@ +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// Generated for CI builds and emulator tests. These are not real credentials. +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + return switch (defaultTargetPlatform) { + TargetPlatform.android => android, + TargetPlatform.iOS => ios, + TargetPlatform.macOS => macos, + TargetPlatform.windows => windows, + _ => throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ), + }; + } + + static const web = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:web:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + authDomain: 'flutterfire-e2e-tests.firebaseapp.com', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const android = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:android:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const ios = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:ios:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const macos = ios; + + static const windows = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:web:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + authDomain: 'flutterfire-e2e-tests.firebaseapp.com', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/pubspec.yaml b/packages/firebase_data_connect/firebase_data_connect/example/pubspec.yaml index fdd7d1e1cdcf..65a6bbe06abe 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/pubspec.yaml +++ b/packages/firebase_data_connect/firebase_data_connect/example/pubspec.yaml @@ -33,6 +33,9 @@ dev_dependencies: flutter_lints: ^6.0.0 integration_test: sdk: flutter + # `integration_test/report_test_results.dart` names each executed test via + # `package:test_api/hooks.dart`; `any` defers to the version flutter_test pins. + test_api: any flutter: uses-material-design: true diff --git a/packages/firebase_data_connect/firebase_data_connect/example/test_driver/integration_test.dart b/packages/firebase_data_connect/firebase_data_connect/example/test_driver/integration_test.dart index f1ac26f27b88..691723cbbfe1 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/test_driver/integration_test.dart +++ b/packages/firebase_data_connect/firebase_data_connect/example/test_driver/integration_test.dart @@ -2,6 +2,33 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'dart:io'; + import 'package:integration_test/integration_test_driver.dart'; -Future main() => integrationDriver(); +Future main() => integrationDriver( + writeResponseOnFailure: true, + responseDataCallback: (Map? data) async { + final results = + (data?['testResults'] as Map?)?.cast() ?? + const {}; + var passed = 0; + var failed = 0; + for (final entry in results.entries) { + final ok = entry.value == 'success'; + ok ? passed++ : failed++; + // ignore: avoid_print + print('${ok ? '✅' : '❌'} ${entry.key}'); + } + // ignore: avoid_print + print('Web e2e summary: $passed passed, $failed failed, ' + '${results.length} total'); + if (results.isEmpty) { + // ignore: avoid_print + print('[E] No tests reported by the app - treating as ' + 'infrastructure failure.'); + exit(1); + } + await writeResponseData(data); + }, + ); diff --git a/packages/firebase_database/firebase_database/example/android/app/google-services.json b/packages/firebase_database/firebase_database/example/android/app/google-services.json new file mode 100644 index 000000000000..cafe0c80ffc9 --- /dev/null +++ b/packages/firebase_database/firebase_database/example/android/app/google-services.json @@ -0,0 +1,24 @@ +{ + "project_info": { + "project_number": "123456789012", + "project_id": "flutterfire-e2e-tests", + "storage_bucket": "flutterfire-e2e-tests.appspot.com", + "firebase_url": "https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:123456789012:android:0000000000000000000000", + "android_client_info": { + "package_name": "io.flutter.plugins.firebase.database.example" + } + }, + "api_key": [ + { + "current_key": "AIzaSyDUMMYKEYFORFLUTTERFIRECITESTS0000" + } + ] + } + ], + "configuration_version": "1" +} diff --git a/packages/firebase_database/firebase_database/example/android/app/src/main/AndroidManifest.xml b/packages/firebase_database/firebase_database/example/android/app/src/main/AndroidManifest.xml index 74a78b939e5e..637f57c840d1 100644 --- a/packages/firebase_database/firebase_database/example/android/app/src/main/AndroidManifest.xml +++ b/packages/firebase_database/firebase_database/example/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ diff --git a/packages/firebase_database/firebase_database/example/integration_test/e2e_test.dart b/packages/firebase_database/firebase_database/example/integration_test/e2e_test.dart index 331415d6dbf8..c4686ec807bd 100644 --- a/packages/firebase_database/firebase_database/example/integration_test/e2e_test.dart +++ b/packages/firebase_database/firebase_database/example/integration_test/e2e_test.dart @@ -11,6 +11,7 @@ import 'package:firebase_database_example/firebase_options.dart'; import 'data_snapshot_e2e.dart'; import 'database_e2e.dart'; import 'database_reference_e2e.dart'; +import 'report_test_results.dart'; import 'web_only_stub.dart' if (dart.library.js_interop) 'web_only.dart'; import 'firebase_database_configuration_e2e.dart'; import 'query_e2e.dart'; @@ -26,13 +27,25 @@ const emulatorPort = 9000; const emulatorHost = 'localhost'; void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + reportTestResultsToDriver(binding); group('firebase_database', () { setUpAll(() async { - await Firebase.initializeApp( - options: DefaultFirebaseOptions.currentPlatform, - ); + // The native SDK may already have configured [DEFAULT] from a bundled + // GoogleService-Info.plist (the plugin registrant does this before any + // Dart runs). Dart's Firebase.apps cannot see that app until the first + // platform-channel call, so the only reliable guard is catching the + // duplicate-app error and keeping the natively configured instance. + try { + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + } on FirebaseException catch (e) { + if (e.code != 'duplicate-app') { + rethrow; + } + } database = FirebaseDatabase.instance; database.useDatabaseEmulator(emulatorHost, emulatorPort); await database.goOnline(); diff --git a/packages/firebase_database/firebase_database/example/integration_test/report_test_results.dart b/packages/firebase_database/firebase_database/example/integration_test/report_test_results.dart new file mode 100644 index 000000000000..fb80e3ba19f7 --- /dev/null +++ b/packages/firebase_database/firebase_database/example/integration_test/report_test_results.dart @@ -0,0 +1,40 @@ +// Copyright 2022, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/common.dart' show Failure; +import 'package:integration_test/integration_test.dart'; +import 'package:test_api/hooks.dart' show TestHandle; + +/// Records every executed test via the package:test lifecycle (plain `test()` +/// declarations never reach the binding's `results` map - only `testWidgets` +/// does) and publishes them through +/// [IntegrationTestWidgetsFlutterBinding.reportData], the only channel that +/// crosses the wire on web. +void reportTestResultsToDriver(IntegrationTestWidgetsFlutterBinding binding) { + final executed = []; + setUp(() { + executed.add(TestHandle.current.name); + }); + tearDownAll(() { + // `TestHandle.current.name` is the full name, prefixed with the enclosing + // group names, while `binding.results` is keyed by the bare `testWidgets` + // description - so match a failed entry on the suffix as well as on + // equality, otherwise no grouped test could ever be marked failed. + final failures = [ + for (final entry in binding.results.entries) + if (entry.value is Failure) entry.key, + ]; + bool didFail(String name) => failures + .any((failure) => name == failure || name.endsWith(' $failure')); + + binding.reportData ??= {}; + binding.reportData!['testResults'] = { + // Plain test() failures are not individually attributable here, but they + // fail the run as a whole via allTestsPassed; testWidgets failures are + // attributed from the binding's results map. + for (final name in executed) name: didFail(name) ? 'failed' : 'success', + }; + }); +} diff --git a/packages/firebase_database/firebase_database/example/ios/Runner/GoogleService-Info.plist b/packages/firebase_database/firebase_database/example/ios/Runner/GoogleService-Info.plist new file mode 100644 index 000000000000..d5b86a6b289e --- /dev/null +++ b/packages/firebase_database/firebase_database/example/ios/Runner/GoogleService-Info.plist @@ -0,0 +1,22 @@ + + + + + API_KEY + AIzaSyDUMMYKEYFORFLUTTERFIRECITESTS0000 + GCM_SENDER_ID + 123456789012 + PLIST_VERSION + 1 + BUNDLE_ID + io.flutter.plugins.firebase.database.example + PROJECT_ID + flutterfire-e2e-tests + GOOGLE_APP_ID + 1:123456789012:ios:0000000000000000000000 + STORAGE_BUCKET + flutterfire-e2e-tests.appspot.com + DATABASE_URL + https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app + + diff --git a/packages/firebase_database/firebase_database/example/lib/firebase_options.dart b/packages/firebase_database/firebase_database/example/lib/firebase_options.dart new file mode 100644 index 000000000000..a5a1360f6620 --- /dev/null +++ b/packages/firebase_database/firebase_database/example/lib/firebase_options.dart @@ -0,0 +1,61 @@ +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// Generated for CI builds and emulator tests. These are not real credentials. +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + return switch (defaultTargetPlatform) { + TargetPlatform.android => android, + TargetPlatform.iOS => ios, + TargetPlatform.macOS => macos, + TargetPlatform.windows => windows, + _ => throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ), + }; + } + + static const web = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:web:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + authDomain: 'flutterfire-e2e-tests.firebaseapp.com', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const android = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:android:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const ios = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:ios:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const macos = ios; + + static const windows = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:web:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + authDomain: 'flutterfire-e2e-tests.firebaseapp.com', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); +} diff --git a/packages/firebase_database/firebase_database/example/macos/Runner/GoogleService-Info.plist b/packages/firebase_database/firebase_database/example/macos/Runner/GoogleService-Info.plist new file mode 100644 index 000000000000..ce843cc0c85d --- /dev/null +++ b/packages/firebase_database/firebase_database/example/macos/Runner/GoogleService-Info.plist @@ -0,0 +1,22 @@ + + + + + API_KEY + AIzaSyDUMMYKEYFORFLUTTERFIRECITESTS0000 + GCM_SENDER_ID + 123456789012 + PLIST_VERSION + 1 + BUNDLE_ID + io.flutter.plugins.firebaseDatabaseExample + PROJECT_ID + flutterfire-e2e-tests + GOOGLE_APP_ID + 1:123456789012:ios:0000000000000000000000 + STORAGE_BUCKET + flutterfire-e2e-tests.appspot.com + DATABASE_URL + https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app + + diff --git a/packages/firebase_database/firebase_database/example/pubspec.yaml b/packages/firebase_database/firebase_database/example/pubspec.yaml index 49845c49c9e0..e75fd7831bf4 100755 --- a/packages/firebase_database/firebase_database/example/pubspec.yaml +++ b/packages/firebase_database/firebase_database/example/pubspec.yaml @@ -25,6 +25,9 @@ dev_dependencies: sdk: flutter integration_test: sdk: flutter + # `integration_test/report_test_results.dart` names each executed test via + # `package:test_api/hooks.dart`; `any` defers to the version flutter_test pins. + test_api: any flutter: uses-material-design: true diff --git a/packages/firebase_database/firebase_database/example/test_driver/integration_test.dart b/packages/firebase_database/firebase_database/example/test_driver/integration_test.dart index f1ac26f27b88..691723cbbfe1 100644 --- a/packages/firebase_database/firebase_database/example/test_driver/integration_test.dart +++ b/packages/firebase_database/firebase_database/example/test_driver/integration_test.dart @@ -2,6 +2,33 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'dart:io'; + import 'package:integration_test/integration_test_driver.dart'; -Future main() => integrationDriver(); +Future main() => integrationDriver( + writeResponseOnFailure: true, + responseDataCallback: (Map? data) async { + final results = + (data?['testResults'] as Map?)?.cast() ?? + const {}; + var passed = 0; + var failed = 0; + for (final entry in results.entries) { + final ok = entry.value == 'success'; + ok ? passed++ : failed++; + // ignore: avoid_print + print('${ok ? '✅' : '❌'} ${entry.key}'); + } + // ignore: avoid_print + print('Web e2e summary: $passed passed, $failed failed, ' + '${results.length} total'); + if (results.isEmpty) { + // ignore: avoid_print + print('[E] No tests reported by the app - treating as ' + 'infrastructure failure.'); + exit(1); + } + await writeResponseData(data); + }, + ); diff --git a/packages/firebase_in_app_messaging/firebase_in_app_messaging/example/lib/firebase_options.dart b/packages/firebase_in_app_messaging/firebase_in_app_messaging/example/lib/firebase_options.dart new file mode 100644 index 000000000000..a5a1360f6620 --- /dev/null +++ b/packages/firebase_in_app_messaging/firebase_in_app_messaging/example/lib/firebase_options.dart @@ -0,0 +1,61 @@ +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// Generated for CI builds and emulator tests. These are not real credentials. +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + return switch (defaultTargetPlatform) { + TargetPlatform.android => android, + TargetPlatform.iOS => ios, + TargetPlatform.macOS => macos, + TargetPlatform.windows => windows, + _ => throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ), + }; + } + + static const web = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:web:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + authDomain: 'flutterfire-e2e-tests.firebaseapp.com', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const android = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:android:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const ios = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:ios:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const macos = ios; + + static const windows = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:web:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + authDomain: 'flutterfire-e2e-tests.firebaseapp.com', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); +} diff --git a/packages/firebase_messaging/firebase_messaging/example/android/app/build.gradle b/packages/firebase_messaging/firebase_messaging/example/android/app/build.gradle index 8ebdee7a1533..09bca843079a 100644 --- a/packages/firebase_messaging/firebase_messaging/example/android/app/build.gradle +++ b/packages/firebase_messaging/firebase_messaging/example/android/app/build.gradle @@ -44,7 +44,13 @@ android { } defaultConfig { - applicationId = "io.flutter.plugins.firebase.messaging.example" + // Deliberately the mega test app's Firebase app identity: the live-service + // e2e suite in `integration_test/` talks to real Firebase backends, which + // only accept app ids registered in the `flutterfire-e2e-tests` project. + // `namespace` and the Kotlin package below are unaffected. Two of these + // examples cannot be co-installed on one device - CI installs one per + // emulator. + applicationId = "io.flutter.plugins.firebase.tests" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. minSdk = 23 @@ -63,7 +69,10 @@ android { } dependencies { - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' + // flutter_local_notifications (a dependency of this example, but not of the + // `tests` mega-app this suite was migrated out of) publishes AAR metadata + // requiring desugar_jdk_libs 2.1.4+; 2.0.4 fails :app:checkDebugAarMetadata. + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4' } flutter { diff --git a/tests/integration_test/firebase_messaging/firebase_messaging_e2e_test.dart b/packages/firebase_messaging/firebase_messaging/example/integration_test/e2e_test.dart similarity index 96% rename from tests/integration_test/firebase_messaging/firebase_messaging_e2e_test.dart rename to packages/firebase_messaging/firebase_messaging/example/integration_test/e2e_test.dart index c3710988aa5b..85f37fbd5f95 100644 --- a/tests/integration_test/firebase_messaging/firebase_messaging_e2e_test.dart +++ b/packages/firebase_messaging/firebase_messaging/example/integration_test/e2e_test.dart @@ -9,13 +9,16 @@ import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'package:tests/firebase_options.dart'; +import 'package:firebase_messaging_example/firebase_options.dart'; + +import 'report_test_results.dart'; // ignore: do_not_use_environment const bool skipTestsOnCI = bool.fromEnvironment('CI'); void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + reportTestResultsToDriver(binding); group( 'firebase_messaging', diff --git a/packages/firebase_messaging/firebase_messaging/example/integration_test/report_test_results.dart b/packages/firebase_messaging/firebase_messaging/example/integration_test/report_test_results.dart new file mode 100644 index 000000000000..038d20c39931 --- /dev/null +++ b/packages/firebase_messaging/firebase_messaging/example/integration_test/report_test_results.dart @@ -0,0 +1,40 @@ +// Copyright 2019, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/common.dart' show Failure; +import 'package:integration_test/integration_test.dart'; +import 'package:test_api/hooks.dart' show TestHandle; + +/// Records every executed test via the package:test lifecycle (plain `test()` +/// declarations never reach the binding's `results` map - only `testWidgets` +/// does) and publishes them through +/// [IntegrationTestWidgetsFlutterBinding.reportData], the only channel that +/// crosses the wire on web. +void reportTestResultsToDriver(IntegrationTestWidgetsFlutterBinding binding) { + final executed = []; + setUp(() { + executed.add(TestHandle.current.name); + }); + tearDownAll(() { + // `TestHandle.current.name` is the full name, prefixed with the enclosing + // group names, while `binding.results` is keyed by the bare `testWidgets` + // description - so match a failed entry on the suffix as well as on + // equality, otherwise no grouped test could ever be marked failed. + final failures = [ + for (final entry in binding.results.entries) + if (entry.value is Failure) entry.key, + ]; + bool didFail(String name) => failures + .any((failure) => name == failure || name.endsWith(' $failure')); + + binding.reportData ??= {}; + binding.reportData!['testResults'] = { + // Plain test() failures are not individually attributable here, but they + // fail the run as a whole via allTestsPassed; testWidgets failures are + // attributed from the binding's results map. + for (final name in executed) name: didFail(name) ? 'failed' : 'success', + }; + }); +} diff --git a/packages/firebase_messaging/firebase_messaging/example/ios/Flutter/AppFrameworkInfo.plist b/packages/firebase_messaging/firebase_messaging/example/ios/Flutter/AppFrameworkInfo.plist index 8c6e56146e23..ab8e063fe872 100644 --- a/packages/firebase_messaging/firebase_messaging/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/firebase_messaging/firebase_messaging/example/ios/Flutter/AppFrameworkInfo.plist @@ -20,7 +20,5 @@ ???? CFBundleVersion 1.0 - MinimumOSVersion - 12.0 diff --git a/packages/firebase_messaging/firebase_messaging/example/ios/Flutter/Debug.xcconfig b/packages/firebase_messaging/firebase_messaging/example/ios/Flutter/Debug.xcconfig index e8efba114687..ec97fc6f3021 100644 --- a/packages/firebase_messaging/firebase_messaging/example/ios/Flutter/Debug.xcconfig +++ b/packages/firebase_messaging/firebase_messaging/example/ios/Flutter/Debug.xcconfig @@ -1,2 +1,2 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/packages/firebase_messaging/firebase_messaging/example/ios/Flutter/Release.xcconfig b/packages/firebase_messaging/firebase_messaging/example/ios/Flutter/Release.xcconfig index 399e9340e6f6..c4855bfe2000 100644 --- a/packages/firebase_messaging/firebase_messaging/example/ios/Flutter/Release.xcconfig +++ b/packages/firebase_messaging/firebase_messaging/example/ios/Flutter/Release.xcconfig @@ -1,2 +1,2 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/packages/firebase_messaging/firebase_messaging/example/ios/Runner.xcodeproj/project.pbxproj b/packages/firebase_messaging/firebase_messaging/example/ios/Runner.xcodeproj/project.pbxproj index a25173e1f7cf..c347267c9d02 100644 --- a/packages/firebase_messaging/firebase_messaging/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/firebase_messaging/firebase_messaging/example/ios/Runner.xcodeproj/project.pbxproj @@ -402,8 +402,8 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.messaging; - PRODUCT_NAME = "Firebase Cloud Messaging Example"; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests; + PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; VERSIONING_SYSTEM = "apple-generic"; }; @@ -541,8 +541,8 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.messaging; - PRODUCT_NAME = "Firebase Cloud Messaging Example"; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests; + PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; VERSIONING_SYSTEM = "apple-generic"; }; @@ -573,8 +573,8 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.messaging; - PRODUCT_NAME = "Firebase Cloud Messaging Example"; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests; + PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; VERSIONING_SYSTEM = "apple-generic"; }; diff --git a/packages/firebase_messaging/firebase_messaging/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/firebase_messaging/firebase_messaging/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 20582f866aa0..798710d49f7b 100644 --- a/packages/firebase_messaging/firebase_messaging/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/firebase_messaging/firebase_messaging/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -44,6 +44,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" shouldUseLaunchSchemeArgsEnv = "YES"> diff --git a/packages/firebase_messaging/firebase_messaging/example/ios/Runner/AppDelegate.m b/packages/firebase_messaging/firebase_messaging/example/ios/Runner/AppDelegate.m index 21cf90bc85af..5bb8da1edf5c 100644 --- a/packages/firebase_messaging/firebase_messaging/example/ios/Runner/AppDelegate.m +++ b/packages/firebase_messaging/firebase_messaging/example/ios/Runner/AppDelegate.m @@ -1,7 +1,17 @@ #import "AppDelegate.h" -#import #import "GeneratedPluginRegistrant.h" +// `` is the CocoaPods header layout. Swift Package +// Manager does not reproduce it - the plugin's public headers are only +// reachable through its generated Clang module - so the iOS SPM job cannot +// build this file unless both spellings are tried. The plugin's own headers +// use the same `__has_include` dance for `firebase_core`. +#if __has_include() +#import +#else +@import firebase_messaging; +#endif + @implementation AppDelegate - (BOOL)application:(UIApplication *)application diff --git a/packages/firebase_messaging/firebase_messaging/example/lib/firebase_options.dart b/packages/firebase_messaging/firebase_messaging/example/lib/firebase_options.dart new file mode 100644 index 000000000000..a5a1360f6620 --- /dev/null +++ b/packages/firebase_messaging/firebase_messaging/example/lib/firebase_options.dart @@ -0,0 +1,61 @@ +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// Generated for CI builds and emulator tests. These are not real credentials. +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + return switch (defaultTargetPlatform) { + TargetPlatform.android => android, + TargetPlatform.iOS => ios, + TargetPlatform.macOS => macos, + TargetPlatform.windows => windows, + _ => throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ), + }; + } + + static const web = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:web:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + authDomain: 'flutterfire-e2e-tests.firebaseapp.com', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const android = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:android:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const ios = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:ios:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const macos = ios; + + static const windows = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:web:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + authDomain: 'flutterfire-e2e-tests.firebaseapp.com', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); +} diff --git a/packages/firebase_messaging/firebase_messaging/example/macos/Podfile b/packages/firebase_messaging/firebase_messaging/example/macos/Podfile index 049abe295427..9ec46f8cd53c 100644 --- a/packages/firebase_messaging/firebase_messaging/example/macos/Podfile +++ b/packages/firebase_messaging/firebase_messaging/example/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.14' +platform :osx, '10.15' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/packages/firebase_messaging/firebase_messaging/example/macos/Runner.xcodeproj/project.pbxproj b/packages/firebase_messaging/firebase_messaging/example/macos/Runner.xcodeproj/project.pbxproj index f8fbe2f366a4..1318dad059ff 100644 --- a/packages/firebase_messaging/firebase_messaging/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/firebase_messaging/firebase_messaging/example/macos/Runner.xcodeproj/project.pbxproj @@ -434,11 +434,8 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = YMA4Y8JWM2; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter/ephemeral", @@ -448,8 +445,8 @@ "$(inherited)", "@executable_path/../Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.messaging; - PRODUCT_NAME = "Firebase Cloud Messaging Example"; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests; + PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; @@ -569,11 +566,8 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = YYX2P3XVJ7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter/ephemeral", @@ -583,8 +577,8 @@ "$(inherited)", "@executable_path/../Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.messaging; - PRODUCT_NAME = "Firebase Cloud Messaging Example"; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests; + PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -598,11 +592,8 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = ""; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter/ephemeral", @@ -612,8 +603,8 @@ "$(inherited)", "@executable_path/../Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.messaging; - PRODUCT_NAME = "Firebase Cloud Messaging Example"; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests; + PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; diff --git a/packages/firebase_messaging/firebase_messaging/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/firebase_messaging/firebase_messaging/example/macos/Runner/Configs/AppInfo.xcconfig index f653d20264ba..37f981d127bd 100644 --- a/packages/firebase_messaging/firebase_messaging/example/macos/Runner/Configs/AppInfo.xcconfig +++ b/packages/firebase_messaging/firebase_messaging/example/macos/Runner/Configs/AppInfo.xcconfig @@ -8,7 +8,7 @@ PRODUCT_NAME = example // The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.example +PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests // The copyright displayed in application information PRODUCT_COPYRIGHT = Copyright © 2020 io.flutter.plugins. All rights reserved. diff --git a/packages/firebase_messaging/firebase_messaging/example/macos/Runner/DebugProfile.entitlements b/packages/firebase_messaging/firebase_messaging/example/macos/Runner/DebugProfile.entitlements index b76b509eebbf..3ba6c1266f21 100644 --- a/packages/firebase_messaging/firebase_messaging/example/macos/Runner/DebugProfile.entitlements +++ b/packages/firebase_messaging/firebase_messaging/example/macos/Runner/DebugProfile.entitlements @@ -2,8 +2,6 @@ - com.apple.developer.aps-environment - development com.apple.security.app-sandbox com.apple.security.cs.allow-jit diff --git a/packages/firebase_messaging/firebase_messaging/example/pubspec.yaml b/packages/firebase_messaging/firebase_messaging/example/pubspec.yaml index 07c4289c74d3..d821baea7e20 100644 --- a/packages/firebase_messaging/firebase_messaging/example/pubspec.yaml +++ b/packages/firebase_messaging/firebase_messaging/example/pubspec.yaml @@ -14,5 +14,14 @@ dependencies: flutter_local_notifications: ^21.0.0 http: ^1.0.0 +dev_dependencies: + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + # `integration_test/report_test_results.dart` names each executed test via + # `package:test_api/hooks.dart`; `any` defers to the version flutter_test pins. + test_api: any + flutter: uses-material-design: true diff --git a/packages/firebase_messaging/firebase_messaging/example/test_driver/integration_test.dart b/packages/firebase_messaging/firebase_messaging/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..691723cbbfe1 --- /dev/null +++ b/packages/firebase_messaging/firebase_messaging/example/test_driver/integration_test.dart @@ -0,0 +1,34 @@ +// Copyright 2022, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io'; + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver( + writeResponseOnFailure: true, + responseDataCallback: (Map? data) async { + final results = + (data?['testResults'] as Map?)?.cast() ?? + const {}; + var passed = 0; + var failed = 0; + for (final entry in results.entries) { + final ok = entry.value == 'success'; + ok ? passed++ : failed++; + // ignore: avoid_print + print('${ok ? '✅' : '❌'} ${entry.key}'); + } + // ignore: avoid_print + print('Web e2e summary: $passed passed, $failed failed, ' + '${results.length} total'); + if (results.isEmpty) { + // ignore: avoid_print + print('[E] No tests reported by the app - treating as ' + 'infrastructure failure.'); + exit(1); + } + await writeResponseData(data); + }, + ); diff --git a/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/android/app/build.gradle b/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/android/app/build.gradle index 68bd6af4eacb..135becc4c974 100644 --- a/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/android/app/build.gradle +++ b/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/android/app/build.gradle @@ -42,6 +42,12 @@ android { } defaultConfig { + // Deliberately the mega test app's Firebase app identity: the live-service + // e2e suite in `integration_test/` talks to real Firebase backends, which + // only accept app ids registered in the `flutterfire-e2e-tests` project. + // `namespace` and the Kotlin package below are unaffected. Two of these + // examples cannot be co-installed on one device - CI installs one per + // emulator. applicationId = "io.flutter.plugins.firebase.tests" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. diff --git a/tests/integration_test/firebase_ml_model_downloader/firebase_ml_model_downloader_e2e_test.dart b/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/integration_test/e2e_test.dart similarity index 54% rename from tests/integration_test/firebase_ml_model_downloader/firebase_ml_model_downloader_e2e_test.dart rename to packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/integration_test/e2e_test.dart index d5347bfd17f8..01fad2dd0de3 100644 --- a/tests/integration_test/firebase_ml_model_downloader/firebase_ml_model_downloader_e2e_test.dart +++ b/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/integration_test/e2e_test.dart @@ -10,18 +10,32 @@ import 'package:firebase_ml_model_downloader/firebase_ml_model_downloader.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'package:tests/firebase_options.dart'; +import 'package:firebase_ml_model_downloader_example/firebase_options.dart'; + +import 'report_test_results.dart'; void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + reportTestResultsToDriver(binding); group( 'firebase_ml_model_downloader', () { setUpAll(() async { - await Firebase.initializeApp( - options: DefaultFirebaseOptions.currentPlatform, - ); + // The native SDK may already have configured [DEFAULT] from a bundled + // GoogleService-Info.plist (the plugin registrant does this before any + // Dart runs). Dart's Firebase.apps cannot see that app until the first + // platform-channel call, so the only reliable guard is catching the + // duplicate-app error and keeping the natively configured instance. + try { + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + } on FirebaseException catch (e) { + if (e.code != 'duplicate-app') { + rethrow; + } + } }); group('listDownloadedModels', () { diff --git a/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/integration_test/report_test_results.dart b/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/integration_test/report_test_results.dart new file mode 100644 index 000000000000..fb80e3ba19f7 --- /dev/null +++ b/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/integration_test/report_test_results.dart @@ -0,0 +1,40 @@ +// Copyright 2022, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/common.dart' show Failure; +import 'package:integration_test/integration_test.dart'; +import 'package:test_api/hooks.dart' show TestHandle; + +/// Records every executed test via the package:test lifecycle (plain `test()` +/// declarations never reach the binding's `results` map - only `testWidgets` +/// does) and publishes them through +/// [IntegrationTestWidgetsFlutterBinding.reportData], the only channel that +/// crosses the wire on web. +void reportTestResultsToDriver(IntegrationTestWidgetsFlutterBinding binding) { + final executed = []; + setUp(() { + executed.add(TestHandle.current.name); + }); + tearDownAll(() { + // `TestHandle.current.name` is the full name, prefixed with the enclosing + // group names, while `binding.results` is keyed by the bare `testWidgets` + // description - so match a failed entry on the suffix as well as on + // equality, otherwise no grouped test could ever be marked failed. + final failures = [ + for (final entry in binding.results.entries) + if (entry.value is Failure) entry.key, + ]; + bool didFail(String name) => failures + .any((failure) => name == failure || name.endsWith(' $failure')); + + binding.reportData ??= {}; + binding.reportData!['testResults'] = { + // Plain test() failures are not individually attributable here, but they + // fail the run as a whole via allTestsPassed; testWidgets failures are + // attributed from the binding's results map. + for (final name in executed) name: didFail(name) ? 'failed' : 'success', + }; + }); +} diff --git a/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/ios/Runner.xcodeproj/project.pbxproj b/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/ios/Runner.xcodeproj/project.pbxproj index 9b9c77e9d671..8d3d70b03a06 100644 --- a/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/ios/Runner.xcodeproj/project.pbxproj @@ -347,7 +347,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; "LD_RUNPATH_SEARCH_PATHS[arch=*]" = /usr/lib/swift; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; @@ -424,7 +424,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; "LD_RUNPATH_SEARCH_PATHS[arch=*]" = /usr/lib/swift; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; @@ -474,7 +474,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; "LD_RUNPATH_SEARCH_PATHS[arch=*]" = /usr/lib/swift; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; diff --git a/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/lib/firebase_options.dart b/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/lib/firebase_options.dart new file mode 100644 index 000000000000..a5a1360f6620 --- /dev/null +++ b/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/lib/firebase_options.dart @@ -0,0 +1,61 @@ +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// Generated for CI builds and emulator tests. These are not real credentials. +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + return switch (defaultTargetPlatform) { + TargetPlatform.android => android, + TargetPlatform.iOS => ios, + TargetPlatform.macOS => macos, + TargetPlatform.windows => windows, + _ => throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ), + }; + } + + static const web = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:web:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + authDomain: 'flutterfire-e2e-tests.firebaseapp.com', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const android = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:android:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const ios = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:ios:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const macos = ios; + + static const windows = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:web:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + authDomain: 'flutterfire-e2e-tests.firebaseapp.com', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); +} diff --git a/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/macos/Podfile b/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/macos/Podfile index 22d9caad2e9d..9ec46f8cd53c 100644 --- a/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/macos/Podfile +++ b/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.12' +platform :osx, '10.15' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/macos/Runner.xcodeproj/project.pbxproj b/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/macos/Runner.xcodeproj/project.pbxproj index 8551f7a9f784..e72c7ddec45b 100644 --- a/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/macos/Runner.xcodeproj/project.pbxproj @@ -424,7 +424,6 @@ CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = YYX2P3XVJ7; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -552,7 +551,6 @@ CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = YYX2P3XVJ7; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -574,7 +572,6 @@ CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = YYX2P3XVJ7; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", diff --git a/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/macos/Runner/Configs/AppInfo.xcconfig index cf9be60ca471..43d10e8bcd7f 100644 --- a/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/macos/Runner/Configs/AppInfo.xcconfig +++ b/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/macos/Runner/Configs/AppInfo.xcconfig @@ -8,7 +8,7 @@ PRODUCT_NAME = example // The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = com.example.example +PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests // The copyright displayed in application information PRODUCT_COPYRIGHT = Copyright © 2021 com.example. All rights reserved. diff --git a/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/pubspec.yaml b/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/pubspec.yaml index 27cf784abdb1..8182fbdfe48a 100644 --- a/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/pubspec.yaml +++ b/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/pubspec.yaml @@ -17,6 +17,13 @@ dependencies: dev_dependencies: flutter_lints: ^6.0.0 + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + # `integration_test/report_test_results.dart` names each executed test via + # `package:test_api/hooks.dart`; `any` defers to the version flutter_test pins. + test_api: any flutter: uses-material-design: true diff --git a/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/test_driver/integration_test.dart b/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..691723cbbfe1 --- /dev/null +++ b/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/test_driver/integration_test.dart @@ -0,0 +1,34 @@ +// Copyright 2022, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io'; + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver( + writeResponseOnFailure: true, + responseDataCallback: (Map? data) async { + final results = + (data?['testResults'] as Map?)?.cast() ?? + const {}; + var passed = 0; + var failed = 0; + for (final entry in results.entries) { + final ok = entry.value == 'success'; + ok ? passed++ : failed++; + // ignore: avoid_print + print('${ok ? '✅' : '❌'} ${entry.key}'); + } + // ignore: avoid_print + print('Web e2e summary: $passed passed, $failed failed, ' + '${results.length} total'); + if (results.isEmpty) { + // ignore: avoid_print + print('[E] No tests reported by the app - treating as ' + 'infrastructure failure.'); + exit(1); + } + await writeResponseData(data); + }, + ); diff --git a/packages/firebase_performance/firebase_performance/example/.metadata b/packages/firebase_performance/firebase_performance/example/.metadata index 784ce1298249..e68d14000b19 100644 --- a/packages/firebase_performance/firebase_performance/example/.metadata +++ b/packages/firebase_performance/firebase_performance/example/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "a14f74ff3a1cbd521163c5f03d68113d50af93d3" + revision: "ee80f08bbf97172ec030b8751ceab557177a34a6" channel: "stable" project_type: app @@ -13,11 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 - base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 - - platform: web - create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 - base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 + create_revision: ee80f08bbf97172ec030b8751ceab557177a34a6 + base_revision: ee80f08bbf97172ec030b8751ceab557177a34a6 + - platform: ios + create_revision: ee80f08bbf97172ec030b8751ceab557177a34a6 + base_revision: ee80f08bbf97172ec030b8751ceab557177a34a6 # User provided section diff --git a/packages/firebase_performance/firebase_performance/example/android/app/build.gradle b/packages/firebase_performance/firebase_performance/example/android/app/build.gradle index 8b3cf88dfa14..32be6e1e60cc 100644 --- a/packages/firebase_performance/firebase_performance/example/android/app/build.gradle +++ b/packages/firebase_performance/firebase_performance/example/android/app/build.gradle @@ -43,6 +43,12 @@ android { } defaultConfig { + // Deliberately the mega test app's Firebase app identity: the live-service + // e2e suite in `integration_test/` talks to real Firebase backends, which + // only accept app ids registered in the `flutterfire-e2e-tests` project. + // `namespace` and the Kotlin package below are unaffected. Two of these + // examples cannot be co-installed on one device - CI installs one per + // emulator. applicationId = "io.flutter.plugins.firebase.tests" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. diff --git a/tests/integration_test/firebase_performance/firebase_performance_e2e_test.dart b/packages/firebase_performance/firebase_performance/example/integration_test/e2e_test.dart similarity index 90% rename from tests/integration_test/firebase_performance/firebase_performance_e2e_test.dart rename to packages/firebase_performance/firebase_performance/example/integration_test/e2e_test.dart index 75099753b039..101cfc47ae31 100644 --- a/tests/integration_test/firebase_performance/firebase_performance_e2e_test.dart +++ b/packages/firebase_performance/firebase_performance/example/integration_test/e2e_test.dart @@ -8,15 +8,29 @@ import 'package:flutter/foundation.dart' show TargetPlatform, defaultTargetPlatform, kIsWeb; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'package:tests/firebase_options.dart'; +import 'package:firebase_performance_example/firebase_options.dart'; + +import 'report_test_results.dart'; void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + reportTestResultsToDriver(binding); setUpAll(() async { - await Firebase.initializeApp( - options: DefaultFirebaseOptions.currentPlatform, - ); + // The native SDK may already have configured [DEFAULT] from a bundled + // GoogleService-Info.plist (the plugin registrant does this before any + // Dart runs). Dart's Firebase.apps cannot see that app until the first + // platform-channel call, so the only reliable guard is catching the + // duplicate-app error and keeping the natively configured instance. + try { + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + } on FirebaseException catch (e) { + if (e.code != 'duplicate-app') { + rethrow; + } + } }); group( diff --git a/packages/firebase_performance/firebase_performance/example/integration_test/report_test_results.dart b/packages/firebase_performance/firebase_performance/example/integration_test/report_test_results.dart new file mode 100644 index 000000000000..ddeaeab1eaf0 --- /dev/null +++ b/packages/firebase_performance/firebase_performance/example/integration_test/report_test_results.dart @@ -0,0 +1,40 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/common.dart' show Failure; +import 'package:integration_test/integration_test.dart'; +import 'package:test_api/hooks.dart' show TestHandle; + +/// Records every executed test via the package:test lifecycle (plain `test()` +/// declarations never reach the binding's `results` map - only `testWidgets` +/// does) and publishes them through +/// [IntegrationTestWidgetsFlutterBinding.reportData], the only channel that +/// crosses the wire on web. +void reportTestResultsToDriver(IntegrationTestWidgetsFlutterBinding binding) { + final executed = []; + setUp(() { + executed.add(TestHandle.current.name); + }); + tearDownAll(() { + // `TestHandle.current.name` is the full name, prefixed with the enclosing + // group names, while `binding.results` is keyed by the bare `testWidgets` + // description - so match a failed entry on the suffix as well as on + // equality, otherwise no grouped test could ever be marked failed. + final failures = [ + for (final entry in binding.results.entries) + if (entry.value is Failure) entry.key, + ]; + bool didFail(String name) => failures + .any((failure) => name == failure || name.endsWith(' $failure')); + + binding.reportData ??= {}; + binding.reportData!['testResults'] = { + // Plain test() failures are not individually attributable here, but they + // fail the run as a whole via allTestsPassed; testWidgets failures are + // attributed from the binding's results map. + for (final name in executed) name: didFail(name) ? 'failed' : 'success', + }; + }); +} diff --git a/packages/firebase_performance/firebase_performance/example/ios/.gitignore b/packages/firebase_performance/firebase_performance/example/ios/.gitignore new file mode 100644 index 000000000000..7a7f9873ad7d --- /dev/null +++ b/packages/firebase_performance/firebase_performance/example/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/packages/firebase_performance/firebase_performance/example/ios/Flutter/AppFrameworkInfo.plist b/packages/firebase_performance/firebase_performance/example/ios/Flutter/AppFrameworkInfo.plist index 9b41e7d87980..391a902b2beb 100644 --- a/packages/firebase_performance/firebase_performance/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/firebase_performance/firebase_performance/example/ios/Flutter/AppFrameworkInfo.plist @@ -20,11 +20,5 @@ ???? CFBundleVersion 1.0 - UIRequiredDeviceCapabilities - - arm64 - - MinimumOSVersion - 11.0 diff --git a/packages/firebase_performance/firebase_performance/example/ios/Podfile b/packages/firebase_performance/firebase_performance/example/ios/Podfile index 211ff74f84c6..22efb526f6ba 100644 --- a/packages/firebase_performance/firebase_performance/example/ios/Podfile +++ b/packages/firebase_performance/firebase_performance/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project - platform :ios, '15.0' +platform :ios, '15.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -29,8 +29,6 @@ flutter_ios_podfile_setup target 'Runner' do use_frameworks! - use_modular_headers! - flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) end diff --git a/packages/firebase_performance/firebase_performance/example/ios/Runner.xcodeproj/project.pbxproj b/packages/firebase_performance/firebase_performance/example/ios/Runner.xcodeproj/project.pbxproj index bbab2619fc7a..32e6443b8bea 100644 --- a/packages/firebase_performance/firebase_performance/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/firebase_performance/firebase_performance/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,23 +3,30 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; - 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB31CF90195004384FC /* Generated.xcconfig */; }; - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; - 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - 9903DF608794B096EA88A2AE /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 1113342D5D37BB68C9894616 /* GoogleService-Info.plist */; }; - A179044980BFAE87AA7CC5EA /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43C088DEB2D7CF557A30EB8F /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; @@ -34,20 +41,18 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 1113342D5D37BB68C9894616 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 355BC2C50462780197DDCB5E /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 43C088DEB2D7CF557A30EB8F /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 73333FD263564BF21EA2CA68 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; @@ -59,19 +64,18 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - A179044980BFAE87AA7CC5EA /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 556632B50846818D9A90AA4B /* Frameworks */ = { + 331C8082294A63A400263BE5 /* RunnerTests */ = { isa = PBXGroup; children = ( - 43C088DEB2D7CF557A30EB8F /* Pods_Runner.framework */, + 331C807B294A618700263BE5 /* RunnerTests.swift */, ); - name = Frameworks; + path = RunnerTests; sourceTree = ""; }; 9740EEB11CF90186004384FC /* Flutter */ = { @@ -91,9 +95,7 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, - DE7EB5CF2B4FC4A8BCD09CC6 /* Pods */, - 556632B50846818D9A90AA4B /* Frameworks */, - 1113342D5D37BB68C9894616 /* GoogleService-Info.plist */, + 331C8082294A63A400263BE5 /* RunnerTests */, ); sourceTree = ""; }; @@ -101,6 +103,7 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; @@ -108,57 +111,57 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, ); path = Runner; sourceTree = ""; }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 97C146F21CF9000F007C117D /* main.m */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; - DE7EB5CF2B4FC4A8BCD09CC6 /* Pods */ = { - isa = PBXGroup; - children = ( - 73333FD263564BF21EA2CA68 /* Pods-Runner.debug.xcconfig */, - 355BC2C50462780197DDCB5E /* Pods-Runner.release.xcconfig */, - ); - name = Pods; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 92CA2C06BD2DACC2EF38987C /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - C8F9A74D28F7E3549E25E81B /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); dependencies = ( ); name = Runner; + packageProductDependencies = ( + ); productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; @@ -169,21 +172,25 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1300; - ORGANIZATIONNAME = "The Chromium Authors"; + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; - DevelopmentTeam = YYX2P3XVJ7; + LastSwiftMigration = 1100; }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( - English, en, Base, ); @@ -193,22 +200,27 @@ projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - 9903DF608794B096EA88A2AE /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -217,10 +229,12 @@ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -229,26 +243,9 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 92CA2C06BD2DACC2EF38987C /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -261,59 +258,37 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - C8F9A74D28F7E3549E25E81B /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/FirebaseABTesting/FirebaseABTesting.framework", - "${BUILT_PRODUCTS_DIR}/FirebaseCore/FirebaseCore.framework", - "${BUILT_PRODUCTS_DIR}/FirebaseCoreInternal/FirebaseCoreInternal.framework", - "${BUILT_PRODUCTS_DIR}/FirebaseInstallations/FirebaseInstallations.framework", - "${BUILT_PRODUCTS_DIR}/FirebasePerformance/FirebasePerformance.framework", - "${BUILT_PRODUCTS_DIR}/FirebaseRemoteConfig/FirebaseRemoteConfig.framework", - "${BUILT_PRODUCTS_DIR}/GoogleDataTransport/GoogleDataTransport.framework", - "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework", - "${BUILT_PRODUCTS_DIR}/PromisesObjC/FBLPromises.framework", - "${BUILT_PRODUCTS_DIR}/integration_test/integration_test.framework", - "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseABTesting.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseCore.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseCoreInternal.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseInstallations.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebasePerformance.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseRemoteConfig.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleDataTransport.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBLPromises.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/integration_test.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, - 97C146F31CF9000F007C117D /* main.m in Sources */, + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -334,10 +309,132 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 7K2HVKAM5V; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -347,12 +444,14 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; @@ -365,6 +464,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -391,6 +491,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -400,12 +501,14 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; @@ -418,6 +521,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -429,6 +533,9 @@ IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; @@ -439,21 +546,20 @@ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = YYX2P3XVJ7; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 7K2HVKAM5V; ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( + LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/Flutter", + "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; @@ -463,21 +569,19 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = YYX2P3XVJ7; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 7K2HVKAM5V; ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( + LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/Flutter", + "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; @@ -485,11 +589,22 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147031CF9000F007C117D /* Debug */, 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -499,11 +614,13 @@ buildConfigurations = ( 97C147061CF9000F007C117D /* Debug */, 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } diff --git a/packages/firebase_performance/firebase_performance/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/firebase_performance/firebase_performance/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/firebase_performance/firebase_performance/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/firebase_performance/firebase_performance/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/firebase_performance/firebase_performance/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000000..f9b0d7c5ea15 --- /dev/null +++ b/packages/firebase_performance/firebase_performance/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/firebase_performance/firebase_performance/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/firebase_performance/firebase_performance/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index df3b421f9b8a..c3fedb29c990 100644 --- a/packages/firebase_performance/firebase_performance/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/firebase_performance/firebase_performance/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,10 +1,28 @@ + + + + + + + + + + + + + + @@ -59,15 +91,9 @@ ReferencedContainer = "container:Runner.xcodeproj"> - - - - - - diff --git a/packages/firebase_performance/firebase_performance/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/firebase_performance/firebase_performance/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000000..f9b0d7c5ea15 --- /dev/null +++ b/packages/firebase_performance/firebase_performance/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/firebase_performance/firebase_performance/example/ios/Runner/AppDelegate.h b/packages/firebase_performance/firebase_performance/example/ios/Runner/AppDelegate.h deleted file mode 100644 index 01e6e1d4793a..000000000000 --- a/packages/firebase_performance/firebase_performance/example/ios/Runner/AppDelegate.h +++ /dev/null @@ -1,6 +0,0 @@ -#import -#import - -@interface AppDelegate : FlutterAppDelegate - -@end diff --git a/packages/firebase_performance/firebase_performance/example/ios/Runner/AppDelegate.m b/packages/firebase_performance/firebase_performance/example/ios/Runner/AppDelegate.m deleted file mode 100644 index 9c45e766f906..000000000000 --- a/packages/firebase_performance/firebase_performance/example/ios/Runner/AppDelegate.m +++ /dev/null @@ -1,15 +0,0 @@ -#include "AppDelegate.h" -#include "GeneratedPluginRegistrant.h" - -@implementation AppDelegate - -- (BOOL)application:(UIApplication *)application - didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - return [super application:application didFinishLaunchingWithOptions:launchOptions]; -} - -- (void)didInitializeImplicitFlutterEngine:(NSObject *)engineBridge { - [GeneratedPluginRegistrant registerWithRegistry:engineBridge.pluginRegistry]; -} - -@end diff --git a/packages/firebase_performance/firebase_performance/example/ios/Runner/AppDelegate.swift b/packages/firebase_performance/firebase_performance/example/ios/Runner/AppDelegate.swift new file mode 100644 index 000000000000..c30b367ec0a9 --- /dev/null +++ b/packages/firebase_performance/firebase_performance/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,16 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } +} diff --git a/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png index 3d43d11e66f4..dc9ada4725e9 100644 Binary files a/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png index 28c6bf03016f..7353c41ecf9c 100644 Binary files a/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and b/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png index 2ccbfd967d96..797d452e4589 100644 Binary files a/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and b/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png index f091b6b0bca8..6ed2d933e112 100644 Binary files a/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and b/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png index 4cde12118dda..4cd7b0099ca8 100644 Binary files a/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and b/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png index d0ef06e7edb8..fe730945a01f 100644 Binary files a/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and b/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png index dcdc2306c285..321773cd857a 100644 Binary files a/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and b/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png index 2ccbfd967d96..797d452e4589 100644 Binary files a/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and b/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png index c8f9ed8f5cee..502f463a9bc8 100644 Binary files a/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and b/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png index a6d6b8609df0..0ec303439225 100644 Binary files a/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and b/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png index a6d6b8609df0..0ec303439225 100644 Binary files a/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and b/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png index 75b2d164a5a9..e9f5fea27c70 100644 Binary files a/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and b/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png index c4df70d39da7..84ac32ae7d98 100644 Binary files a/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and b/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png index 6a84f41e14e2..8953cba09064 100644 Binary files a/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and b/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png index d0e1f5853602..0467bf12aa4d 100644 Binary files a/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/packages/firebase_performance/firebase_performance/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/packages/firebase_performance/firebase_performance/example/ios/Runner/Info.plist b/packages/firebase_performance/firebase_performance/example/ios/Runner/Info.plist index 93ccf4a11c44..a49ed9a27d8f 100644 --- a/packages/firebase_performance/firebase_performance/example/ios/Runner/Info.plist +++ b/packages/firebase_performance/firebase_performance/example/ios/Runner/Info.plist @@ -2,8 +2,12 @@ + CADisableMinimumFrameDurationOnPhone + CFBundleDevelopmentRegion - en + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Firebase Performance Example CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -15,38 +19,13 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.0 + $(FLUTTER_BUILD_NAME) CFBundleSignature ???? CFBundleVersion - 1 + $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UIRequiredDeviceCapabilities - - arm64 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - CADisableMinimumFrameDurationOnPhone - UIApplicationSceneManifest UIApplicationSupportsMultipleScenes @@ -58,15 +37,34 @@ UISceneClassName UIWindowScene - UISceneDelegateClassName - FlutterSceneDelegate UISceneConfigurationName flutter + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate UISceneStoryboardFile Main + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + diff --git a/packages/firebase_performance/firebase_performance/example/ios/Runner/Runner-Bridging-Header.h b/packages/firebase_performance/firebase_performance/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 000000000000..308a2a560b42 --- /dev/null +++ b/packages/firebase_performance/firebase_performance/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/packages/firebase_performance/firebase_performance/example/ios/Runner/SceneDelegate.swift b/packages/firebase_performance/firebase_performance/example/ios/Runner/SceneDelegate.swift new file mode 100644 index 000000000000..b9ce8ea2b2ad --- /dev/null +++ b/packages/firebase_performance/firebase_performance/example/ios/Runner/SceneDelegate.swift @@ -0,0 +1,6 @@ +import Flutter +import UIKit + +class SceneDelegate: FlutterSceneDelegate { + +} diff --git a/packages/firebase_performance/firebase_performance/example/ios/Runner/main.m b/packages/firebase_performance/firebase_performance/example/ios/Runner/main.m deleted file mode 100644 index dff6597e4513..000000000000 --- a/packages/firebase_performance/firebase_performance/example/ios/Runner/main.m +++ /dev/null @@ -1,9 +0,0 @@ -#import -#import -#import "AppDelegate.h" - -int main(int argc, char* argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} diff --git a/packages/firebase_performance/firebase_performance/example/ios/RunnerTests/RunnerTests.swift b/packages/firebase_performance/firebase_performance/example/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 000000000000..86a7c3b1b611 --- /dev/null +++ b/packages/firebase_performance/firebase_performance/example/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/packages/firebase_performance/firebase_performance/example/lib/firebase_options.dart b/packages/firebase_performance/firebase_performance/example/lib/firebase_options.dart new file mode 100644 index 000000000000..a5a1360f6620 --- /dev/null +++ b/packages/firebase_performance/firebase_performance/example/lib/firebase_options.dart @@ -0,0 +1,61 @@ +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// Generated for CI builds and emulator tests. These are not real credentials. +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + return switch (defaultTargetPlatform) { + TargetPlatform.android => android, + TargetPlatform.iOS => ios, + TargetPlatform.macOS => macos, + TargetPlatform.windows => windows, + _ => throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ), + }; + } + + static const web = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:web:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + authDomain: 'flutterfire-e2e-tests.firebaseapp.com', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const android = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:android:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const ios = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:ios:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const macos = ios; + + static const windows = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:web:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + authDomain: 'flutterfire-e2e-tests.firebaseapp.com', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); +} diff --git a/packages/firebase_performance/firebase_performance/example/pubspec.yaml b/packages/firebase_performance/firebase_performance/example/pubspec.yaml index 385600d391f2..89291e4d16f5 100644 --- a/packages/firebase_performance/firebase_performance/example/pubspec.yaml +++ b/packages/firebase_performance/firebase_performance/example/pubspec.yaml @@ -20,6 +20,9 @@ dev_dependencies: sdk: flutter integration_test: sdk: flutter + # `integration_test/report_test_results.dart` names each executed test via + # `package:test_api/hooks.dart`; `any` defers to the version flutter_test pins. + test_api: any flutter: uses-material-design: true diff --git a/packages/firebase_performance/firebase_performance/example/test_driver/integration_test.dart b/packages/firebase_performance/firebase_performance/example/test_driver/integration_test.dart index f1ac26f27b88..691723cbbfe1 100644 --- a/packages/firebase_performance/firebase_performance/example/test_driver/integration_test.dart +++ b/packages/firebase_performance/firebase_performance/example/test_driver/integration_test.dart @@ -2,6 +2,33 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'dart:io'; + import 'package:integration_test/integration_test_driver.dart'; -Future main() => integrationDriver(); +Future main() => integrationDriver( + writeResponseOnFailure: true, + responseDataCallback: (Map? data) async { + final results = + (data?['testResults'] as Map?)?.cast() ?? + const {}; + var passed = 0; + var failed = 0; + for (final entry in results.entries) { + final ok = entry.value == 'success'; + ok ? passed++ : failed++; + // ignore: avoid_print + print('${ok ? '✅' : '❌'} ${entry.key}'); + } + // ignore: avoid_print + print('Web e2e summary: $passed passed, $failed failed, ' + '${results.length} total'); + if (results.isEmpty) { + // ignore: avoid_print + print('[E] No tests reported by the app - treating as ' + 'infrastructure failure.'); + exit(1); + } + await writeResponseData(data); + }, + ); diff --git a/packages/firebase_remote_config/firebase_remote_config/example/android/app/build.gradle b/packages/firebase_remote_config/firebase_remote_config/example/android/app/build.gradle index 8bf1becfc625..0f790a320633 100644 --- a/packages/firebase_remote_config/firebase_remote_config/example/android/app/build.gradle +++ b/packages/firebase_remote_config/firebase_remote_config/example/android/app/build.gradle @@ -42,7 +42,13 @@ android { } defaultConfig { - applicationId = "io.flutter.plugins.firebase.remoteconfig.example" + // Deliberately the mega test app's Firebase app identity: the live-service + // e2e suite in `integration_test/` talks to real Firebase backends, which + // only accept app ids registered in the `flutterfire-e2e-tests` project. + // `namespace` and the Kotlin package below are unaffected. Two of these + // examples cannot be co-installed on one device - CI installs one per + // emulator. + applicationId = "io.flutter.plugins.firebase.tests" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. minSdk = 23 diff --git a/tests/integration_test/firebase_remote_config/firebase_remote_config_e2e_test.dart b/packages/firebase_remote_config/firebase_remote_config/example/integration_test/e2e_test.dart similarity index 89% rename from tests/integration_test/firebase_remote_config/firebase_remote_config_e2e_test.dart rename to packages/firebase_remote_config/firebase_remote_config/example/integration_test/e2e_test.dart index 971f280369dd..0e851ed09a5b 100644 --- a/tests/integration_test/firebase_remote_config/firebase_remote_config_e2e_test.dart +++ b/packages/firebase_remote_config/firebase_remote_config/example/integration_test/e2e_test.dart @@ -7,18 +7,32 @@ import 'package:firebase_remote_config/firebase_remote_config.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'package:tests/firebase_options.dart'; +import 'package:firebase_remote_config_example/firebase_options.dart'; + +import 'report_test_results.dart'; void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + reportTestResultsToDriver(binding); group( 'firebase_remote_config', () { setUpAll(() async { - await Firebase.initializeApp( - options: DefaultFirebaseOptions.currentPlatform, - ); + // The native SDK may already have configured [DEFAULT] from a bundled + // GoogleService-Info.plist (the plugin registrant does this before any + // Dart runs). Dart's Firebase.apps cannot see that app until the first + // platform-channel call, so the only reliable guard is catching the + // duplicate-app error and keeping the natively configured instance. + try { + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + } on FirebaseException catch (e) { + if (e.code != 'duplicate-app') { + rethrow; + } + } await FirebaseRemoteConfig.instance.setConfigSettings( RemoteConfigSettings( fetchTimeout: const Duration(seconds: 8), diff --git a/packages/firebase_remote_config/firebase_remote_config/example/integration_test/report_test_results.dart b/packages/firebase_remote_config/firebase_remote_config/example/integration_test/report_test_results.dart new file mode 100644 index 000000000000..038d20c39931 --- /dev/null +++ b/packages/firebase_remote_config/firebase_remote_config/example/integration_test/report_test_results.dart @@ -0,0 +1,40 @@ +// Copyright 2019, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/common.dart' show Failure; +import 'package:integration_test/integration_test.dart'; +import 'package:test_api/hooks.dart' show TestHandle; + +/// Records every executed test via the package:test lifecycle (plain `test()` +/// declarations never reach the binding's `results` map - only `testWidgets` +/// does) and publishes them through +/// [IntegrationTestWidgetsFlutterBinding.reportData], the only channel that +/// crosses the wire on web. +void reportTestResultsToDriver(IntegrationTestWidgetsFlutterBinding binding) { + final executed = []; + setUp(() { + executed.add(TestHandle.current.name); + }); + tearDownAll(() { + // `TestHandle.current.name` is the full name, prefixed with the enclosing + // group names, while `binding.results` is keyed by the bare `testWidgets` + // description - so match a failed entry on the suffix as well as on + // equality, otherwise no grouped test could ever be marked failed. + final failures = [ + for (final entry in binding.results.entries) + if (entry.value is Failure) entry.key, + ]; + bool didFail(String name) => failures + .any((failure) => name == failure || name.endsWith(' $failure')); + + binding.reportData ??= {}; + binding.reportData!['testResults'] = { + // Plain test() failures are not individually attributable here, but they + // fail the run as a whole via allTestsPassed; testWidgets failures are + // attributed from the binding's results map. + for (final name in executed) name: didFail(name) ? 'failed' : 'success', + }; + }); +} diff --git a/packages/firebase_remote_config/firebase_remote_config/example/ios/Runner.xcodeproj/project.pbxproj b/packages/firebase_remote_config/firebase_remote_config/example/ios/Runner.xcodeproj/project.pbxproj index a2635496bf65..de6b21ededee 100644 --- a/packages/firebase_remote_config/firebase_remote_config/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/firebase_remote_config/firebase_remote_config/example/ios/Runner.xcodeproj/project.pbxproj @@ -430,7 +430,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.remoteconfig.example; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; @@ -455,7 +455,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.remoteconfig.example; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; diff --git a/packages/firebase_remote_config/firebase_remote_config/example/lib/firebase_options.dart b/packages/firebase_remote_config/firebase_remote_config/example/lib/firebase_options.dart new file mode 100644 index 000000000000..a5a1360f6620 --- /dev/null +++ b/packages/firebase_remote_config/firebase_remote_config/example/lib/firebase_options.dart @@ -0,0 +1,61 @@ +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// Generated for CI builds and emulator tests. These are not real credentials. +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + return switch (defaultTargetPlatform) { + TargetPlatform.android => android, + TargetPlatform.iOS => ios, + TargetPlatform.macOS => macos, + TargetPlatform.windows => windows, + _ => throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ), + }; + } + + static const web = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:web:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + authDomain: 'flutterfire-e2e-tests.firebaseapp.com', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const android = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:android:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const ios = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:ios:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const macos = ios; + + static const windows = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:web:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + authDomain: 'flutterfire-e2e-tests.firebaseapp.com', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); +} diff --git a/packages/firebase_remote_config/firebase_remote_config/example/macos/Podfile b/packages/firebase_remote_config/firebase_remote_config/example/macos/Podfile index c795730db8ed..b52666a10389 100644 --- a/packages/firebase_remote_config/firebase_remote_config/example/macos/Podfile +++ b/packages/firebase_remote_config/firebase_remote_config/example/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.14' +platform :osx, '10.15' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/packages/firebase_remote_config/firebase_remote_config/example/macos/Runner.xcodeproj/project.pbxproj b/packages/firebase_remote_config/firebase_remote_config/example/macos/Runner.xcodeproj/project.pbxproj index fb2fead0e810..e1a52e3bc2ad 100644 --- a/packages/firebase_remote_config/firebase_remote_config/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/firebase_remote_config/firebase_remote_config/example/macos/Runner.xcodeproj/project.pbxproj @@ -396,7 +396,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.remoteconfig.example.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example"; @@ -410,7 +410,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.remoteconfig.example.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example"; @@ -424,7 +424,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.remoteconfig.example.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example"; diff --git a/packages/firebase_remote_config/firebase_remote_config/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/firebase_remote_config/firebase_remote_config/example/macos/Runner/Configs/AppInfo.xcconfig index 22543f75d24d..addd824fc297 100644 --- a/packages/firebase_remote_config/firebase_remote_config/example/macos/Runner/Configs/AppInfo.xcconfig +++ b/packages/firebase_remote_config/firebase_remote_config/example/macos/Runner/Configs/AppInfo.xcconfig @@ -8,7 +8,7 @@ PRODUCT_NAME = example // The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.remoteconfig.example +PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.tests // The copyright displayed in application information PRODUCT_COPYRIGHT = Copyright © 2023 io.flutter.plugins.firebase.remoteconfig. All rights reserved. diff --git a/packages/firebase_remote_config/firebase_remote_config/example/macos/Runner/DebugProfile.entitlements b/packages/firebase_remote_config/firebase_remote_config/example/macos/Runner/DebugProfile.entitlements index 1fbcb4eafd8a..3ba6c1266f21 100644 --- a/packages/firebase_remote_config/firebase_remote_config/example/macos/Runner/DebugProfile.entitlements +++ b/packages/firebase_remote_config/firebase_remote_config/example/macos/Runner/DebugProfile.entitlements @@ -10,7 +10,5 @@ com.apple.security.network.server - keychain-access-groups - diff --git a/packages/firebase_remote_config/firebase_remote_config/example/pubspec.yaml b/packages/firebase_remote_config/firebase_remote_config/example/pubspec.yaml index d5b64e9ba3a0..2cd1cc08b3d2 100644 --- a/packages/firebase_remote_config/firebase_remote_config/example/pubspec.yaml +++ b/packages/firebase_remote_config/firebase_remote_config/example/pubspec.yaml @@ -14,5 +14,14 @@ dependencies: flutter: sdk: flutter +dev_dependencies: + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + # `integration_test/report_test_results.dart` names each executed test via + # `package:test_api/hooks.dart`; `any` defers to the version flutter_test pins. + test_api: any + flutter: uses-material-design: true diff --git a/packages/firebase_remote_config/firebase_remote_config/example/test_driver/integration_test.dart b/packages/firebase_remote_config/firebase_remote_config/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..691723cbbfe1 --- /dev/null +++ b/packages/firebase_remote_config/firebase_remote_config/example/test_driver/integration_test.dart @@ -0,0 +1,34 @@ +// Copyright 2022, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io'; + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver( + writeResponseOnFailure: true, + responseDataCallback: (Map? data) async { + final results = + (data?['testResults'] as Map?)?.cast() ?? + const {}; + var passed = 0; + var failed = 0; + for (final entry in results.entries) { + final ok = entry.value == 'success'; + ok ? passed++ : failed++; + // ignore: avoid_print + print('${ok ? '✅' : '❌'} ${entry.key}'); + } + // ignore: avoid_print + print('Web e2e summary: $passed passed, $failed failed, ' + '${results.length} total'); + if (results.isEmpty) { + // ignore: avoid_print + print('[E] No tests reported by the app - treating as ' + 'infrastructure failure.'); + exit(1); + } + await writeResponseData(data); + }, + ); diff --git a/packages/firebase_storage/firebase_storage/example/android/app/google-services.json b/packages/firebase_storage/firebase_storage/example/android/app/google-services.json new file mode 100644 index 000000000000..28028d3cbeca --- /dev/null +++ b/packages/firebase_storage/firebase_storage/example/android/app/google-services.json @@ -0,0 +1,24 @@ +{ + "project_info": { + "project_number": "123456789012", + "project_id": "flutterfire-e2e-tests", + "storage_bucket": "flutterfire-e2e-tests.appspot.com", + "firebase_url": "https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:123456789012:android:0000000000000000000000", + "android_client_info": { + "package_name": "io.flutter.plugins.firebasestorageexample" + } + }, + "api_key": [ + { + "current_key": "AIzaSyDUMMYKEYFORFLUTTERFIRECITESTS0000" + } + ] + } + ], + "configuration_version": "1" +} diff --git a/packages/firebase_storage/firebase_storage/example/android/app/src/main/AndroidManifest.xml b/packages/firebase_storage/firebase_storage/example/android/app/src/main/AndroidManifest.xml index f8f8dcf140bb..cdec86e7e65b 100644 --- a/packages/firebase_storage/firebase_storage/example/android/app/src/main/AndroidManifest.xml +++ b/packages/firebase_storage/firebase_storage/example/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ diff --git a/packages/firebase_storage/firebase_storage/example/android/gradle.properties b/packages/firebase_storage/firebase_storage/example/android/gradle.properties index 3c0f502f334a..1570ea30cb80 100644 --- a/packages/firebase_storage/firebase_storage/example/android/gradle.properties +++ b/packages/firebase_storage/firebase_storage/example/android/gradle.properties @@ -1,4 +1,8 @@ org.gradle.jvmargs=-Xmx4G -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true android.enableJetifier=true -androidGradlePluginVersion=8.3.0 \ No newline at end of file +androidGradlePluginVersion=8.9.1 +# This builtInKotlin flag was added automatically by Flutter migrator +android.builtInKotlin=false +# This newDsl flag was added automatically by Flutter migrator +android.newDsl=false diff --git a/packages/firebase_storage/firebase_storage/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/firebase_storage/firebase_storage/example/android/gradle/wrapper/gradle-wrapper.properties index e411586a54a8..d6e308a63789 100644 --- a/packages/firebase_storage/firebase_storage/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/firebase_storage/firebase_storage/example/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/firebase_storage/firebase_storage/example/android/settings.gradle b/packages/firebase_storage/firebase_storage/example/android/settings.gradle index 30463c1cf2f2..4fb566e9929e 100644 --- a/packages/firebase_storage/firebase_storage/example/android/settings.gradle +++ b/packages/firebase_storage/firebase_storage/example/android/settings.gradle @@ -22,7 +22,7 @@ plugins { // START: FlutterFire Configuration id "com.google.gms.google-services" version "4.3.15" apply false // END: FlutterFire Configuration - id "org.jetbrains.kotlin.android" version "1.9.22" apply false + id "org.jetbrains.kotlin.android" version "2.1.0" apply false } include ":app" diff --git a/packages/firebase_storage/firebase_storage/example/integration_test/e2e_test.dart b/packages/firebase_storage/firebase_storage/example/integration_test/e2e_test.dart index 50d112cd7f20..31581c7afceb 100644 --- a/packages/firebase_storage/firebase_storage/example/integration_test/e2e_test.dart +++ b/packages/firebase_storage/firebase_storage/example/integration_test/e2e_test.dart @@ -10,18 +10,31 @@ import 'package:firebase_storage_example/firebase_options.dart'; import 'instance_e2e.dart'; import 'list_result_e2e.dart'; import 'reference_e2e.dart'; +import 'report_test_results.dart'; import 'task_e2e.dart'; import 'second_bucket.dart'; import 'test_utils.dart'; void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + reportTestResultsToDriver(binding); group('firebase_storage', () { setUpAll(() async { - await Firebase.initializeApp( - options: DefaultFirebaseOptions.currentPlatform, - ); + // The native SDK may already have configured [DEFAULT] from a bundled + // GoogleService-Info.plist (the plugin registrant does this before any + // Dart runs). Dart's Firebase.apps cannot see that app until the first + // platform-channel call, so the only reliable guard is catching the + // duplicate-app error and keeping the natively configured instance. + try { + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + } on FirebaseException catch (e) { + if (e.code != 'duplicate-app') { + rethrow; + } + } await FirebaseStorage.instance .useStorageEmulator(testEmulatorHost, testEmulatorPort); diff --git a/packages/firebase_storage/firebase_storage/example/integration_test/report_test_results.dart b/packages/firebase_storage/firebase_storage/example/integration_test/report_test_results.dart new file mode 100644 index 000000000000..fb80e3ba19f7 --- /dev/null +++ b/packages/firebase_storage/firebase_storage/example/integration_test/report_test_results.dart @@ -0,0 +1,40 @@ +// Copyright 2022, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/common.dart' show Failure; +import 'package:integration_test/integration_test.dart'; +import 'package:test_api/hooks.dart' show TestHandle; + +/// Records every executed test via the package:test lifecycle (plain `test()` +/// declarations never reach the binding's `results` map - only `testWidgets` +/// does) and publishes them through +/// [IntegrationTestWidgetsFlutterBinding.reportData], the only channel that +/// crosses the wire on web. +void reportTestResultsToDriver(IntegrationTestWidgetsFlutterBinding binding) { + final executed = []; + setUp(() { + executed.add(TestHandle.current.name); + }); + tearDownAll(() { + // `TestHandle.current.name` is the full name, prefixed with the enclosing + // group names, while `binding.results` is keyed by the bare `testWidgets` + // description - so match a failed entry on the suffix as well as on + // equality, otherwise no grouped test could ever be marked failed. + final failures = [ + for (final entry in binding.results.entries) + if (entry.value is Failure) entry.key, + ]; + bool didFail(String name) => failures + .any((failure) => name == failure || name.endsWith(' $failure')); + + binding.reportData ??= {}; + binding.reportData!['testResults'] = { + // Plain test() failures are not individually attributable here, but they + // fail the run as a whole via allTestsPassed; testWidgets failures are + // attributed from the binding's results map. + for (final name in executed) name: didFail(name) ? 'failed' : 'success', + }; + }); +} diff --git a/packages/firebase_storage/firebase_storage/example/ios/Runner/GoogleService-Info.plist b/packages/firebase_storage/firebase_storage/example/ios/Runner/GoogleService-Info.plist new file mode 100644 index 000000000000..49ffc1c15f96 --- /dev/null +++ b/packages/firebase_storage/firebase_storage/example/ios/Runner/GoogleService-Info.plist @@ -0,0 +1,22 @@ + + + + + API_KEY + AIzaSyDUMMYKEYFORFLUTTERFIRECITESTS0000 + GCM_SENDER_ID + 123456789012 + PLIST_VERSION + 1 + BUNDLE_ID + io.flutter.plugins.firebase.storage.example + PROJECT_ID + flutterfire-e2e-tests + GOOGLE_APP_ID + 1:123456789012:ios:0000000000000000000000 + STORAGE_BUCKET + flutterfire-e2e-tests.appspot.com + DATABASE_URL + https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app + + diff --git a/packages/firebase_storage/firebase_storage/example/lib/firebase_options.dart b/packages/firebase_storage/firebase_storage/example/lib/firebase_options.dart new file mode 100644 index 000000000000..a5a1360f6620 --- /dev/null +++ b/packages/firebase_storage/firebase_storage/example/lib/firebase_options.dart @@ -0,0 +1,61 @@ +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// Generated for CI builds and emulator tests. These are not real credentials. +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + return switch (defaultTargetPlatform) { + TargetPlatform.android => android, + TargetPlatform.iOS => ios, + TargetPlatform.macOS => macos, + TargetPlatform.windows => windows, + _ => throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ), + }; + } + + static const web = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:web:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + authDomain: 'flutterfire-e2e-tests.firebaseapp.com', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const android = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:android:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const ios = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:ios:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const macos = ios; + + static const windows = FirebaseOptions( + apiKey: 'dummy-api-key', + appId: '1:123456789012:web:0000000000000000000000', + messagingSenderId: '123456789012', + projectId: 'flutterfire-e2e-tests', + authDomain: 'flutterfire-e2e-tests.firebaseapp.com', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); +} diff --git a/packages/firebase_storage/firebase_storage/example/macos/Runner/GoogleService-Info.plist b/packages/firebase_storage/firebase_storage/example/macos/Runner/GoogleService-Info.plist new file mode 100644 index 000000000000..49ffc1c15f96 --- /dev/null +++ b/packages/firebase_storage/firebase_storage/example/macos/Runner/GoogleService-Info.plist @@ -0,0 +1,22 @@ + + + + + API_KEY + AIzaSyDUMMYKEYFORFLUTTERFIRECITESTS0000 + GCM_SENDER_ID + 123456789012 + PLIST_VERSION + 1 + BUNDLE_ID + io.flutter.plugins.firebase.storage.example + PROJECT_ID + flutterfire-e2e-tests + GOOGLE_APP_ID + 1:123456789012:ios:0000000000000000000000 + STORAGE_BUCKET + flutterfire-e2e-tests.appspot.com + DATABASE_URL + https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app + + diff --git a/packages/firebase_storage/firebase_storage/example/pubspec.yaml b/packages/firebase_storage/firebase_storage/example/pubspec.yaml index 3f7993404111..cd4cc7fb08e0 100755 --- a/packages/firebase_storage/firebase_storage/example/pubspec.yaml +++ b/packages/firebase_storage/firebase_storage/example/pubspec.yaml @@ -23,6 +23,9 @@ dev_dependencies: sdk: flutter integration_test: sdk: flutter + # `integration_test/report_test_results.dart` names each executed test via + # `package:test_api/hooks.dart`; `any` defers to the version flutter_test pins. + test_api: any flutter: uses-material-design: true diff --git a/packages/firebase_storage/firebase_storage/example/test_driver/integration_test.dart b/packages/firebase_storage/firebase_storage/example/test_driver/integration_test.dart index f1ac26f27b88..691723cbbfe1 100644 --- a/packages/firebase_storage/firebase_storage/example/test_driver/integration_test.dart +++ b/packages/firebase_storage/firebase_storage/example/test_driver/integration_test.dart @@ -2,6 +2,33 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'dart:io'; + import 'package:integration_test/integration_test_driver.dart'; -Future main() => integrationDriver(); +Future main() => integrationDriver( + writeResponseOnFailure: true, + responseDataCallback: (Map? data) async { + final results = + (data?['testResults'] as Map?)?.cast() ?? + const {}; + var passed = 0; + var failed = 0; + for (final entry in results.entries) { + final ok = entry.value == 'success'; + ok ? passed++ : failed++; + // ignore: avoid_print + print('${ok ? '✅' : '❌'} ${entry.key}'); + } + // ignore: avoid_print + print('Web e2e summary: $passed passed, $failed failed, ' + '${results.length} total'); + if (results.isEmpty) { + // ignore: avoid_print + print('[E] No tests reported by the app - treating as ' + 'infrastructure failure.'); + exit(1); + } + await writeResponseData(data); + }, + ); diff --git a/pubspec.yaml b/pubspec.yaml index 09edaef71924..0915387b4767 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -256,6 +256,55 @@ melos: description: | Run all e2e tests for cloud_functions. + test:e2e:firebase_ai: + run: | + cd packages/firebase_ai/firebase_ai/example + flutter test integration_test/e2e_test.dart + description: | + Run all e2e tests for firebase_ai. + + test:e2e:firebase_analytics: + run: | + cd packages/firebase_analytics/firebase_analytics/example + flutter test integration_test/e2e_test.dart + description: | + Run all e2e tests for firebase_analytics. + + test:e2e:firebase_app_check: + run: | + cd packages/firebase_app_check/firebase_app_check/example + flutter test integration_test/e2e_test.dart + description: | + Run all e2e tests for firebase_app_check. + + test:e2e:firebase_app_installations: + run: | + cd packages/firebase_app_installations/firebase_app_installations/example + flutter test integration_test/e2e_test.dart + description: | + Run all e2e tests for firebase_app_installations. + + test:e2e:firebase_crashlytics: + run: | + cd packages/firebase_crashlytics/firebase_crashlytics/example + flutter test integration_test/e2e_test.dart + description: | + Run all e2e tests for firebase_crashlytics. + + test:e2e:firebase_messaging: + run: | + cd packages/firebase_messaging/firebase_messaging/example + flutter test integration_test/e2e_test.dart + description: | + Run all e2e tests for firebase_messaging. + + test:e2e:firebase_ml_model_downloader: + run: | + cd packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example + flutter test integration_test/e2e_test.dart + description: | + Run all e2e tests for firebase_ml_model_downloader. + test:e2e:firebase_auth: run: | cd packages/firebase_auth/firebase_auth/example @@ -273,10 +322,17 @@ melos: test:e2e:firebase_performance: run: | cd packages/firebase_performance/firebase_performance/example - flutter test integration_test/firebase_performance_e2e_test.dart + flutter test integration_test/e2e_test.dart description: | Run all e2e tests for firebase_performance. + test:e2e:firebase_remote_config: + run: | + cd packages/firebase_remote_config/firebase_remote_config/example + flutter test integration_test/e2e_test.dart + description: | + Run all e2e tests for firebase_remote_config. + test:e2e:firebase_storage: run: | cd packages/firebase_storage/firebase_storage/example @@ -287,7 +343,7 @@ melos: test:e2e:web: run: | melos exec -c 1 --fail-fast -- \ - "flutter drive --target=./integration_test/e2e_test.dart --driver=./test_driver/integration_test.dart -d chrome --dart-define=LOCAL_WEB_E2E=true" + "flutter drive --target=./integration_test/e2e_test.dart --driver=./test_driver/integration_test.dart -d chrome" description: | Run all e2e tests on web platform. Please ensure you have "chromedriver" installed and running. packageFilters: @@ -309,6 +365,41 @@ melos: description: | Run all e2e tests for cloud_functions on web platform. Please ensure you have "chromedriver" installed and running. + test:e2e:web:firebase_ai: + run: | + cd packages/firebase_ai/firebase_ai/example + flutter drive --target=./integration_test/e2e_test.dart --driver=./test_driver/integration_test.dart -d chrome + description: | + Run all e2e tests for firebase_ai on web platform. Please ensure you have "chromedriver" installed and running. + + test:e2e:web:firebase_analytics: + run: | + cd packages/firebase_analytics/firebase_analytics/example + flutter drive --target=./integration_test/e2e_test.dart --driver=./test_driver/integration_test.dart -d chrome + description: | + Run all e2e tests for firebase_analytics on web platform. Please ensure you have "chromedriver" installed and running. + + test:e2e:web:firebase_app_check: + run: | + cd packages/firebase_app_check/firebase_app_check/example + flutter drive --target=./integration_test/e2e_test.dart --driver=./test_driver/integration_test.dart -d chrome + description: | + Run all e2e tests for firebase_app_check on web platform. Please ensure you have "chromedriver" installed and running. + + test:e2e:web:firebase_app_installations: + run: | + cd packages/firebase_app_installations/firebase_app_installations/example + flutter drive --target=./integration_test/e2e_test.dart --driver=./test_driver/integration_test.dart -d chrome + description: | + Run all e2e tests for firebase_app_installations on web platform. Please ensure you have "chromedriver" installed and running. + + test:e2e:web:firebase_messaging: + run: | + cd packages/firebase_messaging/firebase_messaging/example + flutter drive --target=./integration_test/e2e_test.dart --driver=./test_driver/integration_test.dart -d chrome + description: | + Run all e2e tests for firebase_messaging on web platform. Please ensure you have "chromedriver" installed and running. + test:e2e:web:firebase_auth: run: | cd packages/firebase_auth/firebase_auth/example @@ -323,6 +414,13 @@ melos: description: | Run all e2e tests for firebase_database on web platform. Please ensure you have "chromedriver" installed and running. + test:e2e:web:firebase_remote_config: + run: | + cd packages/firebase_remote_config/firebase_remote_config/example + flutter drive --target=./integration_test/e2e_test.dart --driver=./test_driver/integration_test.dart -d chrome + description: | + Run all e2e tests for firebase_remote_config on web platform. Please ensure you have "chromedriver" installed and running. + test:e2e:web:firebase_storage: run: | cd packages/firebase_storage/firebase_storage/example @@ -333,7 +431,7 @@ melos: test:e2e:web:firebase_performance: run: | cd packages/firebase_performance/firebase_performance/example - flutter drive --target=./integration_test/firebase_performance_e2e_test.dart --driver=./test_driver/integration_test.dart --release -d chrome + flutter drive --target=./integration_test/e2e_test.dart --driver=./test_driver/integration_test.dart --release -d chrome description: | Run all e2e tests for firebase_performance on web platform. Please ensure you have "chromedriver" installed and running. @@ -432,6 +530,7 @@ melos: --ignore "**/Runner/AppDelegate.swift" \ --ignore "**/Runner/main.m" \ --ignore "**/Runner/MainFlutterWindow.swift" \ + --ignore "**/Runner/SceneDelegate.swift" \ --ignore "**/Runner/Runner-Bridging-Header.h" \ --ignore "**/RunnerTests/RunnerTests.swift" \ . @@ -479,6 +578,7 @@ melos: --ignore "**/Runner/AppDelegate.swift" \ --ignore "**/Runner/main.m" \ --ignore "**/Runner/MainFlutterWindow.swift" \ + --ignore "**/Runner/SceneDelegate.swift" \ --ignore "**/Runner/Runner-Bridging-Header.h" \ --ignore "**/RunnerTests/RunnerTests.swift" \ . diff --git a/tests/integration_test/core_shard_test.dart b/tests/integration_test/core_shard_test.dart new file mode 100644 index 000000000000..9fadd747a213 --- /dev/null +++ b/tests/integration_test/core_shard_test.dart @@ -0,0 +1,27 @@ +// Copyright 2022, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// CI entrypoint for the `tests` app; `e2e_test.dart` is the identical +// local-run and Windows target. +// +// It holds firebase_core, the only suite left here: every other product now +// runs its own suite from its own example app. It doubles as the coexistence +// smoke test - the `tests` app depends on every plugin, so building this target +// still proves they all compile and link together, which no single-product +// example can show. + +import 'package:integration_test/integration_test.dart'; + +import 'firebase_core/firebase_core_e2e_test.dart' as firebase_core; +import 'report_test_results.dart'; + +void main() { + // `firebase_core.main()` calls `ensureInitialized()` itself, but the hook has + // to be registered before the suite declares its groups, so initialize here + // too - `ensureInitialized()` is idempotent and returns the same binding. + final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + reportTestResultsToDriver(binding); + + firebase_core.main(); +} diff --git a/tests/integration_test/e2e_test.dart b/tests/integration_test/e2e_test.dart index ec7b49bf926d..30b878f5feee 100644 --- a/tests/integration_test/e2e_test.dart +++ b/tests/integration_test/e2e_test.dart @@ -2,93 +2,33 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -import 'package:flutter/foundation.dart'; +// The `tests` app used to aggregate every product's e2e suite. All thirteen +// products now own their suite inside their own example app, each driven by its +// own path-filtered `.github/workflows/e2e_tests_.yaml`. +// +// What is left here is the all-plugins coexistence smoke test: `tests` still +// depends on every plugin (see `tests/pubspec.yaml`), so building and running +// this target proves the whole set still compiles, links and boots together - +// something no single-product example can show. firebase_core is the suite it +// runs, because it is the only one with no live backend behind it. +// +// `core_shard_test.dart` is the same thing under the name CI uses; this +// file stays as the local-run entrypoint (`flutter test integration_test`) and +// as the Windows target. + import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'firebase_ai/firebase_ai_e2e_test.dart' as firebase_ai; -import 'firebase_analytics/firebase_analytics_e2e_test.dart' - as firebase_analytics; -import 'firebase_app_check/firebase_app_check_e2e_test.dart' - as firebase_app_check; -import 'firebase_app_installations/firebase_app_installations_e2e_test.dart' - as firebase_app_installations; import 'firebase_core/firebase_core_e2e_test.dart' as firebase_core; -import 'firebase_crashlytics/firebase_crashlytics_e2e_test.dart' - as firebase_crashlytics; -import 'firebase_messaging/firebase_messaging_e2e_test.dart' - as firebase_messaging; -import 'firebase_ml_model_downloader/firebase_ml_model_downloader_e2e_test.dart' - as firebase_ml_model_downloader; -import 'firebase_performance/firebase_performance_e2e_test.dart' - as firebase_performance; -import 'firebase_remote_config/firebase_remote_config_e2e_test.dart' - as firebase_remote_config; - -// Github Actions environment variable -// ignore: do_not_use_environment -final isCI = const String.fromEnvironment('CI').isNotEmpty; +import 'report_test_results.dart'; void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('FlutterFire', () { - // ignore: do_not_use_environment - if (const String.fromEnvironment('LOCAL_WEB_E2E') == 'true') { - // for running web e2e locally which doesn't suffer throttling issues - runAllTests(); - return; - } - - // ignore: do_not_use_environment - if (const String.fromEnvironment('APP_CHECK_E2E') == 'true') { - // app check has been separated out for web due to throttling issues - firebase_app_check.main(); - return; - } - if (kIsWeb) { - // Web has its own ordering because App Check runs in a separate job. - firebase_core.main(); - firebase_ai.main(); - firebase_crashlytics.main(); - firebase_analytics.main(); - firebase_app_installations.main(); - firebase_messaging.main(); - firebase_ml_model_downloader.main(); - firebase_performance.main(); - firebase_remote_config.main(); - return; - } + final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + reportTestResultsToDriver(binding); - switch (defaultTargetPlatform) { - case TargetPlatform.android: - case TargetPlatform.iOS: - case TargetPlatform.macOS: - runAllTests(); - break; - case TargetPlatform.windows: - firebase_core.main(); - firebase_remote_config.main(); - firebase_app_check.main(); - break; - default: - throw UnsupportedError( - '$defaultTargetPlatform is not supported on FlutterFire E2E tests', - ); - } - }); + group('FlutterFire', runAllTests); } void runAllTests() { - // Native platforms run the full suite in package order. firebase_core.main(); - firebase_ai.main(); - firebase_crashlytics.main(); - firebase_analytics.main(); - firebase_app_installations.main(); - firebase_messaging.main(); - firebase_ml_model_downloader.main(); - firebase_performance.main(); - firebase_remote_config.main(); - firebase_app_check.main(); } diff --git a/tests/integration_test/report_test_results.dart b/tests/integration_test/report_test_results.dart new file mode 100644 index 000000000000..fb80e3ba19f7 --- /dev/null +++ b/tests/integration_test/report_test_results.dart @@ -0,0 +1,40 @@ +// Copyright 2022, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/common.dart' show Failure; +import 'package:integration_test/integration_test.dart'; +import 'package:test_api/hooks.dart' show TestHandle; + +/// Records every executed test via the package:test lifecycle (plain `test()` +/// declarations never reach the binding's `results` map - only `testWidgets` +/// does) and publishes them through +/// [IntegrationTestWidgetsFlutterBinding.reportData], the only channel that +/// crosses the wire on web. +void reportTestResultsToDriver(IntegrationTestWidgetsFlutterBinding binding) { + final executed = []; + setUp(() { + executed.add(TestHandle.current.name); + }); + tearDownAll(() { + // `TestHandle.current.name` is the full name, prefixed with the enclosing + // group names, while `binding.results` is keyed by the bare `testWidgets` + // description - so match a failed entry on the suffix as well as on + // equality, otherwise no grouped test could ever be marked failed. + final failures = [ + for (final entry in binding.results.entries) + if (entry.value is Failure) entry.key, + ]; + bool didFail(String name) => failures + .any((failure) => name == failure || name.endsWith(' $failure')); + + binding.reportData ??= {}; + binding.reportData!['testResults'] = { + // Plain test() failures are not individually attributable here, but they + // fail the run as a whole via allTestsPassed; testWidgets failures are + // attributed from the binding's results map. + for (final name in executed) name: didFail(name) ? 'failed' : 'success', + }; + }); +} diff --git a/tests/integration_test/shards/core_misc_shard_test.dart b/tests/integration_test/shards/core_misc_shard_test.dart deleted file mode 100644 index 85f1848bbcd2..000000000000 --- a/tests/integration_test/shards/core_misc_shard_test.dart +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2022, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -// CI shard entrypoint. `e2e_test.dart` still runs every suite in one process -// (Windows and local runs use it); CI splits that run across shards so a hang -// or flake costs one small job instead of the whole suite. -// -// This shard collects every suite that is not Auth, Storage, Database or -// Functions. The suites run in the same relative order as -// `e2e_test.dart`'s `runAllTests()`, since they still share one process here. - -import 'package:flutter/foundation.dart'; - -import '../firebase_ai/firebase_ai_e2e_test.dart' as firebase_ai; -import '../firebase_analytics/firebase_analytics_e2e_test.dart' - as firebase_analytics; -import '../firebase_app_check/firebase_app_check_e2e_test.dart' - as firebase_app_check; -import '../firebase_app_installations/firebase_app_installations_e2e_test.dart' - as firebase_app_installations; -import '../firebase_core/firebase_core_e2e_test.dart' as firebase_core; -import '../firebase_crashlytics/firebase_crashlytics_e2e_test.dart' - as firebase_crashlytics; -import '../firebase_messaging/firebase_messaging_e2e_test.dart' - as firebase_messaging; -import '../firebase_ml_model_downloader/firebase_ml_model_downloader_e2e_test.dart' - as firebase_ml_model_downloader; -import '../firebase_performance/firebase_performance_e2e_test.dart' - as firebase_performance; -import '../firebase_remote_config/firebase_remote_config_e2e_test.dart' - as firebase_remote_config; - -void main() { - firebase_core.main(); - firebase_ai.main(); - firebase_crashlytics.main(); - firebase_analytics.main(); - firebase_app_installations.main(); - firebase_messaging.main(); - firebase_ml_model_downloader.main(); - firebase_performance.main(); - firebase_remote_config.main(); - - if (!kIsWeb) { - // App Check is throttled on web, so web runs it in its own job - // (`web-app-check`, driven by the APP_CHECK_E2E dart-define). Matches the - // `kIsWeb` branch of `e2e_test.dart`, which also leaves it out. - firebase_app_check.main(); - } -} diff --git a/tests/pubspec.yaml b/tests/pubspec.yaml index f3475110b9ef..e8735c1a8ed6 100644 --- a/tests/pubspec.yaml +++ b/tests/pubspec.yaml @@ -56,6 +56,9 @@ dev_dependencies: sdk: flutter integration_test: sdk: flutter + # `integration_test/report_test_results.dart` names each executed test via + # `package:test_api/hooks.dart`; `any` defers to the version flutter_test pins. + test_api: any flutter: uses-material-design: true diff --git a/tests/test_driver/integration_test.dart b/tests/test_driver/integration_test.dart index f1ac26f27b88..691723cbbfe1 100644 --- a/tests/test_driver/integration_test.dart +++ b/tests/test_driver/integration_test.dart @@ -2,6 +2,33 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'dart:io'; + import 'package:integration_test/integration_test_driver.dart'; -Future main() => integrationDriver(); +Future main() => integrationDriver( + writeResponseOnFailure: true, + responseDataCallback: (Map? data) async { + final results = + (data?['testResults'] as Map?)?.cast() ?? + const {}; + var passed = 0; + var failed = 0; + for (final entry in results.entries) { + final ok = entry.value == 'success'; + ok ? passed++ : failed++; + // ignore: avoid_print + print('${ok ? '✅' : '❌'} ${entry.key}'); + } + // ignore: avoid_print + print('Web e2e summary: $passed passed, $failed failed, ' + '${results.length} total'); + if (results.isEmpty) { + // ignore: avoid_print + print('[E] No tests reported by the app - treating as ' + 'infrastructure failure.'); + exit(1); + } + await writeResponseData(data); + }, + );