tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

startup-test-android.py (7737B)


      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)