summaryrefslogtreecommitdiffstats
path: root/mobile/android/fenix/automation/taskcluster/androidTest/copy-robo-crash-artifacts.py
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/fenix/automation/taskcluster/androidTest/copy-robo-crash-artifacts.py')
-rw-r--r--mobile/android/fenix/automation/taskcluster/androidTest/copy-robo-crash-artifacts.py273
1 files changed, 273 insertions, 0 deletions
diff --git a/mobile/android/fenix/automation/taskcluster/androidTest/copy-robo-crash-artifacts.py b/mobile/android/fenix/automation/taskcluster/androidTest/copy-robo-crash-artifacts.py
new file mode 100644
index 0000000000..9945f08ef6
--- /dev/null
+++ b/mobile/android/fenix/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())