diff --git a/.github/actions/composite/buildAndroidAPKDelta/action.yml b/.github/actions/composite/buildAndroidAPKDelta/action.yml new file mode 100644 index 000000000000..f466bb2a061a --- /dev/null +++ b/.github/actions/composite/buildAndroidAPKDelta/action.yml @@ -0,0 +1,29 @@ +name: Build an Android apk +description: Build an Android apk for an E2E test build and upload it as an artifact + +inputs: + ARTIFACT_NAME: + description: The name of the workflow artifact where the APK should be uploaded + required: true + +runs: + using: composite + steps: + - uses: Expensify/App/.github/actions/composite/setupNode@main + + - uses: ruby/setup-ruby@a05e47355e80e57b9a67566a813648fa67d92011 + with: + ruby-version: "2.7" + bundler-cache: true + + - uses: gradle/gradle-build-action@3fbe033aaae657f011f88f29be9e65ed26bd29ef + + - name: Build APK + run: npm run android-build-e2edelta + shell: bash + + - name: Upload APK + uses: actions/upload-artifact@65d862660abb392b8c4a3d1195a2108db131dd05 + with: + name: ${{ inputs.ARTIFACT_NAME }} + path: android/app/build/outputs/apk/e2e/release/app-e2edelta-release.apk diff --git a/.github/workflows/e2ePerformanceTests.yml b/.github/workflows/e2ePerformanceTests.yml index e1bb286179cf..874ff87f68d8 100644 --- a/.github/workflows/e2ePerformanceTests.yml +++ b/.github/workflows/e2ePerformanceTests.yml @@ -46,14 +46,28 @@ jobs: git fetch origin tag ${{ steps.getMostRecentRelease.outputs.VERSION }} --no-tags --depth=1 git switch --detach ${{ steps.getMostRecentRelease.outputs.VERSION }} + - uses: Expensify/App/.github/actions/composite/setupNode@main + + - uses: ruby/setup-ruby@a05e47355e80e57b9a67566a813648fa67d92011 + with: + ruby-version: "2.7" + bundler-cache: true + + - uses: gradle/gradle-build-action@3fbe033aaae657f011f88f29be9e65ed26bd29ef + - name: Configure MapBox SDK run: ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }} - name: Build APK - if: ${{ !fromJSON(steps.checkForExistingArtifact.outputs.exists) }} - uses: Expensify/App/.github/actions/composite/buildAndroidAPK@main + run: npm run android-build-e2e + shell: bash + + - name: Upload APK + uses: actions/upload-artifact@65d862660abb392b8c4a3d1195a2108db131dd05 with: - ARTIFACT_NAME: baseline-apk-${{ steps.getMostRecentRelease.outputs.VERSION }} + name: baseline-apk-${{ steps.getMostRecentRelease.outputs.VERSION }} + path: android/app/build/outputs/apk/e2e/release/app-e2e-release.apk + buildDelta: runs-on: ubuntu-latest-xl @@ -113,10 +127,24 @@ jobs: - name: Configure MapBox SDK run: ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }} + - uses: Expensify/App/.github/actions/composite/setupNode@main + + - uses: ruby/setup-ruby@a05e47355e80e57b9a67566a813648fa67d92011 + with: + ruby-version: "2.7" + bundler-cache: true + + - uses: gradle/gradle-build-action@3fbe033aaae657f011f88f29be9e65ed26bd29ef + - name: Build APK - uses: Expensify/App/.github/actions/composite/buildAndroidAPK@main + run: npm run android-build-e2edelta + shell: bash + + - name: Upload APK + uses: actions/upload-artifact@65d862660abb392b8c4a3d1195a2108db131dd05 with: - ARTIFACT_NAME: delta-apk-${{ steps.getDeltaRef.outputs.DELTA_REF }} + name: delta-apk-${{ steps.getDeltaRef.outputs.DELTA_REF }} + path: android/app/build/outputs/apk/e2edelta/release/app-e2edelta-release.apk runTestsInAWS: runs-on: ubuntu-latest @@ -140,7 +168,7 @@ jobs: # The downloaded artifact will be a file named "app-e2e-release.apk" so we have to rename it - name: Rename baseline APK - run: mv "${{steps.downloadBaselineAPK.outputs.download-path}}/app-e2e-release.apk" "${{steps.downloadBaselineAPK.outputs.download-path}}/app-e2eRelease-main.apk" + run: mv "${{steps.downloadBaselineAPK.outputs.download-path}}/app-e2e-release.apk" "${{steps.downloadBaselineAPK.outputs.download-path}}/app-e2eRelease.apk" - name: Download delta APK uses: actions/download-artifact@e9ef242655d12993efdcda9058dee2db83a2cb9b @@ -150,7 +178,7 @@ jobs: path: zip - name: Rename delta APK - run: mv "${{steps.downloadDeltaAPK.outputs.download-path}}/app-e2e-release.apk" "${{steps.downloadDeltaAPK.outputs.download-path}}/app-e2eRelease-delta.apk" + run: mv "${{steps.downloadDeltaAPK.outputs.download-path}}/app-e2edelta-release.apk" "${{steps.downloadDeltaAPK.outputs.download-path}}/app-e2edeltaRelease.apk" - name: Copy e2e code into zip folder run: cp -r tests/e2e zip @@ -172,12 +200,12 @@ jobs: name: App E2E Performance Regression Tests project_arn: ${{ secrets.AWS_PROJECT_ARN }} device_pool_arn: ${{ secrets.AWS_DEVICE_POOL_ARN }} - app_file: zip/app-e2eRelease-main.apk + app_file: zip/app-e2eRelease.apk app_type: ANDROID_APP test_type: APPIUM_NODE test_package_file: App.zip test_package_type: APPIUM_NODE_TEST_PACKAGE - test_spec_file: tests/e2e/TestSpecMain.yml + test_spec_file: tests/e2e/TestSpec.yml test_spec_type: APPIUM_NODE_TEST_SPEC remote_src: false file_artifacts: Customer Artifacts.zip @@ -192,38 +220,13 @@ jobs: unzip "Customer Artifacts.zip" -d mainResults cat ./mainResults/Host_Machine_Files/\$WORKING_DIRECTORY/debug.log - - name: Unzip AWS Device Farm main results - run: unzip "Customer Artifacts.zip" -d mainResults - - - name: Delete Customer Artifacts.zip - run: rm "Customer Artifacts.zip" - - - name: Schedule AWS Device Farm test run on delta branch - uses: realm/aws-devicefarm/test-application@7b9a91236c456c97e28d384c9e476035d5ea686b - with: - name: App E2E Performance Regression Tests - project_arn: ${{ secrets.AWS_PROJECT_ARN }} - device_pool_arn: ${{ secrets.AWS_DEVICE_POOL_ARN }} - app_file: zip/app-e2eRelease-delta.apk - app_type: ANDROID_APP - test_type: APPIUM_NODE - test_package_file: App.zip - test_package_type: APPIUM_NODE_TEST_PACKAGE - test_spec_file: tests/e2e/TestSpecDelta.yml - test_spec_type: APPIUM_NODE_TEST_SPEC - remote_src: false - file_artifacts: Customer Artifacts.zip - cleanup: true - timeout: 5400 - - - name: Unzip AWS Device Farm delta results - run: unzip "Customer Artifacts.zip" -d deltaResults - - - name: Compare results - run: node tests/e2e/merge.js --mainPath ./mainResults/Host_Machine_Files/\$WORKING_DIRECTORY/main.json --deltaPath ./deltaResults//Host_Machine_Files/\$WORKING_DIRECTORY/delta.json --outputPath ./output.md + - name: Unzip AWS Device Farm results + if: ${{ always() }} + run: unzip "Customer Artifacts.zip" - - name: Print results - run: cat "./output.md" + - name: Print AWS Device Farm run results + if: ${{ always() }} + run: cat "./Host_Machine_Files/\$WORKING_DIRECTORY/output.md" - name: Check if test failed, if so post the results and add the DeployBlocker label run: | diff --git a/android/app/build.gradle b/android/app/build.gradle index d99b2f762163..25516308afca 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -58,7 +58,8 @@ project.ext.envConfigFiles = [ adhocRelease: ".env.adhoc", developmentRelease: ".env", developmentDebug: ".env", - e2eRelease: "tests/e2e/.env.e2e" + e2eRelease: "tests/e2e/.env.e2e", + e2edeltaRelease: "tests/e2e/.env.e2edelta" ] /** @@ -101,7 +102,14 @@ android { e2e { // If are building a version that won't be uploaded to the play store, we don't have to use production keys // applies all non-production flavors - applicationIdSuffix ".adhoc" + applicationIdSuffix ".e2e" + signingConfig signingConfigs.debug + resValue "string", "build_config_package", "com.expensify.chat" + } + e2edelta { + // If are building a version that won't be uploaded to the play store, we don't have to use production keys + // applies all non-production flavors + applicationIdSuffix ".e2edelta" signingConfig signingConfigs.debug resValue "string", "build_config_package", "com.expensify.chat" } @@ -150,12 +158,13 @@ android { } // ... except for the e2e flavor, which we maybe want to build locally: productFlavors.e2e.signingConfig signingConfigs.debug + productFlavors.e2edelta.signingConfig signingConfigs.debug } } // since we don't need variants adhocDebug and e2eDebug, we can force gradle to ignore them variantFilter { variant -> - if (variant.name == "adhocDebug" || variant.name == "e2eDebug") { + if (variant.name == "adhocDebug" || variant.name == "e2eDebug" || variant.name == "e2edeltaDebug") { setIgnore(true) } } diff --git a/android/app/google-services.json b/android/app/google-services.json index 35f7f5b68921..c0dcb51310ef 100644 --- a/android/app/google-services.json +++ b/android/app/google-services.json @@ -1,143 +1,231 @@ { - "project_info": { - "project_number": "921154746561", - "firebase_url": "https://expensify-chat.firebaseio.com", - "project_id": "expensify-chat", - "storage_bucket": "expensify-chat.appspot.com" - }, - "client": [ - { - "client_info": { - "mobilesdk_app_id": "1:921154746561:android:4f04268f25f84eaf027c40", - "android_client_info": { - "package_name": "com.expensify.chat" + "project_info": { + "project_number": "921154746561", + "firebase_url": "https://expensify-chat.firebaseio.com", + "project_id": "expensify-chat", + "storage_bucket": "expensify-chat.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:921154746561:android:4f04268f25f84eaf027c40", + "android_client_info": { + "package_name": "com.expensify.chat" + } + }, + "oauth_client": [ + { + "client_id": "921154746561-o0pgqgc84e3e97s9iljlmimcb5nesqad.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.expensify.chat", + "certificate_hash": "5e8f16062ea3cd2c4a0d547876baa6f38cabf625" } }, - "oauth_client": [ - { - "client_id": "921154746561-o0pgqgc84e3e97s9iljlmimcb5nesqad.apps.googleusercontent.com", - "client_type": 1, - "android_info": { - "package_name": "com.expensify.chat", - "certificate_hash": "5e8f16062ea3cd2c4a0d547876baa6f38cabf625" - } - }, - { - "client_id": "921154746561-gpsoaqgqfuqrfsjdf8l7vohfkfj7b9up.apps.googleusercontent.com", - "client_type": 3 - } - ], - "api_key": [ - { - "current_key": "AIzaSyCVwQb9lBI06bDIwHOw10AkdJyquXoMngk" - } - ], - "services": { - "appinvite_service": { - "other_platform_oauth_client": [ - { - "client_id": "921154746561-gpsoaqgqfuqrfsjdf8l7vohfkfj7b9up.apps.googleusercontent.com", - "client_type": 3 - }, - { - "client_id": "921154746561-080fav7kvk6s70k6nd70mt50isubgff4.apps.googleusercontent.com", - "client_type": 2, - "ios_info": { - "bundle_id": "com.expensify.chat.adhoc" - } + { + "client_id": "921154746561-gpsoaqgqfuqrfsjdf8l7vohfkfj7b9up.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCVwQb9lBI06bDIwHOw10AkdJyquXoMngk" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "921154746561-gpsoaqgqfuqrfsjdf8l7vohfkfj7b9up.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "921154746561-080fav7kvk6s70k6nd70mt50isubgff4.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "com.expensify.chat.adhoc" } - ] - } + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:921154746561:android:333e293a7fef83a8027c40", + "android_client_info": { + "package_name": "com.expensify.chat.adhoc" } }, - { - "client_info": { - "mobilesdk_app_id": "1:921154746561:android:333e293a7fef83a8027c40", - "android_client_info": { - "package_name": "com.expensify.chat.adhoc" + "oauth_client": [ + { + "client_id": "921154746561-cbegir0tnc2gan6k1gre5vtn75p60hom.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.expensify.chat.adhoc", + "certificate_hash": "5e8f16062ea3cd2c4a0d547876baa6f38cabf625" } }, - "oauth_client": [ - { - "client_id": "921154746561-cbegir0tnc2gan6k1gre5vtn75p60hom.apps.googleusercontent.com", - "client_type": 1, - "android_info": { - "package_name": "com.expensify.chat.adhoc", - "certificate_hash": "5e8f16062ea3cd2c4a0d547876baa6f38cabf625" + { + "client_id": "921154746561-gpsoaqgqfuqrfsjdf8l7vohfkfj7b9up.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCVwQb9lBI06bDIwHOw10AkdJyquXoMngk" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "921154746561-gpsoaqgqfuqrfsjdf8l7vohfkfj7b9up.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "921154746561-080fav7kvk6s70k6nd70mt50isubgff4.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "com.expensify.chat.adhoc" + } } - }, - { - "client_id": "921154746561-gpsoaqgqfuqrfsjdf8l7vohfkfj7b9up.apps.googleusercontent.com", - "client_type": 3 - } - ], - "api_key": [ - { - "current_key": "AIzaSyCVwQb9lBI06bDIwHOw10AkdJyquXoMngk" + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:921154746561:android:333e293a7fef83a8027c40", + "android_client_info": { + "package_name": "com.expensify.chat.e2e" + } + }, + "oauth_client": [ + { + "client_id": "921154746561-cbegir0tnc2gan6k1gre5vtn75p60hom.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.expensify.chat.e2e", + "certificate_hash": "5e8f16062ea3cd2c4a0d547876baa6f38cabf625" } - ], - "services": { - "appinvite_service": { - "other_platform_oauth_client": [ - { - "client_id": "921154746561-gpsoaqgqfuqrfsjdf8l7vohfkfj7b9up.apps.googleusercontent.com", - "client_type": 3 - }, - { - "client_id": "921154746561-080fav7kvk6s70k6nd70mt50isubgff4.apps.googleusercontent.com", - "client_type": 2, - "ios_info": { - "bundle_id": "com.expensify.chat.adhoc" - } + }, + { + "client_id": "921154746561-gpsoaqgqfuqrfsjdf8l7vohfkfj7b9up.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCVwQb9lBI06bDIwHOw10AkdJyquXoMngk" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "921154746561-gpsoaqgqfuqrfsjdf8l7vohfkfj7b9up.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "921154746561-080fav7kvk6s70k6nd70mt50isubgff4.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "com.expensify.chat.e2e" } - ] - } + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:921154746561:android:333e293a7fef83a8027c40", + "android_client_info": { + "package_name": "com.expensify.chat.e2edelta" } }, - { - "client_info": { - "mobilesdk_app_id": "1:921154746561:android:3b19fdbaedb5b586027c40", - "android_client_info": { - "package_name": "com.expensify.chat.dev" + "oauth_client": [ + { + "client_id": "921154746561-cbegir0tnc2gan6k1gre5vtn75p60hom.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.expensify.chat.e2edelta", + "certificate_hash": "5e8f16062ea3cd2c4a0d547876baa6f38cabf625" } }, - "oauth_client": [ - { - "client_id": "921154746561-svjnccrcn6vet45kn9o7sibb3jemipa6.apps.googleusercontent.com", - "client_type": 1, - "android_info": { - "package_name": "com.expensify.chat.dev", - "certificate_hash": "5e8f16062ea3cd2c4a0d547876baa6f38cabf625" + { + "client_id": "921154746561-gpsoaqgqfuqrfsjdf8l7vohfkfj7b9up.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCVwQb9lBI06bDIwHOw10AkdJyquXoMngk" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "921154746561-gpsoaqgqfuqrfsjdf8l7vohfkfj7b9up.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "921154746561-080fav7kvk6s70k6nd70mt50isubgff4.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "com.expensify.chat.e2edelta" + } } - }, - { - "client_id": "921154746561-gpsoaqgqfuqrfsjdf8l7vohfkfj7b9up.apps.googleusercontent.com", - "client_type": 3 - } - ], - "api_key": [ - { - "current_key": "AIzaSyCVwQb9lBI06bDIwHOw10AkdJyquXoMngk" + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:921154746561:android:3b19fdbaedb5b586027c40", + "android_client_info": { + "package_name": "com.expensify.chat.dev" + } + }, + "oauth_client": [ + { + "client_id": "921154746561-svjnccrcn6vet45kn9o7sibb3jemipa6.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.expensify.chat.dev", + "certificate_hash": "5e8f16062ea3cd2c4a0d547876baa6f38cabf625" } - ], - "services": { - "appinvite_service": { - "other_platform_oauth_client": [ - { - "client_id": "921154746561-gpsoaqgqfuqrfsjdf8l7vohfkfj7b9up.apps.googleusercontent.com", - "client_type": 3 - }, - { - "client_id": "921154746561-080fav7kvk6s70k6nd70mt50isubgff4.apps.googleusercontent.com", - "client_type": 2, - "ios_info": { - "bundle_id": "com.expensify.chat.adhoc" - } + }, + { + "client_id": "921154746561-gpsoaqgqfuqrfsjdf8l7vohfkfj7b9up.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCVwQb9lBI06bDIwHOw10AkdJyquXoMngk" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "921154746561-gpsoaqgqfuqrfsjdf8l7vohfkfj7b9up.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "921154746561-080fav7kvk6s70k6nd70mt50isubgff4.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "com.expensify.chat.adhoc" } - ] - } + } + ] } } - ], - "configuration_version": "1" - } + } + ], + "configuration_version": "1" +} diff --git a/android/app/src/e2edelta/AndroidManifest.xml b/android/app/src/e2edelta/AndroidManifest.xml new file mode 100644 index 000000000000..201d730f5211 --- /dev/null +++ b/android/app/src/e2edelta/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 78abf8074155..98366cd3b967 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -15,6 +15,7 @@ opt_out_usage platform :android do desc "Generate a new local APK for e2e testing" + lane :build_e2e do ENV["ENVFILE"]="tests/e2e/.env.e2e" ENV["ENTRY_FILE"]="src/libs/E2E/reactNativeLaunchingTest.js" @@ -28,6 +29,19 @@ platform :android do ) end + lane :build_e2edelta do + ENV["ENVFILE"]="tests/e2e/.env.e2edelta" + ENV["ENTRY_FILE"]="src/libs/E2E/reactNativeLaunchingTest.js" + ENV["E2E_TESTING"]="true" + + gradle( + project_dir: './android', + task: ':app:assemble', + flavor: 'e2edelta', + build_type: 'Release', + ) + end + desc "Generate a new local APK" lane :build do ENV["ENVFILE"]=".env.production" diff --git a/package.json b/package.json index 6cb4dd115272..91cc7c62bda8 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "ios-build": "fastlane ios build", "android-build": "fastlane android build", "android-build-e2e": "bundle exec fastlane android build_e2e", + "android-build-e2edelta": "bundle exec fastlane android build_e2edelta", "test": "TZ=utc jest", "typecheck": "tsc", "lint": "eslint . --max-warnings=0 --cache --cache-location=node_modules/.cache/eslint", @@ -49,10 +50,7 @@ "analyze-packages": "ANALYZE_BUNDLE=true webpack --config config/webpack/webpack.common.js --env envFile=.env.production", "symbolicate:android": "npx metro-symbolicate android/app/build/generated/sourcemaps/react/release/index.android.bundle.map", "symbolicate:ios": "npx metro-symbolicate main.jsbundle.map", - "test:e2e:dev": "node tests/e2e/testRunner.js --development --skipCheckout --config ./config.dev.js --buildMode skip --skipInstallDeps", - "test:e2e:main": "node tests/e2e/testRunner.js --development --skipCheckout", - "test:e2e:delta": "node tests/e2e/testRunner.js --development --label delta --skipCheckout --skipInstallDeps", - "test:e2e:compare": "node tests/e2e/merge.js", + "test:e2e": "node tests/e2e/testRunner.js --development --skipCheckout --skipInstallDeps --buildMode none", "gh-actions-unused-styles": "./.github/scripts/findUnusedKeys.sh", "workflow-test": "./workflow_tests/scripts/runWorkflowTests.sh", "workflow-test:generate": "node workflow_tests/utils/preGenerateTest.js", diff --git a/src/libs/E2E/tests/appStartTimeTest.e2e.js b/src/libs/E2E/tests/appStartTimeTest.e2e.js index 311b891fcd4c..0abbf3323f2a 100644 --- a/src/libs/E2E/tests/appStartTimeTest.e2e.js +++ b/src/libs/E2E/tests/appStartTimeTest.e2e.js @@ -1,3 +1,4 @@ +import Config from 'react-native-config'; import _ from 'underscore'; import E2ELogin from '@libs/E2E/actions/e2eLogin'; import E2EClient from '@libs/E2E/client'; @@ -20,6 +21,7 @@ const test = () => { Promise.all( _.map(metrics, (metric) => E2EClient.submitTestResults({ + branch: Config.E2E_BRANCH, name: `App start ${metric.name}`, duration: metric.duration, }), diff --git a/src/libs/E2E/tests/openSearchPageTest.e2e.js b/src/libs/E2E/tests/openSearchPageTest.e2e.js index 1101a620f413..aad816766f9e 100644 --- a/src/libs/E2E/tests/openSearchPageTest.e2e.js +++ b/src/libs/E2E/tests/openSearchPageTest.e2e.js @@ -1,3 +1,4 @@ +import Config from 'react-native-config'; import E2ELogin from '@libs/E2E/actions/e2eLogin'; import E2EClient from '@libs/E2E/client'; import Navigation from '@libs/Navigation/Navigation'; @@ -31,6 +32,7 @@ const test = () => { console.debug(`[E2E] Submitting!`); E2EClient.submitTestResults({ + branch: Config.E2E_BRANCH, name: 'Open Search Page TTI', duration: entry.duration, }) diff --git a/src/libs/E2E/tests/reportTypingTest.e2e.js b/src/libs/E2E/tests/reportTypingTest.e2e.js index b79166063b4f..90d0dc9e0bb6 100644 --- a/src/libs/E2E/tests/reportTypingTest.e2e.js +++ b/src/libs/E2E/tests/reportTypingTest.e2e.js @@ -1,3 +1,4 @@ +import Config from 'react-native-config'; import E2ELogin from '@libs/E2E/actions/e2eLogin'; import waitForKeyboard from '@libs/E2E/actions/waitForKeyboard'; import E2EClient from '@libs/E2E/client'; @@ -42,6 +43,7 @@ const test = () => { const rerenderCount = getRerenderCount(); E2EClient.submitTestResults({ + branch: Config.E2E_BRANCH, name: 'Composer typing rerender count', renderCount: rerenderCount, }).then(E2EClient.submitTestDone); diff --git a/tests/e2e/.env.e2e b/tests/e2e/.env.e2e index 7c2afd5a820b..a6611ad2ff9d 100644 --- a/tests/e2e/.env.e2e +++ b/tests/e2e/.env.e2e @@ -1,2 +1,3 @@ +E2E_BRANCH=main E2E_TESTING=true CAPTURE_METRICS=true diff --git a/tests/e2e/.env.e2edelta b/tests/e2e/.env.e2edelta new file mode 100644 index 000000000000..a5d7a96d0788 --- /dev/null +++ b/tests/e2e/.env.e2edelta @@ -0,0 +1,3 @@ +E2E_BRANCH=delta +E2E_TESTING=true +CAPTURE_METRICS=true diff --git a/tests/e2e/TestSpecDelta.yml b/tests/e2e/TestSpec.yml similarity index 89% rename from tests/e2e/TestSpecDelta.yml rename to tests/e2e/TestSpec.yml index 2d4906855ca8..4a5be0a5fcdd 100644 --- a/tests/e2e/TestSpecDelta.yml +++ b/tests/e2e/TestSpec.yml @@ -20,7 +20,7 @@ phases: commands: - cd zip - npm install underscore - - node e2e/testRunner.js -- --skipInstallDeps --buildMode "skip" --skipCheckout --label delta --appPath app-e2eRelease-delta.apk + - node e2e/testRunner.js -- --skipInstallDeps --buildMode "skip" --skipCheckout --mainAppPath app-e2eRelease.apk --deltaAppPath app-e2edeltaRelease.apk artifacts: - $WORKING_DIRECTORY diff --git a/tests/e2e/TestSpecMain.yml b/tests/e2e/TestSpecMain.yml deleted file mode 100644 index 6cf1c5d0b273..000000000000 --- a/tests/e2e/TestSpecMain.yml +++ /dev/null @@ -1,26 +0,0 @@ -version: 0.1 - -phases: - install: - commands: - # Install correct version of node - - export NVM_DIR=$HOME/.nvm - - . $NVM_DIR/nvm.sh - - nvm install 16.15.1 - - nvm use 16.15.1 - - # Reverse ports using AWS magic - - PORT=4723 - - IP_ADDRESS=$(ip -4 addr show eth0 | grep -Po "(?<=inet\s)\d+(\.\d+){3}") - - reverse_values="{\"ip_address\":\"$IP_ADDRESS\",\"local_port\":\"$PORT\",\"remote_port\":\"$PORT\"}" - - "curl -H \"Content-Type: application/json\" -X POST -d \"$reverse_values\" http://localhost:31007/reverse_forward_tcp" - - adb reverse tcp:$PORT tcp:$PORT - - test: - commands: - - cd zip - - npm install underscore - - node e2e/testRunner.js -- --skipInstallDeps --buildMode "skip" --skipCheckout --branch main --appPath app-e2eRelease-main.apk - -artifacts: -- $WORKING_DIRECTORY diff --git a/tests/e2e/compare/compare.js b/tests/e2e/compare/compare.js index 63d8697e4053..3be7abc91188 100644 --- a/tests/e2e/compare/compare.js +++ b/tests/e2e/compare/compare.js @@ -1,7 +1,6 @@ -const fs = require('fs/promises'); -const fsSync = require('fs'); const _ = require('underscore'); const {computeProbability, computeZ} = require('./math'); +const {getStats} = require('../measure/math'); const printToConsole = require('./output/console'); const writeToMarkdown = require('./output/markdown'); @@ -26,18 +25,6 @@ const PROBABILITY_CONSIDERED_SIGNIFICANCE = 0.02; */ const DURATION_DIFF_THRESHOLD_SIGNIFICANCE = 100; -const loadFile = (path) => - fs.readFile(path, 'utf8').then((data) => { - const entries = JSON.parse(data); - - const result = {}; - entries.forEach((entry) => { - result[entry.name] = entry; - }); - - return result; - }); - /** * * @param {string} name @@ -83,8 +70,11 @@ function compareResults(compareEntries, baselineEntries) { const current = compareEntries[name]; const baseline = baselineEntries[name]; + const currentStats = getStats(baseline); + const deltaStats = getStats(current); + if (baseline && current) { - compared.push(buildCompareEntry(name, current, baseline)); + compared.push(buildCompareEntry(name, deltaStats, currentStats)); } else if (current) { added.push({ name, @@ -100,11 +90,9 @@ function compareResults(compareEntries, baselineEntries) { const significance = _.chain(compared) .filter((item) => item.isDurationDiffOfSignificance) - .sort((a, b) => b.diff - a.diff) .value(); const meaningless = _.chain(compared) .filter((item) => !item.isDurationDiffOfSignificance) - .sort((a, b) => b.diff - a.diff) .value(); added.sort((a, b) => b.current.mean - a.current.mean); @@ -118,25 +106,14 @@ function compareResults(compareEntries, baselineEntries) { }; } -module.exports = (baselineFile, compareFile, outputFile, outputFormat = 'all') => { - const hasBaselineFile = fsSync.existsSync(baselineFile); - if (!hasBaselineFile) { - throw new Error(`Baseline results files "${baselineFile}" does not exists.`); +module.exports = (main, delta, outputFile, outputFormat = 'all') => { + const outputData = compareResults(main, delta); + + if (outputFormat === 'console' || outputFormat === 'all') { + printToConsole(outputData); + } + + if (outputFormat === 'markdown' || outputFormat === 'all') { + return writeToMarkdown(outputFile, outputData); } - return loadFile(baselineFile).then((baseline) => { - const hasCompareFile = fsSync.existsSync(compareFile); - if (!hasCompareFile) { - throw new Error(`Compare results files "${compareFile}" does not exists.`); - } - return loadFile(compareFile).then((compare) => { - const outputData = compareResults(compare, baseline); - - if (outputFormat === 'console' || outputFormat === 'all') { - printToConsole(outputData); - } - if (outputFormat === 'markdown' || outputFormat === 'all') { - return writeToMarkdown(outputFile, outputData); - } - }); - }); }; diff --git a/tests/e2e/config.js b/tests/e2e/config.js index c466000d0b53..4f7677cad706 100644 --- a/tests/e2e/config.js +++ b/tests/e2e/config.js @@ -22,9 +22,11 @@ const TEST_NAMES = { * ``` */ module.exports = { - APP_PACKAGE: 'com.expensify.chat.adhoc', + MAIN_APP_PACKAGE: 'com.expensify.chat.e2e', + DELTA_APP_PACKAGE: 'com.expensify.chat.e2edelta', - APP_PATH: './app-e2eRelease-main.apk', + MAIN_APP_PATH: './app-e2eRelease.apk', + DELTA_APP_PATH: './app-e2edeltaRelease.apk', ENTRY_FILE: 'src/libs/E2E/reactNativeLaunchingTest.js', diff --git a/tests/e2e/config.local.js b/tests/e2e/config.local.js index 8cdfc50ac625..45b946b91aeb 100644 --- a/tests/e2e/config.local.js +++ b/tests/e2e/config.local.js @@ -1,5 +1,9 @@ module.exports = { - APP_PACKAGE: 'com.expensify.chat.adhoc', - APP_PATH: './android/app/build/outputs/apk/e2e/release/app-e2e-release.apk', - RUNS: 4, + MAIN_APP_PACKAGE: 'com.expensify.chat.e2e', + DELTA_APP_PACKAGE: 'com.expensify.chat.e2edelta', + MAIN_APP_PATH: './android/app/build/outputs/apk/e2e/release/app-e2e-release.apk', + DELTA_APP_PATH: './android/app/build/outputs/apk/e2edelta/release/app-e2edelta-release.apk', + + BOOT_COOL_DOWN: 1 * 1000, + RUNS: 8, }; diff --git a/tests/e2e/testRunner.js b/tests/e2e/testRunner.js index 54cde8f5b336..1e9a9a89caf0 100644 --- a/tests/e2e/testRunner.js +++ b/tests/e2e/testRunner.js @@ -23,11 +23,10 @@ const killApp = require('./utils/killApp'); const launchApp = require('./utils/launchApp'); const createServerInstance = require('./server'); const installApp = require('./utils/installApp'); -const math = require('./measure/math'); -const writeTestStats = require('./measure/writeTestStats'); const withFailTimeout = require('./utils/withFailTimeout'); const reversePort = require('./utils/androidReversePort'); const sleep = require('./utils/sleep'); +const compare = require('./compare/compare'); // VARIABLE CONFIGURATION const args = process.argv.slice(2); @@ -81,10 +80,8 @@ if (args.includes('--config')) { } // Important set app path after correct config file has been set -let appPath = config.APP_PATH; -if (args.includes('--appPath')) { - appPath = args[args.indexOf('--appPath') + 1]; -} +let mainAppPath = config.MAIN_APP_PATH; +let deltaAppPath = config.DELTA_APP_PATH; // Create some variables after the correct config file has been loaded const OUTPUT_FILE = `${config.OUTPUT_DIR}/${label}.json`; @@ -113,57 +110,103 @@ if (isDevMode) { // START OF TEST CODE -const restartApp = async () => { - Logger.log('Killing app …'); - await killApp('android', config.APP_PACKAGE); - Logger.log('Launching app …'); - await launchApp('android', config.APP_PACKAGE); -}; - const runTests = async () => { // check if using buildMode "js-only" or "none" is possible if (buildMode !== 'full') { - const appExists = fs.existsSync(appPath); - if (!appExists) { + const mainAppExists = fs.existsSync(mainAppPath); + const deltaAppExists = fs.existsSync(deltaAppPath); + if (!mainAppExists || !deltaAppExists) { Logger.warn(`Build mode "${buildMode}" is not possible, because the app does not exist. Falling back to build mode "full".`); - Logger.note(`App path: ${appPath}`); + Logger.note(`App path: ${mainAppPath}`); buildMode = 'full'; } } - if (branch != null && !skipCheckout) { - // Switch branch - Logger.log(`Preparing tests on branch '${branch}' - git checkout`); - await execAsync(`git checkout ${branch}`); - } - - if (!skipInstallDeps) { - Logger.log(`Preparing tests on branch '${branch}' - npm install`); - await execAsync('npm i'); - } - // Build app if (buildMode === 'full') { - Logger.log(`Preparing tests on branch '${branch}' - building app`); + Logger.log(`Test setup - building main branch`); + + if (!skipCheckout) { + // Switch branch + Logger.log(`Test setup - checkout main`); + await execAsync(`git checkout main`); + } + + if (!skipInstallDeps) { + Logger.log(`Test setup - npm install`); + await execAsync('npm i'); + } + await execAsync('npm run android-build-e2e'); + + if (branch != null && !skipCheckout) { + // Switch branch + Logger.log(`Test setup - checkout branch '${branch}'`); + await execAsync(`git checkout ${branch}`); + } + + if (!skipInstallDeps) { + Logger.log(`Test setup - npm install`); + await execAsync('npm i'); + } + + Logger.log(`Test setup '${branch}' - building delta branch`); + await execAsync('npm run android-build-e2edelta'); } else if (buildMode === 'js-only') { - Logger.log(`Preparing tests on branch '${branch}' - building js bundle`); + Logger.log(`Test setup '${branch}' - building js bundle`); + + if (!skipInstallDeps) { + Logger.log(`Test setup '${branch}' - npm install`); + await execAsync('npm i'); + } // Build a new JS bundle + if (!skipCheckout) { + // Switch branch + Logger.log(`Test setup - checkout main`); + await execAsync(`git checkout main`); + } + + if (!skipInstallDeps) { + Logger.log(`Test setup - npm install`); + await execAsync('npm i'); + } + const tempDir = `${config.OUTPUT_DIR}/temp`; - const tempBundlePath = `${tempDir}/index.android.bundle`; + let tempBundlePath = `${tempDir}/index.android.bundle`; + await execAsync(`rm -rf ${tempDir} && mkdir ${tempDir}`); + await execAsync(`npx react-native bundle --platform android --dev false --entry-file ${config.ENTRY_FILE} --bundle-output ${tempBundlePath}`, {E2E_TESTING: 'true'}); + // Repackage the existing native app with the new bundle + let tempApkPath = `${tempDir}/app-release.apk`; + await execAsync(`./scripts/android-repackage-app-bundle-and-sign.sh ${mainAppPath} ${tempBundlePath} ${tempApkPath}`); + mainAppPath = tempApkPath; + + // Build a new JS bundle + if (!skipCheckout) { + // Switch branch + Logger.log(`Test setup - checkout main`); + await execAsync(`git checkout ${branch}`); + } + + if (!skipInstallDeps) { + Logger.log(`Test setup - npm install`); + await execAsync('npm i'); + } + + tempBundlePath = `${tempDir}/index.android.bundle`; await execAsync(`rm -rf ${tempDir} && mkdir ${tempDir}`); await execAsync(`npx react-native bundle --platform android --dev false --entry-file ${config.ENTRY_FILE} --bundle-output ${tempBundlePath}`, {E2E_TESTING: 'true'}); // Repackage the existing native app with the new bundle - const tempApkPath = `${tempDir}/app-release.apk`; - await execAsync(`./scripts/android-repackage-app-bundle-and-sign.sh ${appPath} ${tempBundlePath} ${tempApkPath}`); - appPath = tempApkPath; + tempApkPath = `${tempDir}/app-release.apk`; + await execAsync(`./scripts/android-repackage-app-bundle-and-sign.sh ${deltaAppPath} ${tempBundlePath} ${tempApkPath}`); + deltaAppPath = tempApkPath; } - // Install app and reverse port - let progressLog = Logger.progressInfo('Installing app and reversing port'); - await installApp('android', config.APP_PACKAGE, appPath); + let progressLog = Logger.progressInfo('Installing apps and reversing port'); + + await installApp('android', config.MAIN_APP_PACKAGE, defaultConfig.MAIN_APP_PATH); + await installApp('android', config.DELTA_APP_PACKAGE, defaultConfig.DELTA_APP_PATH); await reversePort(); progressLog.done(); @@ -171,8 +214,8 @@ const runTests = async () => { const server = createServerInstance(); await server.start(); - // Create a dict in which we will store the collected metrics for all tests - const resultsByTestName = {}; + // Create a dict in which we will store the run durations for all tests + const results = {}; // Collect results while tests are being executed server.addTestResultListener((testResult) => { @@ -191,28 +234,33 @@ const runTests = async () => { result = testResult.renderCount; } - Logger.log(`[LISTENER] Test '${testResult.name}' measured ${result}`); - resultsByTestName[testResult.name] = (resultsByTestName[testResult.name] || []).concat(result); + Logger.log(`[LISTENER] Test '${testResult.name}' on '${testResult.branch}' measured ${result}`); + + if (!results[testResult.branch]) { + results[testResult.branch] = {}; + } + + results[testResult.branch][testResult.name] = (results[testResult.branch][testResult.name] || []).concat(result); }); // Run the tests - const numOfTests = _.values(config.TESTS_CONFIG).length; - for (let testIndex = 0; testIndex < numOfTests; testIndex++) { - const testConfig = _.values(config.TESTS_CONFIG)[testIndex]; + const suites = _.values(config.TESTS_CONFIG); + for (let suiteIndex = 0; suiteIndex < suites.length; suiteIndex++) { + const suite = _.values(config.TESTS_CONFIG)[suiteIndex]; // check if we want to skip the test if (args.includes('--includes')) { const includes = args[args.indexOf('--includes') + 1]; // assume that "includes" is a regexp - if (!testConfig.name.match(includes)) { + if (!suite.name.match(includes)) { // eslint-disable-next-line no-continue continue; } } - const coolDownLogs = Logger.progressInfo(`Cooling down for ${config.COOL_DOWN / 1000}s`); - coolDownLogs.updateText(`Cooling down for ${config.COOL_DOWN / 1000}s`); + const coolDownLogs = Logger.progressInfo(`Cooling down for ${config.BOOT_COOL_DOWN / 1000}s`); + coolDownLogs.updateText(`Cooling down for ${config.BOOT_COOL_DOWN / 1000}s`); // Having the cooldown right at the beginning should hopefully lower the chances of heat // throttling from the previous run (which we have no control over and will be a @@ -220,18 +268,22 @@ const runTests = async () => { await sleep(config.BOOT_COOL_DOWN); coolDownLogs.done(); - server.setTestConfig(testConfig); + server.setTestConfig(suite); - const warmupLogs = Logger.progressInfo(`Running warmup '${testConfig.name}'`); + const warmupLogs = Logger.progressInfo(`Running warmup '${suite.name}'`); - let progressText = `Warmup for suite '${testConfig.name}' [${testIndex + 1}/${numOfTests}]\n`; + let progressText = `Warmup for suite '${suite.name}' [${suiteIndex + 1}/${suites.length}]\n`; warmupLogs.updateText(progressText); - await restartApp(); + Logger.log('Killing main app'); + await killApp('android', config.MAIN_APP_PACKAGE); + Logger.log('Launching main app'); + await launchApp('android', config.MAIN_APP_PACKAGE); await withFailTimeout( new Promise((resolve) => { const cleanup = server.addTestDoneListener(() => { + Logger.log('Main warm up ready ✅'); cleanup(); resolve(); }); @@ -239,25 +291,68 @@ const runTests = async () => { progressText, ); + Logger.log('Killing main app'); + await killApp('android', config.MAIN_APP_PACKAGE); + + Logger.log('Killing delta app'); + await killApp('android', config.DELTA_APP_PACKAGE); + Logger.log('Launching delta app'); + await launchApp('android', config.DELTA_APP_PACKAGE); + + await withFailTimeout( + new Promise((resolve) => { + const cleanup = server.addTestDoneListener(() => { + Logger.log('Delta warm up ready ✅'); + cleanup(); + resolve(); + }); + }), + progressText, + ); + + Logger.log('Killing delta app'); + await killApp('android', config.DELTA_APP_PACKAGE); + warmupLogs.done(); // We run each test multiple time to average out the results const testLog = Logger.progressInfo(''); for (let i = 0; i < config.RUNS; i++) { - progressText = `Suite '${testConfig.name}' [${testIndex + 1}/${numOfTests}], iteration [${i + 1}/${config.RUNS}]\n`; + progressText = `Suite '${suite.name}' [${suiteIndex + 1}/${suites.length}], iteration [${i + 1}/${config.RUNS}]\n`; testLog.updateText(progressText); - Logger.log('Killing app...'); - await killApp('android', config.APP_PACKAGE); + Logger.log('Killing delta app'); + await killApp('android', config.DELTA_APP_PACKAGE); - testLog.updateText(`Coolin down phone 🧊 ${config.SUITE_COOL_DOWN / 1000}s\n`); + Logger.log('Killing main app'); + await killApp('android', config.MAIN_APP_PACKAGE); - // Adding the cool down between booting the app again, had the side-effect of actually causing a cold boot, - // which increased TTI/bundle load JS times significantly but also stabilized standard deviation. - await sleep(config.SUITE_COOL_DOWN); + Logger.log('Starting main app'); + await launchApp('android', config.MAIN_APP_PACKAGE); - Logger.log('Starting app...'); - await launchApp('android', config.APP_PACKAGE); + // Wait for a test to finish by waiting on its done call to the http server + try { + await withFailTimeout( + new Promise((resolve) => { + const cleanup = server.addTestDoneListener(() => { + Logger.log(`Test iteration ${i + 1} done!`); + cleanup(); + resolve(); + }); + }), + progressText, + ); + } catch (e) { + // When we fail due to a timeout it's interesting to take a screenshot of the emulator to see whats going on + testLog.done(); + throw e; // Rethrow to abort execution + } + + Logger.log('Killing main app'); + await killApp('android', config.MAIN_APP_PACKAGE); + + Logger.log('Starting delta app'); + await launchApp('android', config.DELTA_APP_PACKAGE); // Wait for a test to finish by waiting on its done call to the http server try { @@ -283,16 +378,8 @@ const runTests = async () => { // Calculate statistics and write them to our work file progressLog = Logger.progressInfo('Calculating statics and writing results'); - for (const testName of _.keys(resultsByTestName)) { - const stats = math.getStats(resultsByTestName[testName]); - await writeTestStats( - { - name: testName, - ...stats, - }, - OUTPUT_FILE, - ); - } + compare(results.main, results.delta, `${config.OUTPUT_DIR}/output.md`); + progressLog.done(); await server.stop(); diff --git a/tests/e2e/utils/installApp.js b/tests/e2e/utils/installApp.js index ff961940826a..3741e459ea83 100644 --- a/tests/e2e/utils/installApp.js +++ b/tests/e2e/utils/installApp.js @@ -1,4 +1,3 @@ -const {APP_PACKAGE} = require('../config'); const execAsync = require('./execAsync'); const Logger = require('./logger'); @@ -11,7 +10,7 @@ const Logger = require('./logger'); * @param {String} path * @returns {Promise} */ -module.exports = function (platform = 'android', packageName = APP_PACKAGE, path) { +module.exports = function (platform = 'android', packageName, path) { if (platform !== 'android') { throw new Error(`installApp() missing implementation for platform: ${platform}`); }