diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:34:42 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:34:42 +0000 |
commit | da4c7e7ed675c3bf405668739c3012d140856109 (patch) | |
tree | cdd868dba063fecba609a1d819de271f0d51b23e /mobile/android/focus-android/automation | |
parent | Adding upstream version 125.0.3. (diff) | |
download | firefox-da4c7e7ed675c3bf405668739c3012d140856109.tar.xz firefox-da4c7e7ed675c3bf405668739c3012d140856109.zip |
Adding upstream version 126.0.upstream/126.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'mobile/android/focus-android/automation')
10 files changed, 878 insertions, 0 deletions
diff --git a/mobile/android/focus-android/automation/taskcluster/androidTest/copy-robo-crash-artifacts.py b/mobile/android/focus-android/automation/taskcluster/androidTest/copy-robo-crash-artifacts.py new file mode 100644 index 0000000000..9945f08ef6 --- /dev/null +++ b/mobile/android/focus-android/automation/taskcluster/androidTest/copy-robo-crash-artifacts.py @@ -0,0 +1,273 @@ +#!/usr/bin/env python3 + +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +""" +This script is designed to automate the process of fetching crash logs from Google Cloud Storage (GCS) for failed devices in a Robo test. +It is intended to be run as part of a Taskcluster job following a scheduled test task. +The script requires the presence of a `matrix_ids.json` artifact in the results directory and the availability of the `gsutil` command in the environment. + +The script performs the following operations: +- Loads the `matrix_ids.json` artifact to identify the GCS paths for the crash logs. +- Identifies failed devices based on the outcomes specified in the `matrix_ids.json` artifact. +- Fetches crash logs for the failed devices from the specified GCS paths. +- Copies the fetched crash logs to the current worker artifact results directory. + +The script is configured to log its operations and errors, providing visibility into its execution process. +It uses the `gsutil` command-line tool to interact with GCS, ensuring compatibility with the GCS environment. + +Usage: + python3 script_name.py + +Requirements: + - The `matrix_ids.json` artifact must be present in the results directory. + - The `gsutil` command must be available in the environment. + - The script should be run after a scheduled test task in a Taskcluster job. + +Output: + - Crash logs for failed devices are copied to the current worker artifact results directory. +""" + +import json +import logging +import os +import re +import subprocess +import sys +from enum import Enum + + +def setup_logging(): + """Configure logging for the script.""" + log_format = "%(message)s" + logging.basicConfig(level=logging.INFO, format=log_format) + + +class Worker(Enum): + """ + Worker paths + """ + + RESULTS_DIR = "/builds/worker/artifacts/results" + ARTIFACTS_DIR = "/builds/worker/artifacts" + + +class ArtifactType(Enum): + """ + Artifact types for fetching crash logs and matrix IDs artifact. + """ + + CRASH_LOG = "data_app_crash*.txt" + MATRIX_IDS = "matrix_ids.json" + + +def load_matrix_ids_artifact(matrix_file_path): + """Load the matrix IDs artifact from the specified file path. + + Args: + matrix_file_path (str): The file path to the matrix IDs artifact. + Returns: + dict: The contents of the matrix IDs artifact. + """ + try: + with open(matrix_file_path, "r") as f: + return json.load(f) + except FileNotFoundError: + logging.error(f"Could not find matrix file: {matrix_file_path}") + sys.exit(1) + except json.JSONDecodeError: + logging.error(f"Error decoding matrix file: {matrix_file_path}") + sys.exit(1) + + +def get_gcs_path(matrix_artifact_file): + """ + Extract the root GCS path from the matrix artifact file. + + Args: + matrix_artifact_file (dict): The matrix artifact file contents. + Returns: + str: The root GCS path extracted from the matrix artifact file. + """ + for matrix in matrix_artifact_file.values(): + gcs_path = matrix.get("gcsPath") + if gcs_path: + return gcs_path + return None + + +def check_gsutil_availability(): + """ + Check the availability of the `gsutil` command in the environment. + Exit the script if `gsutil` is not available. + """ + try: + result = subprocess.run(["gsutil", "--version"], capture_output=True, text=True) + if result.returncode != 0: + logging.error(f"Error executing gsutil: {result.stderr}") + sys.exit(1) + except Exception as e: + logging.error(f"Error executing gsutil: {e}") + sys.exit(1) + + +def fetch_artifacts(root_gcs_path, device, artifact_pattern): + """ + Fetch artifacts from the specified GCS path pattern for the given device. + + Args: + root_gcs_path (str): The root GCS path for the artifacts. + device (str): The device name for which to fetch artifacts. + artifact_pattern (str): The pattern to match the artifacts. + Returns: + list: A list of artifacts matching the specified pattern. + """ + gcs_path_pattern = f"gs://{root_gcs_path.rstrip('/')}/{device}/{artifact_pattern}" + + try: + result = subprocess.run( + ["gsutil", "ls", gcs_path_pattern], capture_output=True, text=True + ) + if result.returncode == 0: + return result.stdout.splitlines() + else: + if "AccessDeniedException" in result.stderr: + logging.error(f"Permission denied for GCS path: {gcs_path_pattern}") + elif "network error" in result.stderr.lower(): + logging.error(f"Network error accessing GCS path: {gcs_path_pattern}") + else: + logging.error(f"Failed to list files: {result.stderr}") + return [] + except Exception as e: + logging.error(f"Error executing gsutil: {e}") + return [] + + +def fetch_failed_device_names(matrix_artifact_file): + """ + Fetch the names of devices that failed based on the outcomes specified in the matrix artifact file. + + Args: + matrix_artifact_file (dict): The matrix artifact file contents. + Returns: + list: A list of device names that failed in the test. + """ + failed_devices = [] + for matrix in matrix_artifact_file.values(): + axes = matrix.get("axes", []) + for axis in axes: + if axis.get("outcome") == "failure": + device = axis.get("device") + if device: + failed_devices.append(device) + return failed_devices + + +def gsutil_cp(artifact, dest): + """ + Copy the specified artifact to the destination path using `gsutil`. + + Args: + artifact (str): The path to the artifact to copy. + dest (str): The destination path to copy the artifact to. + Returns: + None + """ + logging.info(f"Copying {artifact} to {dest}") + try: + result = subprocess.run( + ["gsutil", "cp", artifact, dest], capture_output=True, text=True + ) + if result.returncode != 0: + if "AccessDeniedException" in result.stderr: + logging.error(f"Permission denied for GCS path: {artifact}") + elif "network error" in result.stderr.lower(): + logging.error(f"Network error accessing GCS path: {artifact}") + else: + logging.error(f"Failed to list files: {result.stderr}") + except Exception as e: + logging.error(f"Error executing gsutil: {e}") + + +def parse_crash_log(log_path): + crashes_reported = 0 + if os.path.isfile(log_path): + with open(log_path) as f: + contents = f.read() + proc = "unknown" + match = re.search(r"Process: (.*)\n", contents, re.MULTILINE) + if match and len(match.groups()) == 1: + proc = match.group(1) + # Isolate the crash stack and reformat it for treeherder. + # Variation in stacks makes the regex tricky! + # Example: + # java.lang.NullPointerException + # at org.mozilla.fenix.library.bookmarks.BookmarkFragment.getBookmarkInteractor(BookmarkFragment.kt:72) + # at org.mozilla.fenix.library.bookmarks.BookmarkFragment.refreshBookmarks(BookmarkFragment.kt:297) ... + # Example: + # java.lang.IllegalStateException: pending state not allowed + # at org.mozilla.fenix.onboarding.OnboardingFragment.onCreate(OnboardingFragment.kt:83) + # at androidx.fragment.app.Fragment.performCreate(Fragment.java:3094) ... + # Example: + # java.lang.IllegalArgumentException: No handler given, and current thread has no looper! + # at android.hardware.camera2.impl.CameraDeviceImpl.checkHandler(CameraDeviceImpl.java:2380) + # at android.hardware.camera2.impl.CameraDeviceImpl.checkHandler(CameraDeviceImpl.java:2395) + match = re.search( + r"\n([\w\.]+[:\s\w\.,!?#^\'\"]+)\s*(at\s.*\n)", contents, re.MULTILINE + ) + if match and len(match.groups()) == 2: + top_frame = match.group(1).rstrip() + " " + match.group(2) + remainder = contents[match.span()[1] :] + logging.error(f"PROCESS-CRASH | {proc} | {top_frame}{remainder}") + crashes_reported = 1 + return crashes_reported + + +def process_artifacts(artifact_type): + """ + Process the artifacts based on the specified artifact type. + + Args: + artifact_type (ArtifactType): The type of artifact to process. + Returns: + Number of crashes reported in treeherder format. + """ + matrix_ids_artifact = load_matrix_ids_artifact( + Worker.RESULTS_DIR.value + "/" + ArtifactType.MATRIX_IDS.value + ) + failed_device_names = fetch_failed_device_names(matrix_ids_artifact) + root_gcs_path = get_gcs_path(matrix_ids_artifact) + + if not root_gcs_path: + logging.error("Could not find root GCS path in matrix file.") + return 0 + + if not failed_device_names: + return 0 + + crashes_reported = 0 + for device in failed_device_names: + artifacts = fetch_artifacts(root_gcs_path, device, artifact_type.value) + if not artifacts: + logging.info(f"No artifacts found for device: {device}") + continue + + for artifact in artifacts: + gsutil_cp(artifact, Worker.RESULTS_DIR.value) + crashes_reported += parse_crash_log( + os.path.join(Worker.RESULTS_DIR.value, os.path.basename(artifact)) + ) + + return crashes_reported + + +def main(): + setup_logging() + check_gsutil_availability() + return process_artifacts(ArtifactType.CRASH_LOG) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/mobile/android/focus-android/automation/taskcluster/androidTest/flank-arm-beta.yml b/mobile/android/focus-android/automation/taskcluster/androidTest/flank-arm-beta.yml new file mode 100644 index 0000000000..8a3fea6dd4 --- /dev/null +++ b/mobile/android/focus-android/automation/taskcluster/androidTest/flank-arm-beta.yml @@ -0,0 +1,36 @@ +# Google Cloud Documentation: https://cloud.google.com/sdk/gcloud/reference/firebase/test/android/run +# Flank Documentation: https://flank.github.io/flank/ +gcloud: + results-bucket: focus_android_test_artifacts + record-video: true + + timeout: 30m + async: false + num-flaky-test-attempts: 1 + + app: /app/path + test: /test/path + + auto-google-login: false + use-orchestrator: true + environment-variables: + clearPackageData: true + directories-to-pull: + - /sdcard/screenshots + performance-metrics: true + + test-targets: + - class org.mozilla.focus.activity.SearchTest#testBlankSearchDoesNothing + - class org.mozilla.focus.activity.EraseBrowsingDataTest#deleteHistoryOnRestartTest + + device: + - model: Pixel2.arm + version: 30 + locale: en_US + +flank: + project: GOOGLE_PROJECT + max-test-shards: 1 + num-test-runs: 1 + output-style: compact + full-junit-result: true diff --git a/mobile/android/focus-android/automation/taskcluster/androidTest/flank-arm-start-test-robo.yml b/mobile/android/focus-android/automation/taskcluster/androidTest/flank-arm-start-test-robo.yml new file mode 100644 index 0000000000..dda08757fc --- /dev/null +++ b/mobile/android/focus-android/automation/taskcluster/androidTest/flank-arm-start-test-robo.yml @@ -0,0 +1,27 @@ +# Google Cloud Documentation: https://cloud.google.com/sdk/gcloud/reference/firebase/test/android/run +# Flank Documentation: https://flank.github.io/flank/ +gcloud: + results-bucket: focus_android_test_artifacts + record-video: false + timeout: 5m + async: false + + app: /app/path + + auto-google-login: false + use-orchestrator: true + environment-variables: + clearPackageData: true + + device: + - model: MediumPhone.arm + version: 30 + locale: en_US + + type: robo + +flank: + project: GOOGLE_PROJECT + num-test-runs: 1 + output-style: compact + full-junit-result: true diff --git a/mobile/android/focus-android/automation/taskcluster/androidTest/flank-arm-start-test.yml b/mobile/android/focus-android/automation/taskcluster/androidTest/flank-arm-start-test.yml new file mode 100644 index 0000000000..8a3fea6dd4 --- /dev/null +++ b/mobile/android/focus-android/automation/taskcluster/androidTest/flank-arm-start-test.yml @@ -0,0 +1,36 @@ +# Google Cloud Documentation: https://cloud.google.com/sdk/gcloud/reference/firebase/test/android/run +# Flank Documentation: https://flank.github.io/flank/ +gcloud: + results-bucket: focus_android_test_artifacts + record-video: true + + timeout: 30m + async: false + num-flaky-test-attempts: 1 + + app: /app/path + test: /test/path + + auto-google-login: false + use-orchestrator: true + environment-variables: + clearPackageData: true + directories-to-pull: + - /sdcard/screenshots + performance-metrics: true + + test-targets: + - class org.mozilla.focus.activity.SearchTest#testBlankSearchDoesNothing + - class org.mozilla.focus.activity.EraseBrowsingDataTest#deleteHistoryOnRestartTest + + device: + - model: Pixel2.arm + version: 30 + locale: en_US + +flank: + project: GOOGLE_PROJECT + max-test-shards: 1 + num-test-runs: 1 + output-style: compact + full-junit-result: true diff --git a/mobile/android/focus-android/automation/taskcluster/androidTest/flank-arm64-v8a.yml b/mobile/android/focus-android/automation/taskcluster/androidTest/flank-arm64-v8a.yml new file mode 100644 index 0000000000..1439d6c761 --- /dev/null +++ b/mobile/android/focus-android/automation/taskcluster/androidTest/flank-arm64-v8a.yml @@ -0,0 +1,36 @@ +# Google Cloud Documentation: https://cloud.google.com/sdk/gcloud/reference/firebase/test/android/run +# Flank Documentation: https://flank.github.io/flank/ +gcloud: + results-bucket: focus_android_test_artifacts + record-video: true + + timeout: 30m + async: false + num-flaky-test-attempts: 1 + + app: /app/path + test: /test/path + + auto-google-login: false + use-orchestrator: true + environment-variables: + clearPackageData: true + directories-to-pull: + - /sdcard/screenshots + performance-metrics: true + + test-targets: + - package org.mozilla.focus.activity + - package org.mozilla.focus.privacy + + device: + - model: Pixel2.arm + version: 30 + locale: en_US + +flank: + project: GOOGLE_PROJECT + max-test-shards: 50 + num-test-runs: 1 + output-style: compact + full-junit-result: true diff --git a/mobile/android/focus-android/automation/taskcluster/androidTest/flank-x86.yml b/mobile/android/focus-android/automation/taskcluster/androidTest/flank-x86.yml new file mode 100644 index 0000000000..83a745928a --- /dev/null +++ b/mobile/android/focus-android/automation/taskcluster/androidTest/flank-x86.yml @@ -0,0 +1,36 @@ +# Google Cloud Documentation: https://cloud.google.com/sdk/gcloud/reference/firebase/test/android/run +# Flank Documentation: https://flank.github.io/flank/ +gcloud: + results-bucket: focus_android_test_artifacts + record-video: true + + timeout: 30m + async: false + num-flaky-test-attempts: 1 + + app: /app/path + test: /test/path + + auto-google-login: false + use-orchestrator: true + environment-variables: + clearPackageData: true + directories-to-pull: + - /sdcard/screenshots + performance-metrics: true + + test-targets: + - package org.mozilla.focus.activity + - package org.mozilla.focus.privacy + + device: + - model: Pixel2 + version: 30 + locale: en_US + +flank: + project: GOOGLE_PROJECT + max-test-shards: -1 + num-test-runs: 1 + output-style: compact + full-junit-result: true diff --git a/mobile/android/focus-android/automation/taskcluster/androidTest/parse-ui-test-fromfile.py b/mobile/android/focus-android/automation/taskcluster/androidTest/parse-ui-test-fromfile.py new file mode 100644 index 0000000000..7b853d5e9c --- /dev/null +++ b/mobile/android/focus-android/automation/taskcluster/androidTest/parse-ui-test-fromfile.py @@ -0,0 +1,97 @@ +#!/usr/bin/python3 + +import argparse +import sys +import xml +from pathlib import Path + +from beautifultable import BeautifulTable +from junitparser import Attr, Failure, JUnitXml, TestSuite + + +def parse_args(cmdln_args): + parser = argparse.ArgumentParser( + description="Parse and print UI test JUnit results" + ) + parser.add_argument( + "--results", + type=Path, + help="Directory containing task artifact results", + required=True, + ) + return parser.parse_args(args=cmdln_args) + + +class test_suite(TestSuite): + flakes = Attr() + + +def parse_print_failure_results(results): + table = BeautifulTable(maxwidth=256) + table.columns.header = ["UI Test", "Outcome", "Details"] + table.columns.alignment = BeautifulTable.ALIGN_LEFT + table.set_style(BeautifulTable.STYLE_GRID) + + failure_count = 0 + for suite in results: + cur_suite = test_suite.fromelem(suite) + if cur_suite.flakes != "0": + for case in suite: + for entry in case.result: + if case.result: + table.rows.append( + [ + "%s#%s" % (case.classname, case.name), + "Flaky", + entry.text.replace("\t", " "), + ] + ) + break + else: + for case in suite: + for entry in case.result: + if isinstance(entry, Failure): + test_id = "%s#%s" % (case.classname, case.name) + details = entry.text.replace("\t", " ") + table.rows.append( + [ + test_id, + "Failure", + details, + ] + ) + print(f"TEST-UNEXPECTED-FAIL | {test_id} | {details}") + failure_count += 1 + break + print(table) + return failure_count + + +def load_results_file(filename): + ret = None + try: + f = open(filename, "r") + try: + ret = JUnitXml.fromfile(f) + except xml.etree.ElementTree.ParseError as e: + print(f"Error parsing {filename} file: {e}") + finally: + f.close() + except IOError as e: + print(e) + + return ret + + +def main(): + args = parse_args(sys.argv[1:]) + + failure_count = 0 + junitxml = load_results_file(args.results.joinpath("FullJUnitReport.xml")) + if junitxml: + failure_count = parse_print_failure_results(junitxml) + return failure_count + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/mobile/android/focus-android/automation/taskcluster/androidTest/parse-ui-test.py b/mobile/android/focus-android/automation/taskcluster/androidTest/parse-ui-test.py new file mode 100644 index 0000000000..de41a6c7e7 --- /dev/null +++ b/mobile/android/focus-android/automation/taskcluster/androidTest/parse-ui-test.py @@ -0,0 +1,89 @@ +#!/usr/bin/python3 + +from __future__ import print_function + +import argparse +import json +import sys +from pathlib import Path + +import yaml + + +def parse_args(cmdln_args): + parser = argparse.ArgumentParser(description="Parse UI test logs an results") + parser.add_argument( + "--output-md", + type=argparse.FileType("w", encoding="utf-8"), + help="Output markdown file.", + required=True, + ) + parser.add_argument( + "--log", + type=argparse.FileType("r", encoding="utf-8"), + help="Log output of flank.", + required=True, + ) + parser.add_argument( + "--results", type=Path, help="Directory containing flank results", required=True + ) + parser.add_argument( + "--exit-code", type=int, help="Exit code of flank.", required=True + ) + parser.add_argument("--device-type", help="Type of device ", required=True) + parser.add_argument( + "--report-treeherder-failures", + help="Report failures in treeherder format.", + required=False, + action="store_true", + ) + return parser.parse_args(args=cmdln_args) + + +def extract_android_args(log): + return yaml.safe_load(log.split("AndroidArgs\n")[1].split("RunTests\n")[0]) + + +def main(): + args = parse_args(sys.argv[1:]) + + log = args.log.read() + matrix_ids = json.loads(args.results.joinpath("matrix_ids.json").read_text()) + + android_args = extract_android_args(log) + + print = args.output_md.write + + print("# Devices\n") + print(yaml.safe_dump(android_args["gcloud"]["device"])) + + print("# Results\n") + print("| Matrix | Result | Firebase Test Lab | Details\n") + print("| --- | --- | --- | --- |\n") + for matrix, matrix_result in matrix_ids.items(): + for axis in matrix_result["axes"]: + print( + f"| {matrix_result['matrixId']} | {matrix_result['outcome']}" + f"| [Firebase Test Lab]({matrix_result['webLink']}) | {axis['details']}\n" + ) + if ( + args.report_treeherder_failures + and matrix_result["outcome"] != "success" + and matrix_result["outcome"] != "flaky" + ): + # write failures to test log in format known to treeherder logviewer + sys.stdout.write( + f"TEST-UNEXPECTED-FAIL | {matrix_result['outcome']} | {matrix_result['webLink']} | {axis['details']}\n" + ) + print("---\n") + print("# References & Documentation\n") + print( + "* [Automated UI Testing Documentation](https://github.com/mozilla-mobile/shared-docs/blob/main/android/ui-testing.md)\n" + ) + print( + "* Mobile Test Engineering on [Confluence](https://mozilla-hub.atlassian.net/wiki/spaces/MTE/overview) | [Slack](https://mozilla.slack.com/archives/C02KDDS9QM9) | [Alerts](https://mozilla.slack.com/archives/C0134KJ4JHL)\n" + ) + + +if __name__ == "__main__": + main() diff --git a/mobile/android/focus-android/automation/taskcluster/androidTest/robo-test.sh b/mobile/android/focus-android/automation/taskcluster/androidTest/robo-test.sh new file mode 100755 index 0000000000..c2b6ee76f9 --- /dev/null +++ b/mobile/android/focus-android/automation/taskcluster/androidTest/robo-test.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# This script does the following: +# 1. Retrieves gcloud service account token +# 2. Activates gcloud service account +# 3. Connects to Google's Firebase Test Lab (using Flank) +# 4. Executes Robo test +# 5. Puts any artifacts into the test_artifacts folder + +# If a command fails then do not proceed and fail this script too. +set -e + +get_abs_filename() { + relative_filename="$1" + echo "$(cd "$(dirname "$relative_filename")" && pwd)/$(basename "$relative_filename")" +} + +# Basic parameter check +if [[ $# -lt 1 ]]; then + echo "Error: please provide a Flank configuration" + display_help + exit 1 +fi + +device_type="$1" # flank-arm-robo-test.yml | flank-x86-robo-test.yml +APK_APP="$2" +JAVA_BIN="/usr/bin/java" +PATH_TEST="./automation/taskcluster/androidTest" +FLANK_BIN="/builds/worker/test-tools/flank.jar" +ARTIFACT_DIR="/builds/worker/artifacts" +RESULTS_DIR="${ARTIFACT_DIR}/results" + +echo +echo "ACTIVATE SERVICE ACCT" +echo +gcloud config set project "$GOOGLE_PROJECT" +gcloud auth activate-service-account --key-file "$GOOGLE_APPLICATION_CREDENTIALS" +echo +echo +echo + +set +e + +flank_template="${PATH_TEST}/flank-${device_type}.yml" +if [ -f "$flank_template" ]; then + echo "Using Flank template: $flank_template" +else + echo "Error: Flank template not found: $flank_template" + exit 1 +fi + +APK_APP="$(get_abs_filename $APK_APP)" + +function failure_check() { + echo + echo + if [[ $exitcode -ne 0 ]]; then + echo "FAILURE: Robo test run failed, please check above URL" + else + echo "Robo test was successful!" + fi + + echo + echo "RESULTS" + echo + + mkdir -p /builds/worker/artifacts/github + chmod +x $PATH_TEST/parse-ui-test.py + $PATH_TEST/parse-ui-test.py \ + --exit-code "${exitcode}" \ + --log flank.log \ + --results "${RESULTS_DIR}" \ + --output-md "${ARTIFACT_DIR}/github/customCheckRunText.md" \ + --device-type "${device_type}" +} + +echo +echo "FLANK VERSION" +echo +$JAVA_BIN -jar $FLANK_BIN --version +echo +echo + +echo +echo "EXECUTE ROBO TEST" +echo +set -o pipefail && $JAVA_BIN -jar $FLANK_BIN android run \ + --config=$flank_template \ + --app=$APK_APP \ + --local-result-dir="${RESULTS_DIR}" \ + --project=$GOOGLE_PROJECT \ + --client-details=commit=${MOBILE_HEAD_REV:-None},pullRequest=${PULL_REQUEST_NUMBER:-None} \ + | tee flank.log + +exitcode=$? +failure_check + +exit $exitcode diff --git a/mobile/android/focus-android/automation/taskcluster/androidTest/ui-test.sh b/mobile/android/focus-android/automation/taskcluster/androidTest/ui-test.sh new file mode 100755 index 0000000000..362d528b3d --- /dev/null +++ b/mobile/android/focus-android/automation/taskcluster/androidTest/ui-test.sh @@ -0,0 +1,147 @@ +#!/usr/bin/env bash +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# This script does the following: +# 1. Retrieves gcloud service account token +# 2. Activates gcloud service account +# 3. Connects to google Firebase (using TestArmada's Flank tool) +# 4. Executes UI tests +# 5. Puts test artifacts into the test_artifacts folder + +# NOTE: +# Flank supports sharding across multiple devices at a time, but gcloud API +# only supports 1 defined APK per test run. + + +# If a command fails then do not proceed and fail this script too. +set -e + +######################### +# The command line help # +######################### +display_help() { + echo "Usage: $0 Build_Variant [Number_Shards...]" + echo + echo "Examples:" + echo "To run UI tests on ARM device shard (1 test / shard)" + echo "$ ui-test.sh arm64-v8a -1" + echo + echo "To run UI tests on X86 device (on 3 shards)" + echo "$ ui-test.sh x86 3" + echo +} + +get_abs_filename() { + relative_filename="$1" + echo "$(cd "$(dirname "$relative_filename")" && pwd)/$(basename "$relative_filename")" +} + + +# Basic parameter check +if [[ $# -lt 1 ]]; then + echo "Error: please provide at least one build variant (arm|x86)" + display_help + exit 1 +fi + +device_type="$1" # arm64-v8a | armeabi-v7a | x86_64 | x86 +APK_APP="$2" +APK_TEST="$3" +if [[ ! -z "$4" ]]; then + num_shards=$4 +fi + +JAVA_BIN="/usr/bin/java" +PATH_TEST="./automation/taskcluster/androidTest" +FLANK_BIN="/builds/worker/test-tools/flank.jar" +ARTIFACT_DIR="/builds/worker/artifacts" +RESULTS_DIR="${ARTIFACT_DIR}/results" + +echo +echo "ACTIVATE SERVICE ACCT" +echo +# this is where the Google Testcloud project ID is set +gcloud config set project "$GOOGLE_PROJECT" +echo + +gcloud auth activate-service-account --key-file "$GOOGLE_APPLICATION_CREDENTIALS" +echo +echo + +# Disable exiting on error. If the tests fail we want to continue +# and try to download the artifacts. We will exit with the actual error code later. +set +e + +if [[ "${device_type}" =~ ^(arm64-v8a|armeabi-v7a|x86_64|x86)$ ]]; then + flank_template="${PATH_TEST}/flank-${device_type}.yml" +elif [[ "${device_type}" == "arm-start-test" ]]; then + flank_template="${PATH_TEST}/flank-arm-start-test.yml" +elif [[ "${device_type}" == "arm-beta-tests" ]]; then + flank_template="${PATH_TEST}/flank-arm-beta.yml" +else + echo "FAILURE: flank config file not found!" + exitcode=1 +fi + +APK_APP="$(get_abs_filename $APK_APP)" +APK_TEST="$(get_abs_filename $APK_TEST)" +echo "device_type: ${device_type}" +echo "APK_APP: ${APK_APP}" +echo "APK_TEST: ${APK_TEST}" + +# function to exit script with exit code from test run. +# (Only 0 if all test executions passed) +function failure_check() { + echo + echo + if [[ $exitcode -ne 0 ]]; then + echo "FAILURE: UI test run failed, please check above URL" + else + echo "All UI test(s) have passed!" + fi + echo + echo "RESULTS" + echo + + mkdir -p /builds/worker/artifacts/github + chmod +x $PATH_TEST/parse-ui-test.py + $PATH_TEST/parse-ui-test.py \ + --exit-code "${exitcode}" \ + --log flank.log \ + --results "${RESULTS_DIR}" \ + --output-md "${ARTIFACT_DIR}/github/customCheckRunText.md" \ + --device-type "${device_type}" + + chmod +x $PATH_TEST/parse-ui-test-fromfile.py + $PATH_TEST/parse-ui-test-fromfile.py \ + --results "${RESULTS_DIR}" + +} + +echo +echo "FLANK VERSION" +echo +$JAVA_BIN -jar $FLANK_BIN --version +echo +echo + +echo +echo "EXECUTE TEST(S)" +echo +# Note that if --local-results-dir is "results", timestamped sub-directory will +# contain the results. For any other value, the directory itself will have the results. +set -o pipefail && $JAVA_BIN -jar $FLANK_BIN android run \ + --config=$flank_template \ + --max-test-shards=$num_shards \ + --app=$APK_APP --test=$APK_TEST \ + --local-result-dir="${RESULTS_DIR}" \ + --project=$GOOGLE_PROJECT \ + --client-details=commit=${MOBILE_HEAD_REV:-None},pullRequest=${PULL_REQUEST_NUMBER:-None} \ + | tee flank.log + +exitcode=$? +failure_check + +exit $exitcode |