Pier Angelo Vendrame pushed to branch tor-browser-128.6.0esr-14.5-1 at The Tor Project / Applications / Tor Browser

Commits:

4 changed files:

Changes:

  • .gitlab/ci/jobs/startup-test/startup-test-android.py
    1
    +#!/usr/bin/env python3
    
    2
    +import argparse
    
    3
    +import json
    
    4
    +import os
    
    5
    +import sys
    
    6
    +import time
    
    7
    +from datetime import datetime, timedelta
    
    8
    +from enum import Enum
    
    9
    +
    
    10
    +import requests
    
    11
    +
    
    12
    +"""
    
    13
    +This script runs Android tests on BrowserStack using the BrowserStack App Automate Espresso API.
    
    14
    +
    
    15
    +Usage:
    
    16
    +    startup-test-android.py --devices <devices> [--tests <tests>] [--app_file_path <app_file_path>] [--test_file_path <test_file_path>]
    
    17
    +
    
    18
    +Arguments:
    
    19
    +    --devices: Comma-separated list of devices to test on (required).
    
    20
    +    --tests: Comma-separated list of tests to run (optional). If not provided, all tests will run.
    
    21
    +    --app_file_path: Path to the app file (optional). If not provided, yesterday's nightly will be downloaded.
    
    22
    +    --test_file_path: Path to the test file (optional). If not provided, yesterday's nightly will be downloaded.
    
    23
    +
    
    24
    +Environment Variables:
    
    25
    +    BROWSERSTACK_USERNAME: BrowserStack username (required).
    
    26
    +    BROWSERSTACK_API_KEY: BrowserStack API key (required).
    
    27
    +
    
    28
    +Description:
    
    29
    +    - If app and test file paths are not provided, the script downloads the latest nightly build from the Tor Project.
    
    30
    +    - Uploads the app and test files to BrowserStack.
    
    31
    +    - Triggers the test run on the specified devices.
    
    32
    +    - Polls for the test status until completion or timeout.
    
    33
    +    - Prints the test results and exits with an appropriate status code.
    
    34
    +"""
    
    35
    +
    
    36
    +parser = argparse.ArgumentParser(
    
    37
    +    description="Run Android startup tests on BrowserStack."
    
    38
    +)
    
    39
    +parser.add_argument(
    
    40
    +    "--devices",
    
    41
    +    type=str,
    
    42
    +    help="Comma-separated list of devices to test on",
    
    43
    +    required=True,
    
    44
    +)
    
    45
    +parser.add_argument("--tests", type=str, help="Comma-separated list of tests to run")
    
    46
    +parser.add_argument("--app_file_path", type=str, help="Path to the app file")
    
    47
    +parser.add_argument("--test_file_path", type=str, help="Path to the test file")
    
    48
    +
    
    49
    +args = parser.parse_args()
    
    50
    +
    
    51
    +if args.app_file_path:
    
    52
    +    app_file_path = args.app_file_path
    
    53
    +    test_file_path = args.test_file_path
    
    54
    +    if not test_file_path:
    
    55
    +        print(
    
    56
    +            "\033[1;31mIf either app or test file paths are provided, both must be provided.\033[0m"
    
    57
    +        )
    
    58
    +else:
    
    59
    +
    
    60
    +    def download_file(url, dest_path):
    
    61
    +        try:
    
    62
    +            response = requests.get(url, stream=True)
    
    63
    +            response.raise_for_status()
    
    64
    +            with open(dest_path, "wb") as f:
    
    65
    +                for chunk in response.iter_content(chunk_size=8192):
    
    66
    +                    f.write(chunk)
    
    67
    +        except Exception as e:
    
    68
    +            print(f"\033[1;31mFailed to download file from {url}.\033[0m")
    
    69
    +            print(e)
    
    70
    +            sys.exit(1)
    
    71
    +
    
    72
    +    yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y.%m.%d")
    
    73
    +    download_url_base = f"https://nightlies.tbb.torproject.org/nightly-builds/tor-browser-builds/tbb-nightly.{yesterday}/nightly-android-aarch64"
    
    74
    +    print(
    
    75
    +        f"No file paths provided, downloading yesterday's nightly from {download_url_base}"
    
    76
    +    )
    
    77
    +
    
    78
    +    app_file_url = f"{download_url_base}/tor-browser-noopt-android-aarch64-tbb-nightly.{yesterday}.apk"
    
    79
    +    test_file_url = (
    
    80
    +        f"{download_url_base}/tor-browser-tbb-nightly.{yesterday}-androidTest.apk"
    
    81
    +    )
    
    82
    +
    
    83
    +    # BrowserStack will fail if there are `.` in the file name other than before the extension.
    
    84
    +    yesterday = yesterday.replace(".", "-")
    
    85
    +    app_file_path = f"/tmp/nightly-{yesterday}.apk"
    
    86
    +    test_file_path = f"/tmp/nightly-test-{yesterday}.apk"
    
    87
    +
    
    88
    +    download_file(app_file_url, app_file_path)
    
    89
    +    download_file(test_file_url, test_file_path)
    
    90
    +
    
    91
    +devices = [device.strip() for device in args.devices.split(",")]
    
    92
    +tests = args.tests.split(",") if args.tests else []
    
    93
    +
    
    94
    +browserstack_username = os.getenv("BROWSERSTACK_USERNAME")
    
    95
    +browserstack_api_key = os.getenv("BROWSERSTACK_API_KEY")
    
    96
    +if not browserstack_username or not browserstack_api_key:
    
    97
    +    print(
    
    98
    +        "\033[1;31mEnvironment variables BROWSERSTACK_USERNAME and BROWSERSTACK_API_KEY must be set.\033[0m"
    
    99
    +    )
    
    100
    +    sys.exit(1)
    
    101
    +
    
    102
    +# Upload app file
    
    103
    +with open(app_file_path, "rb") as app_file:
    
    104
    +    response = requests.post(
    
    105
    +        "https://api-cloud.browserstack.com/app-automate/espresso/v2/app",
    
    106
    +        auth=(browserstack_username, browserstack_api_key),
    
    107
    +        files={"file": app_file},
    
    108
    +    )
    
    109
    +
    
    110
    +if response.status_code != 200:
    
    111
    +    print("\033[1;31mFailed to upload app file.\033[0m")
    
    112
    +    print(response.text)
    
    113
    +    sys.exit(1)
    
    114
    +
    
    115
    +bs_app_url = response.json().get("app_url")
    
    116
    +print("\033[1;32mSuccessfully uploaded app file.\033[0m")
    
    117
    +print(f"App URL: {bs_app_url}")
    
    118
    +
    
    119
    +# Upload test file
    
    120
    +with open(test_file_path, "rb") as test_file:
    
    121
    +    response = requests.post(
    
    122
    +        "https://api-cloud.browserstack.com/app-automate/espresso/v2/test-suite",
    
    123
    +        auth=(browserstack_username, browserstack_api_key),
    
    124
    +        files={"file": test_file},
    
    125
    +    )
    
    126
    +
    
    127
    +if response.status_code != 200:
    
    128
    +    print("\033[1;31mFailed to upload test file.\033[0m")
    
    129
    +    print(response.text)
    
    130
    +    sys.exit(1)
    
    131
    +
    
    132
    +bs_test_url = response.json().get("test_suite_url")
    
    133
    +print("\033[1;32mSuccessfully uploaded test file.\033[0m")
    
    134
    +print(f"Test URL: {bs_test_url}")
    
    135
    +
    
    136
    +# Trigger tests
    
    137
    +test_params = {
    
    138
    +    "app": bs_app_url,
    
    139
    +    "testSuite": bs_test_url,
    
    140
    +    "devices": devices,
    
    141
    +    "class": tests,
    
    142
    +}
    
    143
    +
    
    144
    +response = requests.post(
    
    145
    +    "https://api-cloud.browserstack.com/app-automate/espresso/v2/build",
    
    146
    +    auth=(browserstack_username, browserstack_api_key),
    
    147
    +    headers={"Content-Type": "application/json"},
    
    148
    +    data=json.dumps(test_params),
    
    149
    +)
    
    150
    +
    
    151
    +if response.status_code != 200:
    
    152
    +    print("\033[1;31mFailed to trigger test run.\033[0m")
    
    153
    +    print(response.text)
    
    154
    +    sys.exit(1)
    
    155
    +
    
    156
    +build_id = response.json().get("build_id")
    
    157
    +print("\033[1;32mSuccessfully triggered test run.\033[0m")
    
    158
    +print(
    
    159
    +    f"Test status also available at: https://app-automate.browserstack.com/builds/{build_id}\n==="
    
    160
    +)
    
    161
    +
    
    162
    +# Poll for status
    
    163
    +POLLING_TIMEOUT = 30 * 60  # 30min
    
    164
    +POLLING_INTERVAL = 30  # 30s
    
    165
    +
    
    166
    +
    
    167
    +class TestStatus(Enum):
    
    168
    +    QUEUED = "queued"
    
    169
    +    RUNNING = "running"
    
    170
    +    ERROR = "error"
    
    171
    +    FAILED = "failed"
    
    172
    +    PASSED = "passed"
    
    173
    +    TIMED_OUT = "timed out"
    
    174
    +    SKIPPED = "skipped"
    
    175
    +
    
    176
    +    @classmethod
    
    177
    +    def from_string(cls, s):
    
    178
    +        try:
    
    179
    +            return cls[s.upper().replace(" ", "_")]
    
    180
    +        except KeyError:
    
    181
    +            raise ValueError(f"\033[1;31m'{s}' is not a valid test status.\033[0m")
    
    182
    +
    
    183
    +    def is_terminal(self):
    
    184
    +        return self not in {TestStatus.QUEUED, TestStatus.RUNNING}
    
    185
    +
    
    186
    +    def is_success(self):
    
    187
    +        return self in {TestStatus.PASSED, TestStatus.SKIPPED}
    
    188
    +
    
    189
    +    def color_print(self):
    
    190
    +        if self == TestStatus.PASSED:
    
    191
    +            return f"\033[1;32m{self.value}\033[0m"
    
    192
    +
    
    193
    +        if self in {TestStatus.ERROR, TestStatus.FAILED, TestStatus.TIMED_OUT}:
    
    194
    +            return f"\033[1;31m{self.value}\033[0m"
    
    195
    +
    
    196
    +        if self == TestStatus.SKIPPED:
    
    197
    +            return f"\033[1;33m{self.value}\033[0m"
    
    198
    +
    
    199
    +        return self.value
    
    200
    +
    
    201
    +
    
    202
    +start_time = time.time()
    
    203
    +elapsed_time = 0
    
    204
    +test_status = None
    
    205
    +while elapsed_time <= POLLING_TIMEOUT:
    
    206
    +    response = requests.get(
    
    207
    +        f"https://api-cloud.browserstack.com/app-automate/espresso/v2/builds/{build_id}",
    
    208
    +        auth=(browserstack_username, browserstack_api_key),
    
    209
    +    )
    
    210
    +
    
    211
    +    if response.status_code != 200:
    
    212
    +        print("\033[1;31mFailed to get test status.\033[0m")
    
    213
    +        print(response.text)
    
    214
    +        sys.exit(1)
    
    215
    +
    
    216
    +    test_status = TestStatus.from_string(response.json().get("status"))
    
    217
    +    if test_status.is_terminal():
    
    218
    +        print(f"===\nTest finished. Result: {test_status.color_print()}")
    
    219
    +        break
    
    220
    +    else:
    
    221
    +        elapsed_time = time.time() - start_time
    
    222
    +        print(f"Test status: {test_status.value} ({elapsed_time:.2f}s)")
    
    223
    +
    
    224
    +        if elapsed_time > POLLING_TIMEOUT:
    
    225
    +            print("===\n\033[1;33mWaited for tests for too long.\033[0m")
    
    226
    +            break
    
    227
    +
    
    228
    +        time.sleep(POLLING_INTERVAL)
    
    229
    +
    
    230
    +if test_status is None or not test_status.is_success():
    
    231
    +    sys.exit(1)

  • .gitlab/ci/jobs/startup-test/startup-test.yml
    ... ... @@ -49,3 +49,15 @@ startup-test-linux:
    49 49
         - ./mach python .gitlab/ci/jobs/startup-test/startup-test.py --platform linux --arch x86_64 --browser $BROWSER
    
    50 50
       rules:
    
    51 51
         - if: $CI_PIPELINE_SOURCE == "schedule"
    
    52
    +
    
    53
    +startup-test-android:
    
    54
    +  extends: .with-local-repo-bash
    
    55
    +  image: $IMAGE_PATH
    
    56
    +  stage: startup-test
    
    57
    +  interruptible: true
    
    58
    +  tags:
    
    59
    +    -  firefox
    
    60
    +  script:
    
    61
    +    - ./mach python .gitlab/ci/jobs/startup-test/startup-test-android.py --devices "Samsung Galaxy S23-13.0, Samsung Galaxy S8-7.0" --tests org.mozilla.fenix.LaunchTest
    
    62
    +  rules:
    
    63
    +    - if: $CI_PIPELINE_SOURCE == "schedule"

  • mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/AppStartupTest.kt
    1
    +package org.mozilla.fenix
    
    2
    +
    
    3
    +import androidx.test.ext.junit.rules.ActivityScenarioRule
    
    4
    +import androidx.test.ext.junit.runners.AndroidJUnit4
    
    5
    +import androidx.test.platform.app.InstrumentationRegistry
    
    6
    +import androidx.test.uiautomator.UiDevice
    
    7
    +import org.junit.Rule
    
    8
    +import org.junit.Test
    
    9
    +import org.junit.runner.RunWith
    
    10
    +import java.util.concurrent.CountDownLatch
    
    11
    +import java.util.concurrent.TimeUnit
    
    12
    +
    
    13
    +
    
    14
    +@RunWith(AndroidJUnit4::class)
    
    15
    +class LaunchTest {
    
    16
    +
    
    17
    +    @get:Rule
    
    18
    +    var rule: ActivityScenarioRule<HomeActivity> = ActivityScenarioRule(HomeActivity::class.java)
    
    19
    +
    
    20
    +    @Test
    
    21
    +    fun appLaunchesWithoutCrash() {
    
    22
    +        val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
    
    23
    +        device.waitForIdle()
    
    24
    +
    
    25
    +        // Simulate a 30-second delay
    
    26
    +        val latch = CountDownLatch(1)
    
    27
    +        Thread {
    
    28
    +            try {
    
    29
    +                Thread.sleep(30_000)
    
    30
    +                latch.countDown()
    
    31
    +            } catch (e: InterruptedException) {
    
    32
    +                e.printStackTrace()
    
    33
    +            }
    
    34
    +        }.start()
    
    35
    +
    
    36
    +        latch.await(30, TimeUnit.SECONDS)
    
    37
    +
    
    38
    +        // If we got here, the app did not crash. Test passed.
    
    39
    +    }
    
    40
    +}

  • mobile/android/fenix/app/src/main/java/org/mozilla/fenix/FenixApplication.kt
    ... ... @@ -192,7 +192,6 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
    192 192
     
    
    193 193
         fun terminate() {
    
    194 194
             onTerminate()
    
    195
    -        System.exit(0)
    
    196 195
         }
    
    197 196
     
    
    198 197
         override fun onTerminate() {