Skip to content
Snippets Groups Projects
Commit b5b135fd authored by Beatriz Rizental's avatar Beatriz Rizental Committed by Pier Angelo Vendrame
Browse files

Add CI for Tor Browser

parent f597f9dc
Branches
Tags
1 merge request!1500TB 43415: Rebased onto 134.0a1
stages:
- update-container-images
- lint
- startup-test
- update-translations
variables:
......@@ -9,4 +11,6 @@ variables:
include:
- local: '.gitlab/ci/mixins.yml'
- local: '.gitlab/ci/jobs/lint/lint.yml'
- local: '.gitlab/ci/jobs/startup-test/startup-test.yml'
- local: '.gitlab/ci/jobs/update-containers.yml'
- local: '.gitlab/ci/jobs/update-translations.yml'
# This image is published in containers.torproject.org/tpo/applications/tor-browser/base
#
# Whenever there are changes to this file,
# they are autopublished on merge to the tpo/applications/tor-browser repository.
#
# The image is updated roughly once a monce when the tor-browser repository is rebased.
FROM containers.torproject.org/tpo/tpa/base-images/python:bookworm
RUN apt-get update && apt-get install -y \
clang \
curl \
git \
libasound2-dev \
libdbus-glib-1-dev \
libgtk-3-dev \
libpango1.0-dev \
libpulse-dev \
libx11-xcb-dev \
libxcomposite-dev \
libxcursor-dev \
libxdamage-dev \
libxi-dev \
libxrandr-dev \
libxtst-dev \
make \
m4 \
mercurial \
nasm \
pkgconf \
unzip \
x11-utils \
xvfb \
xz-utils \
wget && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY taskcluster/docker/recipes/install-node.sh ./install-node.sh
RUN chmod +x install-node.sh
RUN ./install-node.sh
RUN rm ./install-node.sh
COPY taskcluster/kinds/fetch/toolchains.yml ./toolchains.yml
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain $(grep -oP 'rust-\K[0-9.]+(?=:)' ./toolchains.yml)
RUN $HOME/.cargo/bin/cargo install cbindgen --version $(grep -oP 'cbindgen-\K[0-9.]+(?=:)' ./toolchains.yml)
RUN rm ./toolchains.yml
CMD ["/bin/bash"]
FROM debian:latest
# Base image which includes all* dependencies checked by ./mach configure.
#
# * Actually not all dependencies. WASM sandboxed depencies were left out for now.
# This installs all dependencies checked by `./mach configure --without-wasm-sandboxed-libraries`.
#
# # Building and publishing
#
# Whenever this file changes, the updated Docker image must be built and published _manually_ to
# the tor-browser container registry (https://gitlab.torproject.org/tpo/applications/tor-browser/container_registry/185).
#
# This image copies a script from the taskcluster/ folder, which requires it
# to be built from a folder which is a parent of the taskcluster/ folder.
#
# To build, run:
#
# ```bash
# docker build \
# -f <PATH_TO_DOCKERFILE> \
# -t <REGISTRY_URL>/<IMAGE_NAME>:<IMAGE_TAG>
# .
# ```
#
# For example, when building from the root of this repository to the main tor-browser repository
# and assuming image name to be "base" and tag "latest" -- which is the current terminology:
#
# ```bash
# docker build \
# -f .gitlab/ci/docker/Dockerfile \
# -t containers.torproject.org/tpo/applications/tor-browser/base:latest
# .
# ```
RUN apt-get update && apt-get install -y \
clang \
curl \
git \
libasound2-dev \
libdbus-glib-1-dev \
libgtk-3-dev \
libpango1.0-dev \
libpulse-dev \
libx11-xcb-dev \
libxcomposite-dev \
libxcursor-dev \
libxdamage-dev \
libxi-dev \
libxrandr-dev \
libxtst-dev \
m4 \
mercurial \
nasm \
pkg-config \
python3 \
python3-pip \
unzip \
wget
COPY taskcluster/docker/recipes/install-node.sh /scripts/install-node.sh
RUN chmod +x /scripts/install-node.sh
RUN /scripts/install-node.sh
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y
RUN $HOME/.cargo/bin/cargo install cbindgen
WORKDIR /app
CMD ["/bin/bash"]
#!/usr/bin/env python3
import argparse
import json
import os
import sys
import time
from datetime import datetime, timedelta
from enum import Enum
import requests
"""
This script runs Android tests on BrowserStack using the BrowserStack App Automate Espresso API.
Usage:
startup-test-android.py --devices <devices> [--tests <tests>] [--app_file_path <app_file_path>] [--test_file_path <test_file_path>]
Arguments:
--devices: Comma-separated list of devices to test on (required).
--tests: Comma-separated list of tests to run (optional). If not provided, all tests will run.
--app_file_path: Path to the app file (optional). If not provided, yesterday's nightly will be downloaded.
--test_file_path: Path to the test file (optional). If not provided, yesterday's nightly will be downloaded.
Environment Variables:
BROWSERSTACK_USERNAME: BrowserStack username (required).
BROWSERSTACK_API_KEY: BrowserStack API key (required).
Description:
- If app and test file paths are not provided, the script downloads the latest nightly build from the Tor Project.
- Uploads the app and test files to BrowserStack.
- Triggers the test run on the specified devices.
- Polls for the test status until completion or timeout.
- Prints the test results and exits with an appropriate status code.
"""
parser = argparse.ArgumentParser(
description="Run Android startup tests on BrowserStack."
)
parser.add_argument(
"--devices",
type=str,
help="Comma-separated list of devices to test on",
required=True,
)
parser.add_argument("--tests", type=str, help="Comma-separated list of tests to run")
parser.add_argument("--app_file_path", type=str, help="Path to the app file")
parser.add_argument("--test_file_path", type=str, help="Path to the test file")
args = parser.parse_args()
if args.app_file_path:
app_file_path = args.app_file_path
test_file_path = args.test_file_path
if not test_file_path:
print(
"\033[1;31mIf either app or test file paths are provided, both must be provided.\033[0m"
)
else:
def download_file(url, dest_path):
try:
response = requests.get(url, stream=True)
response.raise_for_status()
with open(dest_path, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
except Exception as e:
print(f"\033[1;31mFailed to download file from {url}.\033[0m")
print(e)
sys.exit(1)
yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y.%m.%d")
download_url_base = f"https://nightlies.tbb.torproject.org/nightly-builds/tor-browser-builds/tbb-nightly.{yesterday}/nightly-android-aarch64"
print(
f"No file paths provided, downloading yesterday's nightly from {download_url_base}"
)
app_file_url = f"{download_url_base}/tor-browser-noopt-android-aarch64-tbb-nightly.{yesterday}.apk"
test_file_url = (
f"{download_url_base}/tor-browser-tbb-nightly.{yesterday}-androidTest.apk"
)
# BrowserStack will fail if there are `.` in the file name other than before the extension.
yesterday = yesterday.replace(".", "-")
app_file_path = f"/tmp/nightly-{yesterday}.apk"
test_file_path = f"/tmp/nightly-test-{yesterday}.apk"
download_file(app_file_url, app_file_path)
download_file(test_file_url, test_file_path)
devices = [device.strip() for device in args.devices.split(",")]
tests = args.tests.split(",") if args.tests else []
browserstack_username = os.getenv("BROWSERSTACK_USERNAME")
browserstack_api_key = os.getenv("BROWSERSTACK_API_KEY")
if not browserstack_username or not browserstack_api_key:
print(
"\033[1;31mEnvironment variables BROWSERSTACK_USERNAME and BROWSERSTACK_API_KEY must be set.\033[0m"
)
sys.exit(1)
# Upload app file
with open(app_file_path, "rb") as app_file:
response = requests.post(
"https://api-cloud.browserstack.com/app-automate/espresso/v2/app",
auth=(browserstack_username, browserstack_api_key),
files={"file": app_file},
)
if response.status_code != 200:
print("\033[1;31mFailed to upload app file.\033[0m")
print(response.text)
sys.exit(1)
bs_app_url = response.json().get("app_url")
print("\033[1;32mSuccessfully uploaded app file.\033[0m")
print(f"App URL: {bs_app_url}")
# Upload test file
with open(test_file_path, "rb") as test_file:
response = requests.post(
"https://api-cloud.browserstack.com/app-automate/espresso/v2/test-suite",
auth=(browserstack_username, browserstack_api_key),
files={"file": test_file},
)
if response.status_code != 200:
print("\033[1;31mFailed to upload test file.\033[0m")
print(response.text)
sys.exit(1)
bs_test_url = response.json().get("test_suite_url")
print("\033[1;32mSuccessfully uploaded test file.\033[0m")
print(f"Test URL: {bs_test_url}")
# Trigger tests
test_params = {
"app": bs_app_url,
"testSuite": bs_test_url,
"devices": devices,
"class": tests,
}
response = requests.post(
"https://api-cloud.browserstack.com/app-automate/espresso/v2/build",
auth=(browserstack_username, browserstack_api_key),
headers={"Content-Type": "application/json"},
data=json.dumps(test_params),
)
if response.status_code != 200:
print("\033[1;31mFailed to trigger test run.\033[0m")
print(response.text)
sys.exit(1)
build_id = response.json().get("build_id")
print("\033[1;32mSuccessfully triggered test run.\033[0m")
print(
f"Test status also available at: https://app-automate.browserstack.com/builds/{build_id}\n==="
)
# Poll for status
POLLING_TIMEOUT = 30 * 60 # 30min
POLLING_INTERVAL = 30 # 30s
class TestStatus(Enum):
QUEUED = "queued"
RUNNING = "running"
ERROR = "error"
FAILED = "failed"
PASSED = "passed"
TIMED_OUT = "timed out"
SKIPPED = "skipped"
@classmethod
def from_string(cls, s):
try:
return cls[s.upper().replace(" ", "_")]
except KeyError:
raise ValueError(f"\033[1;31m'{s}' is not a valid test status.\033[0m")
def is_terminal(self):
return self not in {TestStatus.QUEUED, TestStatus.RUNNING}
def is_success(self):
return self in {TestStatus.PASSED, TestStatus.SKIPPED}
def color_print(self):
if self == TestStatus.PASSED:
return f"\033[1;32m{self.value}\033[0m"
if self in {TestStatus.ERROR, TestStatus.FAILED, TestStatus.TIMED_OUT}:
return f"\033[1;31m{self.value}\033[0m"
if self == TestStatus.SKIPPED:
return f"\033[1;33m{self.value}\033[0m"
return self.value
start_time = time.time()
elapsed_time = 0
test_status = None
while elapsed_time <= POLLING_TIMEOUT:
response = requests.get(
f"https://api-cloud.browserstack.com/app-automate/espresso/v2/builds/{build_id}",
auth=(browserstack_username, browserstack_api_key),
)
if response.status_code != 200:
print("\033[1;31mFailed to get test status.\033[0m")
print(response.text)
sys.exit(1)
test_status = TestStatus.from_string(response.json().get("status"))
if test_status.is_terminal():
print(f"===\nTest finished. Result: {test_status.color_print()}")
break
else:
elapsed_time = time.time() - start_time
print(f"Test status: {test_status.value} ({elapsed_time:.2f}s)")
if elapsed_time > POLLING_TIMEOUT:
print("===\n\033[1;33mWaited for tests for too long.\033[0m")
break
time.sleep(POLLING_INTERVAL)
if test_status is None or not test_status.is_success():
sys.exit(1)
#!/usr/bin/env python3
import argparse
import subprocess
from datetime import datetime, timedelta
PLATFORM_TO_ARCH = {
"linux": ["x86_64", "i686"],
"macos": ["x86_64", "aarch64"],
"windows": ["x86_64", "i686"],
}
class DynamicArchAction(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
platform = getattr(namespace, "platform", None)
if not platform:
raise argparse.ArgumentError(
self, "The --platform argument must be provided before --arch."
)
valid_archs = PLATFORM_TO_ARCH.get(platform, [])
if values not in valid_archs:
raise argparse.ArgumentError(
self,
f"Invalid architecture '{values}' for platform '{platform}'. "
f"Valid options are: {', '.join(valid_archs)}",
)
setattr(namespace, self.dest, values)
parser = argparse.ArgumentParser(
description="Downloads and executes yesterday's build of Tor or Mullvad browser nightly."
)
parser.add_argument(
"--platform",
required=True,
help="Specify the platform (linux, macos or windows). Must be provided before --arch.",
choices=PLATFORM_TO_ARCH.keys(),
)
parser.add_argument(
"--arch",
required=True,
help="Specify the architecture (validated dynamically based on --platform).",
action=DynamicArchAction,
)
parser.add_argument(
"--browser",
required=True,
choices=["tor", "mullvad"],
help="Specify the browser (tor or mullvad)",
)
args = parser.parse_args()
arch = f"-{args.arch}"
extra = ""
if args.platform == "linux":
archive_extension = "tar.xz"
binary = f"Browser/start-{args.browser}-browser"
elif args.platform == "macos":
archive_extension = "dmg"
# The URL doesn't include the architecture for MacOS,
# because it's a universal build.
arch = ""
if args.browser == "tor":
binary = "Contents/MacOS/firefox"
else:
binary = "Contents/MacOS/mullvadbrowser"
elif args.platform == "windows":
archive_extension = "exe"
if args.browser == "tor":
extra = "-portable"
binary = "Browser/firefox.exe"
else:
binary = "mullvadbrowser.exe"
yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y.%m.%d")
download_url_base = (
"https://nightlies.tbb.torproject.org/nightly-builds/tor-browser-builds"
)
if args.browser == "tor":
download_url = f"{download_url_base}/tbb-nightly.{yesterday}/nightly-{args.platform}{arch}/{args.browser}-browser-{args.platform}{arch}{extra}-tbb-nightly.{yesterday}.{archive_extension}"
else:
download_url = f"{download_url_base}/tbb-nightly.{yesterday}/mullvadbrowser-nightly-{args.platform}{arch}/{args.browser}-browser-{args.platform}{arch}-tbb-nightly.{yesterday}.{archive_extension}"
subprocess.run(
[
"python3",
"testing/mozharness/scripts/does_it_crash.py",
"--run-for",
"30",
"--thing-url",
download_url,
"--thing-to-run",
binary,
]
)
# startup-test-windows:
# extends: .with-local-repo-pwsh
# variables:
# LOCAL_REPO_PATH: "C:\\Users\\windoes\\tor-browser.git"
# stage: startup-test
# interruptible: true
# parallel:
# matrix:
# - BROWSER: ["tor", "mullvad"]
# tags:
# - x86-win11
# script:
# - ./mach python testing/mozbase/setup_development.py
# - ./mach python .gitlab/ci/jobs/startup-test/startup-test.py --platform windows --arch x86_64 --browser $BROWSER
# rules:
# - if: $CI_PIPELINE_SOURCE == "schedule"
# startup-test-macos:
# extends: .with-local-repo-bash
# variables:
# LOCAL_REPO_PATH: "/Users/gitlab-runner/tor-browser.git"
# stage: startup-test
# interruptible: true
# parallel:
# matrix:
# - BROWSER: ["tor", "mullvad"]
# tags:
# - x86-macos
# script:
# - ./mach python testing/mozbase/setup_development.py
# - ./mach python .gitlab/ci/jobs/startup-test/startup-test.py --platform macos --arch x86_64 --browser $BROWSER
# rules:
# - if: $CI_PIPELINE_SOURCE == "schedule"
startup-test-linux:
extends: .with-local-repo-bash
image: $IMAGE_PATH
stage: startup-test
interruptible: true
parallel:
matrix:
- BROWSER: ["tor", "mullvad"]
tags:
- firefox
script:
- Xvfb :99 -screen 0 1400x900x24 &
- export DISPLAY=:99
- ./mach python testing/mozbase/setup_development.py
- ./mach python .gitlab/ci/jobs/startup-test/startup-test.py --platform linux --arch x86_64 --browser $BROWSER
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
startup-test-android:
extends: .with-local-repo-bash
image: $IMAGE_PATH
stage: startup-test
interruptible: true
tags:
- firefox
script:
- ./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
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
build-base-image:
stage: update-container-images
interruptible: true
image: containers.torproject.org/tpo/tpa/base-images/podman:bookworm
script:
- export TAG="${CI_REGISTRY_IMAGE}/base:latest"
- podman login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- podman build --layers=false $IMAGE -t ${TAG} -f .gitlab/ci/containers/base/Containerfile .
- |
echo -e "\e[33mPushing new image to registry as ${TAG}\e[0m"
podman push ${TAG}
rules:
- if: ($CI_COMMIT_BRANCH && $CI_COMMIT_REF_PROTECTED == 'true' && $CI_PROJECT_NAMESPACE == 'tpo/applications' && $CI_PIPELINE_SOURCE == 'push')
changes:
- '.gitlab/ci/containers/base/Containerfile'
- '.gitlab-ci.yml'
......@@ -13,7 +13,70 @@
- if: ($TRANSLATION_FILES != "" && $FORCE_UPDATE_TRANSLATIONS == "true")
variables:
COMBINED_FILES_JSON: "combined-translation-files.json"
TRANSLATION_FILES: ''
TRANSLATION_FILES: '[
{
"name": "brand.ftl",
"where": ["browser/branding/tb-release", "toolkit/torbutton"],
"branding": {
"versions": [
{
"name": "Alpha",
"suffix": "_alpha",
"where": ["browser/branding/tb-alpha"]
},
{
"name": "Nightly",
"suffix": "_nightly",
"where": ["browser/branding/tb-nightly"]
}
],
"ids": [
"-brand-short-name",
"-brand-full-name"
]
},
"branch": "tor-browser",
"directory": "branding"
},
{
"name": "brand.properties",
"where": ["browser/branding/tb-release", "toolkit/torbutton"],
"branding": {
"versions": [
{
"name": "Alpha",
"suffix": "_alpha",
"where": ["browser/branding/tb-alpha"]
},
{
"name": "Nightly",
"suffix": "_nightly",
"where": ["browser/branding/tb-nightly"]
}
],
"ids": [
"brandShortName",
"brandFullName"
]
},
"branch": "tor-browser"
},
{ "name": "tor-browser.ftl", "branch": "tor-browser" },
{ "name": "aboutTBUpdate.dtd", "branch": "tor-browser" },
{ "name": "torbutton.dtd", "branch": "tor-browser" },
{ "name": "onionLocation.properties", "branch": "tor-browser" },
{ "name": "settings.properties", "branch": "tor-browser" },
{ "name": "torbutton.properties", "branch": "tor-browser" },
{ "name": "torConnect.properties", "branch": "tor-browser" },
{ "name": "torlauncher.properties", "branch": "tor-browser" },
{ "name": "base-browser.ftl", "branch": "base-browser" },
{
"name": "torbrowser_strings.xml",
"branch": "fenix-torbrowserstringsxml",
"exclude-legacy": true
}
]'
TRANSLATION_INCLUDE_LEGACY: "true"
combine-en-US-translations:
......
......@@ -352,7 +352,8 @@ def _install_exe(src, dest):
# possibly gets around UAC in vista (still need to run as administrator)
os.environ["__compat_layer"] = "RunAsInvoker"
cmd = '"%s" /extractdir=%s' % (src, os.path.realpath(dest))
cmd = '"%s" /S /D=%s' % (src, os.path.realpath(dest))
# cmd = '"%s" /extractdir=%s' % (src, os.path.realpath(dest))
subprocess.check_call(cmd)
......
......@@ -267,23 +267,28 @@ def main(args=sys.argv[1:]):
os.environ.get("PATH", "").strip(os.path.pathsep),
)
current_file_path = os.path.abspath(__file__)
topobjdir = os.path.dirname(os.path.dirname(os.path.dirname(current_file_path)))
mach = str(os.path.join(topobjdir, "mach"))
# install non-mozbase dependencies
# these need to be installed separately and the --no-deps flag
# subsequently used due to a bug in setuptools; see
# https://bugzilla.mozilla.org/show_bug.cgi?id=759836
pypi_deps = dict([(i, j) for i, j in alldeps.items() if i not in unrolled])
for package, version in pypi_deps.items():
# easy_install should be available since we rely on setuptools
call(["easy_install", version])
# Originally, Mozilla used easy_install here.
# That tool is deprecated, therefore we swich to pip.
call([sys.executable, mach, "python", "-m", "pip", "install", version])
# install packages required for unit testing
for package in test_packages:
call(["easy_install", package])
call([sys.executable, mach, "python", "-m", "pip", "install", package])
# install extra non-mozbase packages if desired
if options.extra:
for package in extra_packages:
call(["easy_install", package])
call([sys.executable, mach, "python", "-m", "pip", "install", package])
if __name__ == "__main__":
......
......@@ -110,6 +110,13 @@ class DoesItCrash(BaseScript):
for retry in range(3):
if is_win:
proc.send_signal(signal.CTRL_BREAK_EVENT)
# Manually kill all processes we spawned,
# not sure why this is required, but without it we hang forever.
process_name = self.config["thing_to_run"].split("/")[-1]
subprocess.run(
["taskkill", "/T", "/F", "/IM", process_name], check=True
)
else:
os.killpg(proc.pid, signal.SIGKILL)
try:
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment