From 44274552327d2bbc11351920ca66624df952de1e Mon Sep 17 00:00:00 2001 From: Wolfgang Mathurin Date: Wed, 23 Sep 2026 09:29:14 -0700 Subject: [PATCH 1/2] @W-24278591: Complete AuthFlowTester nightly coverage --- .github/workflows/reusable-ui-workflow.yaml | 246 ++++++++++++++---- .../NativeSampleApps/AuthFlowTester/README.md | 15 +- 2 files changed, 205 insertions(+), 56 deletions(-) diff --git a/.github/workflows/reusable-ui-workflow.yaml b/.github/workflows/reusable-ui-workflow.yaml index 5d631f786d..87e635afe9 100644 --- a/.github/workflows/reusable-ui-workflow.yaml +++ b/.github/workflows/reusable-ui-workflow.yaml @@ -228,13 +228,15 @@ jobs: --test-targets "class com.salesforce.samples.authflowtester.MultiUserLoginTests" \ --timeout=45m \ --num-flaky-test-attempts=1 - - name: Run All Single User Tests + - name: Run Login Tests continue-on-error: true if: ${{ ! inputs.is_pr }} env: RUN_NUMBER: ${{ github.run_number }} run: | - GCLOUD_RESULTS_DIR="authflowtester-single-user-build-${RUN_NUMBER}" + # Keep the same disjoint groups as a full PR run. The previous single-user + # invocation reached Firebase's 60-minute limit and silently omitted late tests. + GCLOUD_RESULTS_DIR="authflowtester-nightly-login-build-${RUN_NUMBER}" DEVICE_ARGS=() for LEVEL in $FULL_API_RANGE; do DEVICE_MODEL="MediumPhone.arm" @@ -251,7 +253,69 @@ jobs: --environment-variables clearPackageData=true \ --app "native/NativeSampleApps/AuthFlowTester/build/outputs/apk/debug/AuthFlowTester-debug.apk" \ --test "native/NativeSampleApps/AuthFlowTester/build/outputs/apk/androidTest/debug/AuthFlowTester-debug-androidTest.apk" \ - --test-targets "notClass com.salesforce.samples.authflowtester.MultiUserLoginTests" \ + --test-targets "notClass com.salesforce.samples.authflowtester.MultiUserLoginTests,notClass com.salesforce.samples.authflowtester.DPoPLoginTests,notClass com.salesforce.samples.authflowtester.LoginWithRestartTests,notClass com.salesforce.samples.authflowtester.RTRLoginTests,notClass com.salesforce.samples.authflowtester.RefreshTokenMigrationTests,notClass com.salesforce.samples.authflowtester.WelcomeLoginTests" \ + "${DEVICE_ARGS[@]}" \ + --directories-to-pull=/sdcard \ + --results-dir="${GCLOUD_RESULTS_DIR}" \ + --results-history-name=AuthFlowTester \ + --no-performance-metrics \ + --num-flaky-test-attempts=1 \ + --timeout=60m || true + - name: Run Welcome Discovery Tests + continue-on-error: true + if: ${{ ! inputs.is_pr }} + env: + RUN_NUMBER: ${{ github.run_number }} + run: | + GCLOUD_RESULTS_DIR="authflowtester-nightly-welcome-build-${RUN_NUMBER}" + DEVICE_ARGS=() + for LEVEL in $FULL_API_RANGE; do + DEVICE_MODEL="MediumPhone.arm" + if [ "$PS16K_MIN_API_VERSION" -le "$LEVEL" ]; then + DEVICE_MODEL="MediumPhone_ps16k.arm" + fi + DEVICE_ARGS+=(--device "model=${DEVICE_MODEL},version=${LEVEL},locale=en,orientation=portrait") + done + + gcloud firebase test android run \ + --project mobile-apps-firebase-test \ + --type instrumentation \ + --use-orchestrator \ + --environment-variables clearPackageData=true \ + --app "native/NativeSampleApps/AuthFlowTester/build/outputs/apk/debug/AuthFlowTester-debug.apk" \ + --test "native/NativeSampleApps/AuthFlowTester/build/outputs/apk/androidTest/debug/AuthFlowTester-debug-androidTest.apk" \ + --test-targets "class com.salesforce.samples.authflowtester.WelcomeLoginTests" \ + "${DEVICE_ARGS[@]}" \ + --directories-to-pull=/sdcard \ + --results-dir="${GCLOUD_RESULTS_DIR}" \ + --results-history-name=AuthFlowTester \ + --no-performance-metrics \ + --num-flaky-test-attempts=1 \ + --timeout=10m || true + - name: Run Token Lifecycle Tests + continue-on-error: true + if: ${{ ! inputs.is_pr }} + env: + RUN_NUMBER: ${{ github.run_number }} + run: | + GCLOUD_RESULTS_DIR="authflowtester-nightly-token-lifecycle-build-${RUN_NUMBER}" + DEVICE_ARGS=() + for LEVEL in $FULL_API_RANGE; do + DEVICE_MODEL="MediumPhone.arm" + if [ "$PS16K_MIN_API_VERSION" -le "$LEVEL" ]; then + DEVICE_MODEL="MediumPhone_ps16k.arm" + fi + DEVICE_ARGS+=(--device "model=${DEVICE_MODEL},version=${LEVEL},locale=en,orientation=portrait") + done + + gcloud firebase test android run \ + --project mobile-apps-firebase-test \ + --type instrumentation \ + --use-orchestrator \ + --environment-variables clearPackageData=true \ + --app "native/NativeSampleApps/AuthFlowTester/build/outputs/apk/debug/AuthFlowTester-debug.apk" \ + --test "native/NativeSampleApps/AuthFlowTester/build/outputs/apk/androidTest/debug/AuthFlowTester-debug-androidTest.apk" \ + --test-targets "class com.salesforce.samples.authflowtester.DPoPLoginTests,class com.salesforce.samples.authflowtester.LoginWithRestartTests,class com.salesforce.samples.authflowtester.RTRLoginTests,class com.salesforce.samples.authflowtester.RefreshTokenMigrationTests" \ "${DEVICE_ARGS[@]}" \ --directories-to-pull=/sdcard \ --results-dir="${GCLOUD_RESULTS_DIR}" \ @@ -265,7 +329,7 @@ jobs: env: RUN_NUMBER: ${{ github.run_number }} run: | - GCLOUD_RESULTS_DIR="authflowtester-multi-user-build-${RUN_NUMBER}" + GCLOUD_RESULTS_DIR="authflowtester-nightly-multi-user-build-${RUN_NUMBER}" DEVICE_ARGS=() for LEVEL in $FULL_API_RANGE; do DEVICE_MODEL="MediumPhone.arm" @@ -289,7 +353,7 @@ jobs: --results-history-name=AuthFlowTester \ --no-performance-metrics \ --num-flaky-test-attempts=1 \ - --timeout=30m || true + --timeout=60m || true - name: Copy Test Results continue-on-error: true if: success() || failure() @@ -314,8 +378,8 @@ jobs: gsutil cp "${EXECUTION_DIR}/instrumentation.results" \ "firebase_results/${OUTPUT_PREFIX}_api_${API_LEVEL}_instrumentation.results" done - # Pass 2: merge failed and missing rerun cases. A crashed orchestrator process can - # omit a test from the original XML entirely even though AndroidJUnitRunner found it. + # Pass 2: replace original cases with their final retry outcome. A crashed + # orchestrator process can also omit a test from the original XML entirely. for RESULT_FILE in $(gsutil ls "${BUCKET_PATH}/*/test_result_1.xml" 2>/dev/null | grep "rerun"); do DEVICE_DIR=$(echo "${RESULT_FILE}" | sed 's|.*/\([^/]*\)/test_result_1.xml|\1|') API_LEVEL=$(echo "${DEVICE_DIR}" | sed 's/.*-\([0-9]*\)-.*/\1/') @@ -323,30 +387,56 @@ jobs: ORIG_FILE="firebase_results/${OUTPUT_PREFIX}_api_${API_LEVEL}_test_result.xml" gsutil cp "${RESULT_FILE}" "${RERUN_TMP}" python3 - "${ORIG_FILE}" "${RERUN_TMP}" "${ORIG_FILE}" << 'PYEOF' - import sys, xml.etree.ElementTree as ET - orig = ET.parse(sys.argv[1]) - rerun = ET.parse(sys.argv[2]) - def suite(t): - r = t.getroot() - return r if r.tag == 'testsuite' else r.find('testsuite') - os_el, rs_el = suite(orig), suite(rerun) - failed_keys = set() - existing_keys = set() - for tc in os_el.findall('testcase'): - key = f"{tc.get('name','')}|{tc.get('classname','')}|{tc.get('file','')}" - existing_keys.add(key) - if tc.find('failure') is not None or tc.find('error') is not None: - failed_keys.add(key) - added = 0 - for tc in rs_el.findall('testcase'): - key = f"{tc.get('name','')}|{tc.get('classname','')}|{tc.get('file','')}" - if key in failed_keys or key not in existing_keys: - os_el.append(tc) - existing_keys.add(key) - added += 1 - os_el.set('tests', str(int(os_el.get('tests','0')) + added)) - with open(sys.argv[3], 'w') as f: - f.write(ET.tostring(orig.getroot(), encoding='unicode')) + import sys + import xml.etree.ElementTree as ET + from pathlib import Path + + MAX_JUNIT_BYTES = 25 * 1024 * 1024 + + def parse_bounded(path_value): + path = Path(path_value) + if path.stat().st_size > MAX_JUNIT_BYTES: + raise ValueError(f"JUnit result exceeds {MAX_JUNIT_BYTES} bytes: {path.name}") + data = path.read_bytes() + declaration_sample = data[:256 * 1024].upper() + if b' /dev/null 2>&1; then - copy_results_by_api_level "${SINGLE_PATH}" "single-user" - fi - - MULTI_PATH="${BUCKET}/authflowtester-multi-user-build-${RUN_NUMBER}" - if gsutil ls "${MULTI_PATH}" > /dev/null 2>&1; then - copy_results_by_api_level "${MULTI_PATH}" "multi-user" - fi + for GROUP in login welcome token-lifecycle multi-user; do + NIGHTLY_PATH="${BUCKET}/authflowtester-nightly-${GROUP}-build-${RUN_NUMBER}" + if gsutil ls "${NIGHTLY_PATH}" > /dev/null 2>&1; then + copy_results_by_api_level "${NIGHTLY_PATH}" "nightly-${GROUP}" + fi + done fi - - name: Validate Complete PR Test Results - if: ${{ !cancelled() && inputs.is_pr && inputs.run_all_ui_tests }} + - name: Validate Complete Test Results + if: ${{ !cancelled() && (!inputs.is_pr || inputs.run_all_ui_tests) }} + env: + IS_PR: ${{ inputs.is_pr }} run: | python3 - << 'PYEOF' import glob + import os import re import sys import xml.etree.ElementTree as ET + from pathlib import Path + + MAX_JUNIT_BYTES = 25 * 1024 * 1024 - expected_prefixes = {"pr-login", "pr-welcome", "pr-token-lifecycle", "pr-multi-user"} + def parse_bounded(path_value): + path = Path(path_value) + if path.stat().st_size > MAX_JUNIT_BYTES: + raise ValueError(f"JUnit result exceeds {MAX_JUNIT_BYTES} bytes: {path.name}") + data = path.read_bytes() + declaration_sample = data[:256 * 1024].upper() + if b" Date: Wed, 23 Sep 2026 13:12:23 -0700 Subject: [PATCH 2/2] @W-24278591: Surface retry merge failures --- .github/workflows/reusable-ui-workflow.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/reusable-ui-workflow.yaml b/.github/workflows/reusable-ui-workflow.yaml index 87e635afe9..34f840b71a 100644 --- a/.github/workflows/reusable-ui-workflow.yaml +++ b/.github/workflows/reusable-ui-workflow.yaml @@ -386,7 +386,7 @@ jobs: RERUN_TMP="firebase_results/${OUTPUT_PREFIX}_api_${API_LEVEL}_rerun_tmp.xml" ORIG_FILE="firebase_results/${OUTPUT_PREFIX}_api_${API_LEVEL}_test_result.xml" gsutil cp "${RESULT_FILE}" "${RERUN_TMP}" - python3 - "${ORIG_FILE}" "${RERUN_TMP}" "${ORIG_FILE}" << 'PYEOF' + if ! python3 - "${ORIG_FILE}" "${RERUN_TMP}" "${ORIG_FILE}" << 'PYEOF' import sys import xml.etree.ElementTree as ET from pathlib import Path @@ -438,6 +438,9 @@ jobs: original_suite.set('skipped', str(sum(tc.find('skipped') is not None for tc in testcases))) Path(sys.argv[3]).write_bytes(ET.tostring(orig.getroot(), encoding='utf-8', xml_declaration=True)) PYEOF + then + echo "::warning title=Retry result merge failed::${OUTPUT_PREFIX} API ${API_LEVEL}: keeping the original Firebase result; see the Python error above" + fi rm "${RERUN_TMP}" done }