diff --git a/.github/workflows/e2e_tests_fdc.yaml b/.github/workflows/e2e_tests_fdc.yaml index 4810a0304fb1..3f402dc497db 100644 --- a/.github/workflows/e2e_tests_fdc.yaml +++ b/.github/workflows/e2e_tests_fdc.yaml @@ -52,18 +52,15 @@ jobs: - name: 'Install Tools' run: | sudo npm i -g firebase-tools - echo "FIREBASE_TOOLS_VERSION=$(npm firebase --version)" >> $GITHUB_ENV + echo "FIREBASE_TOOLS_VERSION=$(firebase --version)" >> $GITHUB_ENV - name: Firebase Emulator Cache id: firebase-emulator-cache uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 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-v3-${{ env.FIREBASE_TOOLS_VERSION }} - restore-keys: firebase-emulators-v3 + key: firebase-emulators-v4-${{ runner.os }}-${{ env.FIREBASE_TOOLS_VERSION }} - uses: subosito/flutter-action@f2c4f6686ca8e8d6e6d0f28410eeef506ed66aff with: channel: 'stable' @@ -81,6 +78,7 @@ jobs: - name: Start Firebase Emulator run: | cd ./packages/firebase_data_connect/firebase_data_connect/example + pkill -x postgres || true unset PGSERVICEFILE firebase experiments:enable dataconnect ./start-firebase-emulator.sh @@ -136,8 +134,6 @@ jobs: uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 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 @@ -179,18 +175,15 @@ jobs: - name: 'Install Tools' run: | sudo npm i -g firebase-tools - echo "FIREBASE_TOOLS_VERSION=$(npm firebase --version)" >> $GITHUB_ENV + echo "FIREBASE_TOOLS_VERSION=$(firebase --version)" >> $GITHUB_ENV - name: Firebase Emulator Cache id: firebase-emulator-cache uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 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-v3-${{ env.FIREBASE_TOOLS_VERSION }} - restore-keys: firebase-emulators-v3 + key: firebase-emulators-v4-${{ runner.os }}-${{ env.FIREBASE_TOOLS_VERSION }} - uses: subosito/flutter-action@f2c4f6686ca8e8d6e6d0f28410eeef506ed66aff with: channel: 'stable' @@ -218,6 +211,7 @@ jobs: run: | sudo chown -R 501:20 "/Users/runner/.npm" cd ./packages/firebase_data_connect/firebase_data_connect/example + pkill -x postgres || true unset PGSERVICEFILE firebase experiments:enable dataconnect ./start-firebase-emulator.sh @@ -232,10 +226,9 @@ jobs: run: | # Uncomment following line to have simulator logs printed out for debugging purposes. # xcrun simctl spawn booted log stream --predicate 'eventMessage contains "flutter"' & - # The iOS simulator sometimes fails to connect the VM Service, hanging for - # 12 minutes before timing out. Use a 6-minute limit and retry once with - # a simulator reboot. Normal connection takes 30s-5min. - perl -e 'alarm 360; exec @ARGV' -- flutter test integration_test/e2e_test.dart -d "$SIMULATOR" --dart-define=CI=true --timeout 10x || { + # The iOS simulator sometimes fails to connect the VM Service. Keep a + # limit around the full test command and retry once with a simulator reboot. + perl -e 'alarm 900; exec @ARGV' -- flutter test integration_test/e2e_test.dart -d "$SIMULATOR" --dart-define=CI=true --timeout 10x || { echo "First attempt failed or timed out. Rebooting simulator and retrying..." xcrun simctl shutdown "$SIMULATOR" || true xcrun simctl boot "$SIMULATOR" @@ -248,8 +241,6 @@ jobs: uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 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 @@ -295,22 +286,20 @@ jobs: - name: 'Install Tools' run: | sudo npm i -g firebase-tools - echo "FIREBASE_TOOLS_VERSION=$(npm firebase --version)" >> $GITHUB_ENV + echo "FIREBASE_TOOLS_VERSION=$(firebase --version)" >> $GITHUB_ENV - name: Firebase Emulator Cache id: firebase-emulator-cache uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 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-v3-${{ env.FIREBASE_TOOLS_VERSION }} - restore-keys: firebase-emulators-v3 + key: firebase-emulators-v4-${{ runner.os }}-${{ env.FIREBASE_TOOLS_VERSION }} - name: Start Firebase Emulator run: | sudo chown -R 501:20 "/Users/runner/.npm" cd ./packages/firebase_data_connect/firebase_data_connect/example + pkill -x postgres || true unset PGSERVICEFILE firebase experiments:enable dataconnect ./start-firebase-emulator.sh @@ -336,8 +325,6 @@ jobs: uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 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 @@ -374,21 +361,19 @@ jobs: - name: 'Install Tools' run: | sudo npm i -g firebase-tools - echo "FIREBASE_TOOLS_VERSION=$(npm firebase --version)" >> $GITHUB_ENV + echo "FIREBASE_TOOLS_VERSION=$(firebase --version)" >> $GITHUB_ENV - name: Firebase Emulator Cache id: firebase-emulator-cache uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 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-v3-${{ env.FIREBASE_TOOLS_VERSION }} - restore-keys: firebase-emulators-v3 + key: firebase-emulators-v4-${{ runner.os }}-${{ env.FIREBASE_TOOLS_VERSION }} - name: Start Firebase Emulator run: | cd ./packages/firebase_data_connect/firebase_data_connect/example + pkill -x postgres || true unset PGSERVICEFILE firebase experiments:enable dataconnect ./start-firebase-emulator.sh @@ -415,8 +400,6 @@ jobs: uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 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 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 bae5432a6b96..78e604c432cf 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 @@ -16,6 +16,36 @@ import 'listen_e2e.dart'; import 'query_e2e.dart'; import 'websocket_e2e.dart'; +Future _signInTestUser() async { + final auth = FirebaseAuth.instance; + const password = 'password'; + final email = 'fdc-test-${DateTime.now().microsecondsSinceEpoch}@mail.com'; + + for (var attempt = 0; attempt < 5; attempt++) { + try { + await auth.createUserWithEmailAndPassword( + email: email, + password: password, + ); + return; + } on FirebaseAuthException catch (e) { + if (e.code == 'email-already-in-use') { + await auth.signInWithEmailAndPassword( + email: email, + password: password, + ); + return; + } + + if (attempt == 4) { + rethrow; + } + } + + await Future.delayed(Duration(seconds: attempt + 1)); + } +} + void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -31,8 +61,7 @@ void main() { .useDataConnectEmulator('127.0.0.1', 9399); await FirebaseAuth.instance.useAuthEmulator('127.0.0.1', 9099); - await FirebaseAuth.instance.createUserWithEmailAndPassword( - email: 'test@mail.com', password: 'password'); + await _signInTestUser(); }); runInstanceTests(); diff --git a/packages/firebase_data_connect/firebase_data_connect/example/integration_test/websocket_e2e.dart b/packages/firebase_data_connect/firebase_data_connect/example/integration_test/websocket_e2e.dart index 930c7d4da0c2..a105b4783184 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/integration_test/websocket_e2e.dart +++ b/packages/firebase_data_connect/firebase_data_connect/example/integration_test/websocket_e2e.dart @@ -20,6 +20,18 @@ import 'package:flutter_test/flutter_test.dart'; import 'query_e2e.dart'; // For deleteAllMovies +const _streamTimeout = Duration(seconds: 30); + +Future _waitForStreamEvent(Future future, String description) { + return future.timeout( + _streamTimeout, + onTimeout: () => throw TimeoutException( + 'Timed out waiting for $description', + _streamTimeout, + ), + ); +} + void runWebSocketTests() { group( '$FirebaseDataConnect WebSocketTransport', @@ -44,9 +56,9 @@ void runWebSocketTests() { .subscribe() .listen((value) { if (count1 == 0) { - ready1.complete(); + if (!ready1.isCompleted) ready1.complete(); } else { - update1.complete(); + if (!update1.isCompleted) update1.complete(); } count1++; }); @@ -57,44 +69,57 @@ void runWebSocketTests() { .subscribe() .listen((value) { if (count2 == 0) { - ready2.complete(); + if (!ready2.isCompleted) ready2.complete(); } else { - update2.complete(); + if (!update2.isCompleted) update2.complete(); } count2++; }); - // Wait for both to be ready - await ready1.future; - await ready2.future; - - // Create movies - await MoviesConnector.instance - .createMovie( - genre: 'Action', - title: 'The Matrix', - releaseYear: 1999, - ) - .rating(4.5) - .ref() - .execute(); - - await MoviesConnector.instance - .createMovie( - genre: 'Drama', - title: 'Titanic', - releaseYear: 1997, - ) - .rating(4.8) - .ref() - .execute(); - - // Wait for updates - await update1.future; - await update2.future; - - await sub1.cancel(); - await sub2.cancel(); + try { + // Wait for both to be ready + await _waitForStreamEvent(ready1.future, 'Matrix subscription'); + await _waitForStreamEvent(ready2.future, 'Titan subscription'); + + // Create movies + await MoviesConnector.instance + .createMovie( + genre: 'Action', + title: 'The Matrix', + releaseYear: 1999, + ) + .rating(4.5) + .ref() + .execute(); + + await MoviesConnector.instance + .createMovie( + genre: 'Drama', + title: 'Titanic', + releaseYear: 1997, + ) + .rating(4.8) + .ref() + .execute(); + + // Explicitly resume each active query so this test does not depend on + // emulator-side push timing. + await MoviesConnector.instance + .listMoviesByPartialTitle(input: 'Matrix') + .ref() + .execute(fetchPolicy: QueryFetchPolicy.serverOnly); + await MoviesConnector.instance + .listMoviesByPartialTitle(input: 'Titan') + .ref() + .execute(fetchPolicy: QueryFetchPolicy.serverOnly); + + // Wait for updates + await _waitForStreamEvent(update1.future, 'Matrix update'); + await _waitForStreamEvent(update2.future, 'Titan update'); + } finally { + await sub1.cancel(); + await sub2.cancel(); + } }); testWidgets( @@ -110,36 +135,38 @@ void runWebSocketTests() { .subscribe() .listen((value) { if (count == 0) { - isReady.complete(); + if (!isReady.isCompleted) isReady.complete(); } count++; }); - await isReady.future; - - // Now perform a query, which should go over WebSocket if connected - final result = - await MoviesConnector.instance.listMovies().ref().execute(); - expect(result.data.movies.length, 0); - - // Perform a mutation - await MoviesConnector.instance - .createMovie( - genre: 'Action', - title: 'Inception', - releaseYear: 2010, - ) - .rating(4.9) - .ref() - .execute(); - - // Verify update via query - final result2 = - await MoviesConnector.instance.listMovies().ref().execute(); - expect(result2.data.movies.length, 1); - expect(result2.data.movies[0].title, 'Inception'); - - await sub.cancel(); + try { + await _waitForStreamEvent(isReady.future, 'listMovies subscription'); + + // Now perform a query, which should go over WebSocket if connected + final result = + await MoviesConnector.instance.listMovies().ref().execute(); + expect(result.data.movies.length, 0); + + // Perform a mutation + await MoviesConnector.instance + .createMovie( + genre: 'Action', + title: 'Inception', + releaseYear: 2010, + ) + .rating(4.9) + .ref() + .execute(); + + // Verify update via query + final result2 = + await MoviesConnector.instance.listMovies().ref().execute(); + expect(result2.data.movies.length, 1); + expect(result2.data.movies[0].title, 'Inception'); + } finally { + await sub.cancel(); + } }); testWidgets('should stop receiving events after cancel', @@ -154,14 +181,14 @@ void runWebSocketTests() { .subscribe() .listen((value) { if (count == 0) { - isReady.complete(); + if (!isReady.isCompleted) isReady.complete(); } else { - receivedUpdate.complete(); + if (!receivedUpdate.isCompleted) receivedUpdate.complete(); } count++; }); - await isReady.future; + await _waitForStreamEvent(isReady.future, 'listMovies subscription'); // Cancel the subscription await sub.cancel(); @@ -200,12 +227,12 @@ void runWebSocketTests() { .subscribe() .listen((value) { if (count == 0) { - isReady.complete(); + if (!isReady.isCompleted) isReady.complete(); } count++; }); - await isReady.future; + await _waitForStreamEvent(isReady.future, 'listMovies subscription'); final dataConnect = MoviesConnector.instance.dataConnect; final transport = (dataConnect as dynamic).transport; diff --git a/packages/firebase_data_connect/firebase_data_connect/example/start-firebase-emulator.sh b/packages/firebase_data_connect/firebase_data_connect/example/start-firebase-emulator.sh index 3821f46e402a..ddf9dde87cf0 100755 --- a/packages/firebase_data_connect/firebase_data_connect/example/start-firebase-emulator.sh +++ b/packages/firebase_data_connect/firebase_data_connect/example/start-firebase-emulator.sh @@ -1,5 +1,44 @@ #!/bin/bash -firebase emulators:start --project flutterfire-e2e-tests & -# Added below to fix the e2e tests -# npx "firebase/firebase-tools#mtewani/dart-bugbash" emulators:start --project flutterfire-e2e-tests & -sleep 30 \ No newline at end of file +set -euo pipefail + +LOG_FILE="${TMPDIR:-/tmp}/flutterfire-fdc-emulators.log" +rm -f "$LOG_FILE" + +print_emulator_logs() { + cat "$LOG_FILE" + + if [ -f firebase-debug.log ]; then + echo + echo "firebase-debug.log:" + cat firebase-debug.log + fi + + if [ -f dataconnect-debug.log ]; then + echo + echo "dataconnect-debug.log:" + cat dataconnect-debug.log + fi +} + +firebase emulators:start --project flutterfire-e2e-tests >"$LOG_FILE" 2>&1 & +FIREBASE_PID=$! + +for _ in {1..90}; do + if ! kill -0 "$FIREBASE_PID" 2>/dev/null; then + echo "Firebase emulators exited before becoming ready." + print_emulator_logs + wait "$FIREBASE_PID" + exit 1 + fi + + if grep -q "All emulators ready" "$LOG_FILE"; then + print_emulator_logs + exit 0 + fi + + sleep 1 +done + +echo "Timed out waiting for Firebase emulators to become ready." +print_emulator_logs +exit 1