diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index f058bf422..b4c2ea074 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -162,9 +162,13 @@ jobs: needs: [diff_check] if: ${{ needs.diff_check.outputs.skip_ci != 'true' }} env: - SENTRY_DISABLE_AUTO_UPLOAD: 'true' # TODO: Remove this when testing against a mocked Sentry server - RN_SENTRY_POD_NAME: RNSentry - RN_DIFF_REPOSITORY: https://github.com/react-native-community/rn-diff-purge.git + RN_VERSION: ${{ matrix.rn-version }} + RN_ENGINE: ${{ matrix.engine }} + USE_FRAMEWORKS: ${{ matrix.ios-use-frameworks }} + PRODUCTION: ${{ matrix.build-type == 'production' && '1' || '0' }} + RCT_NEW_ARCH_ENABLED: ${{ matrix.rn-architecture == 'new' && '1' || '0' }} + IOS_RUNTIME: ${{ matrix.runtime }} + IOS_DEVICE: ${{ matrix.device }} strategy: fail-fast: false # keeps matrix running if one fails matrix: @@ -240,15 +244,8 @@ jobs: node-version: 18 - uses: actions/setup-java@v4 - if: ${{ matrix.rn-version == '0.65.3' }} with: - java-version: '11' - distribution: 'adopt' - - - uses: actions/setup-java@v4 - if: ${{ matrix.rn-version != '0.65.3' }} - with: - java-version: '17' + java-version: ${{ matrix.rn-version == '0.65.3' && '11' || '17' }} distribution: 'adopt' - name: Gradle cache @@ -277,56 +274,21 @@ jobs: key: ${{ github.workflow }}-${{ github.job }}-npm-${{ hashFiles('test/e2e/yarn.lock') }} - name: Install SDK JS Dependencies - if: ${{ steps.deps-cache.outputs['cache-hit'] != 'true' }} + if: steps.deps-cache.outputs['cache-hit'] != 'true' run: yarn install - name: Install E2E Tests Library JS Dependencies - if: steps.deps-cache.outputs['deps-cache-e2e-library'] != 'true' + if: steps.deps-cache-e2e-library.outputs['cache-hit'] != 'true' working-directory: test/e2e run: yarn install - - name: Build SDK - run: yarn build - - - name: Build E2E Tests Library - working-directory: test/e2e - run: yarn build - - - name: Package SDK - run: yalc publish - - uses: actions/setup-node@v4 if: ${{ matrix.rn-version == '0.65.3' }} with: node-version: 16 - - name: Download Plain RN ${{ matrix.rn-version }} App - working-directory: test/react-native/versions - run: git clone $RN_DIFF_REPOSITORY --branch release/${{ matrix.rn-version }} --single-branch ${{ matrix.rn-version }} - - - name: Add SDK to App - working-directory: test/react-native/versions/${{ matrix.rn-version }}/RnDiffApp - run: yalc add @sentry/react-native - - - name: Install App JS Dependencies (yarn v1) - working-directory: test/react-native/versions/${{ matrix.rn-version }}/RnDiffApp - if: ${{ matrix.rn-version != '0.73.9' }} - run: | - yarn install - - - name: Install App JS Dependencies (yarn v3) - working-directory: test/react-native/versions/${{ matrix.rn-version }}/RnDiffApp - if: ${{ matrix.rn-version == '0.73.9' }} - run: | - rm -f .yarnrc.yml # original yarnrc contains the exact yarn version which causes corepack to fail to install yarn v3 - echo "nodeLinker: node-modules" > .yarnrc.yml # RN build script require dependencies to be present in node_modules - touch yarn.lock # yarn v3 won't install dependencies in a sub project without a yarn.lock file present - export YARN_ENABLE_IMMUTABLE_INSTALLS=false # yarn v3 run immutable install by default in CI - yarn install - - - name: Add E2E Tests Library to App - working-directory: test/react-native/versions/${{ matrix.rn-version }}/RnDiffApp - run: yarn add ../../../../e2e + - name: Setup Plain RN ${{ matrix.rn-version }} App + run: ./scripts/e2e.mjs ${{ matrix.platform }} --create - uses: ruby/setup-ruby@v1 if: ${{ matrix.platform == 'ios' }} @@ -335,113 +297,16 @@ jobs: ruby-version: '3.3.0' # based on what is used in the sample bundler-cache: true # runs 'bundle install' and caches installed gems automatically cache-version: 1 # cache the installed gems - - run: gem install cocoapods -v 1.15.2 # fixes Hermes pod install https://github.com/CocoaPods/CocoaPods/issues/12226#issuecomment-1930604302 - working-directory: test/react-native/versions/${{ matrix.rn-version }}/RnDiffApp - if: ${{ matrix.platform == 'ios' }} - - - name: Install App Pods - if: ${{ matrix.platform == 'ios' }} - working-directory: test/react-native/versions/${{ matrix.rn-version }}/RnDiffApp/ios - run: | - ../../../../rn.patch.podfile.js --pod-file Podfile --engine ${{ matrix.engine }} - export NO_FLIPPER=1 # Flipper is causing build issues on iOS, so we disable it - [[ "${{ matrix.ios-use-frameworks }}" == "static" ]] && export USE_FRAMEWORKS=static - [[ "${{ matrix.ios-use-frameworks }}" == "dynamic" ]] && export USE_FRAMEWORKS=dynamic - [[ "${{ matrix.build-type }}" == "production" ]] && ENABLE_PROD=1 || ENABLE_PROD=0 - [[ "${{ matrix.rn-architecture }}" == "new" ]] && ENABLE_NEW_ARCH=1 || ENABLE_NEW_ARCH=0 - [[ "${{ matrix.rn-version }}" == "0.65.3" ]] && POD_INSTALL_COMMNAND="pod install" || POD_INSTALL_COMMNAND="bundle exec pod install" - echo "ENABLE_PROD=$ENABLE_PROD" - echo "ENABLE_NEW_ARCH=$ENABLE_NEW_ARCH" - echo "USE_FRAMEWORKS=$USE_FRAMEWORKS" - PRODUCTION=$ENABLE_PROD RCT_NEW_ARCH_ENABLED=$ENABLE_NEW_ARCH $POD_INSTALL_COMMNAND - cat Podfile.lock | grep $RN_SENTRY_POD_NAME - - - name: Patch App RN - working-directory: test/react-native/versions/${{ matrix.rn-version }}/RnDiffApp - run: | - patch --verbose --strip=0 --force --ignore-whitespace --fuzz 4 < ../../../rn.patch - ../../../rn.patch.app.js --app . - ../../../rn.patch.metro.config.js --path metro.config.js - - - name: Patch Android App RN - if: ${{ matrix.platform == 'android' }} - working-directory: test/react-native/versions/${{ matrix.rn-version }}/RnDiffApp - run: | - ../../../rn.patch.gradle.properties.js --gradle-properties android/gradle.properties --engine ${{ matrix.engine }} - ../../../rn.patch.app.build.gradle.js --app-build-gradle android/app/build.gradle - - name: Patch iOS App RN - if: ${{ matrix.platform == 'ios' }} - working-directory: test/react-native/versions/${{ matrix.rn-version }}/RnDiffApp - run: | - ../../../rn.patch.xcode.js \ - --project ios/RnDiffApp.xcodeproj/project.pbxproj \ - --rn-version '${{ matrix.rn-version }}' - - # This prevents modules resolution from outside of the RN Test App projects - # during the native app build - - name: Clean SDK node_modules - run: rm -rf node_modules + - name: Build Plain RN ${{ matrix.rn-version }} App + run: ./scripts/e2e.mjs ${{ matrix.platform }} --build - - name: Build Android App - if: ${{ matrix.platform == 'android' }} - working-directory: test/react-native/versions/${{ matrix.rn-version }}/RnDiffApp/android - run: | - if [[ ${{ matrix.rn-architecture }} == 'new' ]]; then - perl -i -pe's/newArchEnabled=false/newArchEnabled=true/g' gradle.properties - echo 'New Architecture enabled' - fi - [[ "${{ matrix.build-type }}" == "production" ]] && CONFIG='Release' || CONFIG='Debug' - echo "Building $CONFIG" - ./gradlew ":app:assemble$CONFIG" -PreactNativeArchitectures=x86 - - - name: Build iOS App - if: ${{ matrix.platform == 'ios' }} - working-directory: test/react-native/versions/${{ matrix.rn-version }}/RnDiffApp/ios - run: | - [[ "${{ matrix.build-type }}" == "production" ]] && CONFIG='Release' || CONFIG='Debug' - echo "Building $CONFIG" - mkdir -p "DerivedData" - derivedData="$(cd "DerivedData" ; pwd -P)" - set -o pipefail && xcodebuild \ - -workspace RnDiffApp.xcworkspace \ - -configuration "$CONFIG" \ - -scheme RnDiffApp \ - -destination 'platform=iOS Simulator,OS=${{ matrix.runtime }},name=${{ matrix.device }}' \ - ONLY_ACTIVE_ARCH=yes \ - -derivedDataPath "$derivedData" \ - build \ - | tee xcodebuild.log \ - | xcbeautify --quieter --is-ci --disable-colored-output - - - name: Archive Android APK - if: matrix.platform == 'android' && matrix.build-type == 'production' - run: | - BUILD_PATH=test/react-native/versions/${{ matrix.rn-version }}/RnDiffApp/android/app/build/outputs/apk/release - BUILD_NAME=app-release.apk - tar -cvf apk.tar -C $BUILD_PATH $BUILD_NAME - - - name: Archive iOS APP - if: matrix.platform == 'ios' && matrix.build-type == 'production' - run: | - BUILD_PATH=test/react-native/versions/${{ matrix.rn-version }}/RnDiffApp/ios/DerivedData/Build/Products/Release-iphonesimulator - BUILD_NAME=RnDiffApp.app - tar -cvf app.tar -C $BUILD_PATH $BUILD_NAME - - - name: Upload App APK - if: matrix.platform == 'android' && matrix.build-type == 'production' - uses: actions/upload-artifact@v4 - with: - name: ${{ matrix.rn-version }}-${{ matrix.rn-architecture }}-${{ matrix.engine }}-${{ matrix.platform }}-${{ matrix.build-type }}-${{ matrix.ios-use-frameworks }}-app-package - path: apk.tar - retention-days: 1 - - - name: Upload App APP - if: matrix.platform == 'ios' && matrix.build-type == 'production' + - name: Upload App + if: matrix.build-type == 'production' uses: actions/upload-artifact@v4 with: name: ${{ matrix.rn-version }}-${{ matrix.rn-architecture }}-${{ matrix.engine }}-${{ matrix.platform }}-${{ matrix.build-type }}-${{ matrix.ios-use-frameworks }}-app-package - path: app.tar + path: test/e2e/RnDiffApp.ap* retention-days: 1 - name: Upload logs @@ -511,10 +376,6 @@ jobs: name: ${{ matrix.rn-version }}-${{ matrix.rn-architecture }}-${{ matrix.engine }}-${{ matrix.platform }}-${{ matrix.build-type }}-${{ matrix.ios-use-frameworks }}-app-package path: test/e2e - - name: Extract App Package - working-directory: test/e2e - run: tar -xvf *.tar - - uses: actions/setup-node@v4 with: node-version: 20 @@ -547,47 +408,14 @@ jobs: key: ${{ github.workflow }}-${{ github.job }}-npm-${{ hashFiles('test/e2e/yarn.lock') }} - name: Install E2E Tests Library JS Dependencies - if: steps.deps-cache.outputs['deps-cache-e2e-library'] != 'true' + if: steps.deps-cache-e2e-library.outputs['cache-hit'] != 'true' working-directory: test/e2e run: yarn install - - name: Build iOS WebDriverAgent - if: matrix.platform == 'ios' - working-directory: test/e2e - run: | - mkdir -p "DerivedData" - derivedData="$(cd "DerivedData" ; pwd -P)" - set -o pipefail && xcodebuild \ - -project node_modules/appium-webdriveragent/WebDriverAgent.xcodeproj \ - -scheme WebDriverAgentRunner \ - GCC_TREAT_WARNINGS_AS_ERRORS=0 \ - COMPILER_INDEX_STORE_ENABLE=NO \ - -destination 'platform=iOS Simulator,OS=${{ matrix.runtime }},name=${{ matrix.device }}' \ - ONLY_ACTIVE_ARCH=yes \ - -derivedDataPath "$derivedData" \ - build \ - | tee xcodebuild.log \ - | xcbeautify --quieter --is-ci --disable-colored-output - - - name: Start Appium Server - working-directory: test/e2e - run: yarn run appium --log-timestamp --log-no-colors --log appium.${{ matrix.platform }}.log & - - # Wait until the Appium server starts. - - name: Check Appium Server - uses: nick-fields/retry@7152eba30c6575329ac0576536151aca5a72780e # pin@3.0.0 - with: - timeout_seconds: 60 - max_attempts: 10 - command: curl --output /dev/null --silent --head --fail http://127.0.0.1:4723/sessions - - name: Run tests on Android if: ${{ matrix.platform == 'android' }} - env: - APPIUM_APP: ./app-release.apk uses: reactivecircus/android-emulator-runner@77986be26589807b8ebab3fde7bbf5c60dabec32 # pin@v2.31.0 with: - working-directory: test/e2e api-level: 30 force-avd-creation: false disable-animations: true @@ -603,20 +431,17 @@ jobs: -camera-back none -camera-front none -timezone US/Pacific - script: | - # Collect logs - adb logcat '*:D' 2>&1 >adb.log & - adb devices -l + script: ./scripts/e2e.mjs ${{ matrix.platform }} --test - yarn test --verbose + - uses: actions/cache@v4 + if: ${{ matrix.platform == 'ios' }} + with: + path: test/e2e/DerivedData/Build/Products/Debug-iphonesimulator/WebDriverAgentRunner-Runner.app + key: appium-webdriveragent-${{ hashFiles('test/e2e/yarn.lock') }} - name: Run tests on iOS if: ${{ matrix.platform == 'ios' }} - working-directory: test/e2e - env: - APPIUM_APP: ./RnDiffApp.app - APPIUM_DERIVED_DATA: DerivedData - run: yarn test --verbose + run: ./scripts/e2e.mjs ${{ matrix.platform }} --test - name: Upload logs if: ${{ always() }} diff --git a/.gitignore b/.gitignore index 87a34b65e..0fd72ba7b 100644 --- a/.gitignore +++ b/.gitignore @@ -74,6 +74,7 @@ yalc.lock # E2E tests test/react-native/versions +node_modules.bak # Created by Sentry Metro Plugin .sentry/ diff --git a/.vscode/settings.json b/.vscode/settings.json index 3c832d109..486e846db 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,7 +4,6 @@ "editor.formatOnSave": true, "editor.rulers": [120], "editor.tabSize": 2, - "files.autoSave": "onWindowChange", "files.trimTrailingWhitespace": true, "files.insertFinalNewline": true, "search.exclude": { diff --git a/scripts/e2e.mjs b/scripts/e2e.mjs new file mode 100755 index 000000000..4d3e358bf --- /dev/null +++ b/scripts/e2e.mjs @@ -0,0 +1,277 @@ +#!/usr/bin/env node +'use strict'; + +import { execSync, spawn } from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; +import { argv, env } from 'process'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +if (argv.length < 3) { + console.error(`Usage: ${path.basename(__filename)} `); + process.exit(1); +} + +const platform = argv[2]; +if (platform !== 'ios' && platform !== 'android') { + console.error(`Unsupported platform: ${platform}`); + process.exit(1); +} + +var actions = ['create', 'build', 'test']; +if (argv.length >= 4) { + var newActions = []; + for (let index = 0; index < actions.length; index++) { + const action = actions[index]; + if (argv.includes(`--${action}`) || argv.includes(`-${action}`) || argv.includes(action)) { + newActions.push(action); + } + } + actions = newActions; +} +console.log(`Performing actions: ${actions}`); + +if (env.SENTRY_DISABLE_AUTO_UPLOAD === undefined) { + // Auto upload to prod made the CI flaky + // This can be removed in the future or when mocked server is added + env.SENTRY_DISABLE_AUTO_UPLOAD = 'true' +} + +if (env.PRODUCTION === undefined && env.CI == undefined) { + // When executed locally and PROD not specified most likely we wanted production build + env.PRODUCTION = 1; +} + +if (!env.USE_FRAMEWORKS || env.USE_FRAMEWORKS === 'no') { + // In case it's set to an empty string, it causes issues in Podfile. + delete env.USE_FRAMEWORKS; +} + +if (platform == 'ios') { + // Flipper is causing build issues on iOS, so we disable it + env.NO_FLIPPER = 1 +} + +const rootDir = path.resolve(__dirname, '..'); +const rootPackageJson = JSON.parse(fs.readFileSync(`${rootDir}/package.json`, 'utf8')); +const RNVersion = env.RN_VERSION ? env.RN_VERSION : rootPackageJson.devDependencies['react-native']; +const RNEngine = env.RN_ENGINE ? env.RN_ENGINE : 'hermes'; +const buildType = env.PRODUCTION ? 'Release' : 'Debug'; +const appSourceRepo = 'https://github.com/react-native-community/rn-diff-purge.git'; +const appRepoDir = `${rootDir}/test/react-native/versions/${RNVersion}`; +const appName = 'RnDiffApp'; +const appDir = `${appRepoDir}/${appName}`; +const e2eDir = `${rootDir}/test/e2e`; +const testAppName = `${appName}.${platform == 'ios' ? 'app' : 'apk'}`; +const runtime = env.IOS_RUNTIME ? env.IOS_RUNTIME : 'latest'; +const device = env.IOS_DEVICE ? env.IOS_DEVICE : 'iPhone 15'; + +// Build and publish the SDK - we only need to do this once in CI. +// Locally, we may want to get updates from the latest build so do it on every app build. +if (actions.includes('create') || (env.CI === undefined && actions.includes('build'))) { + execSync(`yarn build`, { stdio: 'inherit', cwd: rootDir, env: env }); + execSync(`yalc publish`, { stdio: 'inherit', cwd: rootDir, env: env }); + execSync(`yarn build`, { stdio: 'inherit', cwd: e2eDir, env: env }); +} + +if (actions.includes('create')) { + // Clone the test app repo + if (fs.existsSync(appRepoDir)) fs.rmSync(appRepoDir, { recursive: true }); + execSync(`git clone ${appSourceRepo} --branch release/${RNVersion} --single-branch ${appRepoDir}`, { stdio: 'inherit', env: env }); + + // Install dependencies + // yalc add doesn't fail if the package is not found - it skips silently. + const yalcAddOutput = execSync(`yalc add @sentry/react-native`, { cwd: appDir, env: env, encoding: 'utf-8' }); + if (!yalcAddOutput.match(/Package .* added ==>/)) { + console.error(yalcAddOutput); + process.exit(1); + } else { + console.log(yalcAddOutput.trim()); + } + + // original yarnrc contains the exact yarn version which causes corepack to fail to install yarn v3 + fs.writeFileSync(`${appDir}/.yarnrc.yml`, 'nodeLinker: node-modules', { encoding: 'utf-8' }); + // yarn v3 won't install dependencies in a sub project without a yarn.lock file present + fs.writeFileSync(`${appDir}/yarn.lock`, ''); + + execSync(`yarn install`, { + stdio: 'inherit', cwd: appDir, + // yarn v3 run immutable install by default in CI + env: Object.assign(env, { YARN_ENABLE_IMMUTABLE_INSTALLS: false }) + }); + + execSync(`yarn add ../../../../e2e`, { stdio: 'inherit', cwd: appDir, env: env }); + + // Patch the app + execSync(`patch --verbose --strip=0 --force --ignore-whitespace --fuzz 4 < ../../../rn.patch`, { stdio: 'inherit', cwd: appDir, env: env }); + execSync(`../../../rn.patch.app.js --app .`, { stdio: 'inherit', cwd: appDir, env: env }); + execSync(`../../../rn.patch.metro.config.js --path metro.config.js`, { stdio: 'inherit', cwd: appDir, env: env }); + + // Set up platform-specific app configuration + if (platform == 'ios') { + execSync('ruby --version', { stdio: 'inherit', cwd: `${appDir}`, env: env }); + + execSync(`../../../../rn.patch.podfile.js --pod-file Podfile --engine ${RNEngine}`, { stdio: 'inherit', cwd: `${appDir}/ios`, env: env }); + + if (fs.existsSync(`${appDir}/Gemfile`)) { + execSync(`bundle install`, { stdio: 'inherit', cwd: appDir, env: env }); + execSync('bundle exec pod install --repo-update', { stdio: 'inherit', cwd: `${appDir}/ios`, env: env }); + } else { + execSync('pod install --repo-update', { stdio: 'inherit', cwd: `${appDir}/ios`, env: env }); + } + execSync('cat Podfile.lock | grep RNSentry', { stdio: 'inherit', cwd: `${appDir}/ios`, env: env }); + + execSync(`../../../rn.patch.xcode.js --project ios/${appName}.xcodeproj/project.pbxproj --rn-version ${RNVersion}`, { stdio: 'inherit', cwd: appDir, env: env }); + } else if (platform == 'android') { + execSync(`../../../rn.patch.gradle.properties.js --gradle-properties android/gradle.properties --engine ${RNEngine}`, { stdio: 'inherit', cwd: appDir, env: env }); + execSync(`../../../rn.patch.app.build.gradle.js --app-build-gradle android/app/build.gradle`, { stdio: 'inherit', cwd: appDir, env: env }); + + if (env.RCT_NEW_ARCH_ENABLED) { + execSync(`perl -i -pe's/newArchEnabled=false/newArchEnabled=true/g' android/gradle.properties`, { stdio: 'inherit', cwd: appDir, env: env }); + console.log('New Architecture enabled'); + } + } +} + +if (actions.includes('build')) { + // This prevents modules resolution from outside of the RN Test App projects during the native app build. + // See https://github.com/getsentry/sentry-react-native/pull/3409 + console.log('Renaming node_modules to node_modules.bak'); + fs.renameSync(`${rootDir}/node_modules`, `${rootDir}/node_modules.bak`); + + try { + console.log(`Building ${platform}: ${buildType}`); + var appProduct; + + if (platform == 'ios') { + // Build iOS test app + execSync(`set -o pipefail && xcodebuild \ + -workspace ${appName}.xcworkspace \ + -configuration ${buildType} \ + -scheme ${appName} \ + -destination 'platform=iOS Simulator,OS=${runtime},name=${device}' \ + ONLY_ACTIVE_ARCH=yes \ + -derivedDataPath DerivedData \ + build | tee xcodebuild.log | xcbeautify`, + { stdio: 'inherit', cwd: `${appDir}/ios`, env: env }); + + appProduct = `${appDir}/ios/DerivedData/Build/Products/${buildType}-iphonesimulator/${appName}.app`; + } else if (platform == 'android') { + execSync(`./gradlew assemble${buildType} -PreactNativeArchitectures=x86`, { stdio: 'inherit', cwd: `${appDir}/android`, env: env }); + appProduct = `${appDir}/android/app/build/outputs/apk/release/app-release.apk`; + } + + var testApp = `${e2eDir}/${testAppName}`; + console.log(`Moving ${appProduct} to ${testApp}`); + if (fs.existsSync(testApp)) fs.rmSync(testApp, { recursive: true }); + fs.renameSync(appProduct, testApp); + } finally { + console.log('Restoring node_modules from node_modules.bak'); + fs.renameSync(`${rootDir}/node_modules.bak`, `${rootDir}/node_modules`); + } +} + +if (actions.includes('test')) { + if (platform == 'ios' && !fs.existsSync(`${e2eDir}/DerivedData/Build/Products/Debug-iphonesimulator/WebDriverAgentRunner-Runner.app`)) { + // Build iOS WebDriverAgent + execSync(`set -o pipefail && xcodebuild \ + -project node_modules/appium-webdriveragent/WebDriverAgent.xcodeproj \ + -scheme WebDriverAgentRunner \ + -destination 'platform=iOS Simulator,OS=${runtime},name=${device}' \ + GCC_TREAT_WARNINGS_AS_ERRORS=0 \ + COMPILER_INDEX_STORE_ENABLE=NO \ + ONLY_ACTIVE_ARCH=yes \ + -derivedDataPath DerivedData \ + build | tee xcodebuild-agent.log | xcbeautify`, + { stdio: 'inherit', cwd: e2eDir, env: env }); + } + + // Start the appium server. + var processesToKill = {}; + async function newProcess(name, process) { + await new Promise((resolve, reject) => { + process.on('error', (e) => { + console.error(`Failed to start process '${name}': ${e}`); + reject(e); + }); + process.on('spawn', () => { + console.log(`Process '${name}' (${process.pid}) started`); + resolve(); + }); + }); + + processesToKill[name] = { + process: process, + complete: new Promise((resolve, _reject) => { + process.on('close', resolve); + }) + }; + } + await newProcess('appium', spawn('node_modules/.bin/appium', ['--log-timestamp', '--log-no-colors', '--log', `appium${platform}.log`], { stdio: 'inherit', cwd: e2eDir, env: env, shell: false })); + + try { + await waitForAppium(); + + // Run e2e tests + const testEnv = env; + testEnv.PLATFORM = platform; + testEnv.APPIUM_APP = `./${testAppName}`; + + if (platform == 'ios') { + testEnv.APPIUM_DERIVED_DATA = 'DerivedData'; + } else if (platform == 'android') { + execSync(`adb devices -l`, { stdio: 'inherit', cwd: e2eDir, env: env }); + + execSync(`adb logcat -c`, { stdio: 'inherit', cwd: e2eDir, env: env }); + + var adbLogStream = fs.createWriteStream(`${e2eDir}/adb.log`); + const adbLogProcess = spawn('adb', ['logcat'], { cwd: e2eDir, env: env, shell: false }) + adbLogProcess.stdout.pipe(adbLogStream); + adbLogProcess.stderr.pipe(adbLogStream); + adbLogProcess.on('close', () => adbLogStream.close()) + await newProcess('adb logcat', adbLogProcess); + } + + execSync(`yarn test --verbose`, { stdio: 'inherit', cwd: e2eDir, env: testEnv }); + } finally { + for (const [name, info] of Object.entries(processesToKill)) { + console.log(`Sending termination signal to process '${name}' (${info.process.pid})`); + + // Send SIGTERM first to allow graceful shutdown. + info.process.kill(15); + + // Also send SIGKILL after 10 seconds. + const killTimeout = setTimeout(() => process.kill(9), "10000"); + + // Wait for the process to exit (either via SIGTERM or SIGKILL). + const code = await info.complete; + + // Successfully exited now, no need to kill (if it hasn't run yet). + clearTimeout(killTimeout); + + console.log(`Process '${name}' (${info.process.pid}) exited with code ${code}`); + } + } +} + +async function waitForAppium() { + console.log("Waiting for Appium server to start..."); + for (let i = 0; i < 60; i++) { + try { + await fetch("http://127.0.0.1:4723/sessions", { method: "HEAD" }); + console.log("Appium server started"); + return; + } catch (error) { + console.log(`Appium server hasn't started yet (${error})...`); + await sleep(1000); + } + } + throw new Error("Appium server failed to start"); +} + +async function sleep(millis) { + return new Promise(resolve => setTimeout(resolve, millis)); +} diff --git a/test/e2e/.gitignore b/test/e2e/.gitignore index 894a3f5e3..e8d2a919e 100644 --- a/test/e2e/.gitignore +++ b/test/e2e/.gitignore @@ -1,2 +1,4 @@ *.png *.log +*.app +*.apk