1*8975f5c5SAndroid Build Coastguard Worker# Copyright 2022 The Chromium Authors 2*8975f5c5SAndroid Build Coastguard Worker# Use of this source code is governed by a BSD-style license that can be 3*8975f5c5SAndroid Build Coastguard Worker# found in the LICENSE file. 4*8975f5c5SAndroid Build Coastguard Worker"""Provide helpers for running Fuchsia's `ffx`.""" 5*8975f5c5SAndroid Build Coastguard Worker 6*8975f5c5SAndroid Build Coastguard Workerimport logging 7*8975f5c5SAndroid Build Coastguard Workerimport os 8*8975f5c5SAndroid Build Coastguard Workerimport json 9*8975f5c5SAndroid Build Coastguard Workerimport subprocess 10*8975f5c5SAndroid Build Coastguard Workerimport sys 11*8975f5c5SAndroid Build Coastguard Workerimport tempfile 12*8975f5c5SAndroid Build Coastguard Worker 13*8975f5c5SAndroid Build Coastguard Workerfrom contextlib import AbstractContextManager 14*8975f5c5SAndroid Build Coastguard Workerfrom typing import IO, Iterable, List, Optional 15*8975f5c5SAndroid Build Coastguard Worker 16*8975f5c5SAndroid Build Coastguard Workerfrom common import run_continuous_ffx_command, run_ffx_command, SDK_ROOT 17*8975f5c5SAndroid Build Coastguard Worker 18*8975f5c5SAndroid Build Coastguard WorkerRUN_SUMMARY_SCHEMA = \ 19*8975f5c5SAndroid Build Coastguard Worker 'https://fuchsia.dev/schema/ffx_test/run_summary-8d1dd964.json' 20*8975f5c5SAndroid Build Coastguard Worker 21*8975f5c5SAndroid Build Coastguard Worker 22*8975f5c5SAndroid Build Coastguard Workerdef get_config(name: str) -> Optional[str]: 23*8975f5c5SAndroid Build Coastguard Worker """Run a ffx config get command to retrieve the config value.""" 24*8975f5c5SAndroid Build Coastguard Worker 25*8975f5c5SAndroid Build Coastguard Worker try: 26*8975f5c5SAndroid Build Coastguard Worker return run_ffx_command(cmd=['config', 'get', name], 27*8975f5c5SAndroid Build Coastguard Worker capture_output=True).stdout.strip() 28*8975f5c5SAndroid Build Coastguard Worker except subprocess.CalledProcessError as cpe: 29*8975f5c5SAndroid Build Coastguard Worker # A return code of 2 indicates no previous value set. 30*8975f5c5SAndroid Build Coastguard Worker if cpe.returncode == 2: 31*8975f5c5SAndroid Build Coastguard Worker return None 32*8975f5c5SAndroid Build Coastguard Worker raise 33*8975f5c5SAndroid Build Coastguard Worker 34*8975f5c5SAndroid Build Coastguard Worker 35*8975f5c5SAndroid Build Coastguard Workerclass ScopedFfxConfig(AbstractContextManager): 36*8975f5c5SAndroid Build Coastguard Worker """Temporarily overrides `ffx` configuration. Restores the previous value 37*8975f5c5SAndroid Build Coastguard Worker upon exit.""" 38*8975f5c5SAndroid Build Coastguard Worker 39*8975f5c5SAndroid Build Coastguard Worker def __init__(self, name: str, value: str) -> None: 40*8975f5c5SAndroid Build Coastguard Worker """ 41*8975f5c5SAndroid Build Coastguard Worker Args: 42*8975f5c5SAndroid Build Coastguard Worker name: The name of the property to set. 43*8975f5c5SAndroid Build Coastguard Worker value: The value to associate with `name`. 44*8975f5c5SAndroid Build Coastguard Worker """ 45*8975f5c5SAndroid Build Coastguard Worker self._old_value = None 46*8975f5c5SAndroid Build Coastguard Worker self._new_value = value 47*8975f5c5SAndroid Build Coastguard Worker self._name = name 48*8975f5c5SAndroid Build Coastguard Worker 49*8975f5c5SAndroid Build Coastguard Worker def __enter__(self): 50*8975f5c5SAndroid Build Coastguard Worker """Override the configuration.""" 51*8975f5c5SAndroid Build Coastguard Worker 52*8975f5c5SAndroid Build Coastguard Worker # Cache the old value. 53*8975f5c5SAndroid Build Coastguard Worker self._old_value = get_config(self._name) 54*8975f5c5SAndroid Build Coastguard Worker if self._new_value != self._old_value: 55*8975f5c5SAndroid Build Coastguard Worker run_ffx_command(cmd=['config', 'set', self._name, self._new_value]) 56*8975f5c5SAndroid Build Coastguard Worker return self 57*8975f5c5SAndroid Build Coastguard Worker 58*8975f5c5SAndroid Build Coastguard Worker def __exit__(self, exc_type, exc_val, exc_tb) -> bool: 59*8975f5c5SAndroid Build Coastguard Worker if self._new_value == self._old_value: 60*8975f5c5SAndroid Build Coastguard Worker return False 61*8975f5c5SAndroid Build Coastguard Worker 62*8975f5c5SAndroid Build Coastguard Worker # Allow removal of config to fail. 63*8975f5c5SAndroid Build Coastguard Worker remove_cmd = run_ffx_command(cmd=['config', 'remove', self._name], 64*8975f5c5SAndroid Build Coastguard Worker check=False) 65*8975f5c5SAndroid Build Coastguard Worker if remove_cmd.returncode != 0: 66*8975f5c5SAndroid Build Coastguard Worker logging.warning('Error when removing ffx config %s', self._name) 67*8975f5c5SAndroid Build Coastguard Worker 68*8975f5c5SAndroid Build Coastguard Worker # Explicitly set the value back only if removing the new value doesn't 69*8975f5c5SAndroid Build Coastguard Worker # already restore the old value. 70*8975f5c5SAndroid Build Coastguard Worker if self._old_value is not None and \ 71*8975f5c5SAndroid Build Coastguard Worker self._old_value != get_config(self._name): 72*8975f5c5SAndroid Build Coastguard Worker run_ffx_command(cmd=['config', 'set', self._name, self._old_value]) 73*8975f5c5SAndroid Build Coastguard Worker 74*8975f5c5SAndroid Build Coastguard Worker # Do not suppress exceptions. 75*8975f5c5SAndroid Build Coastguard Worker return False 76*8975f5c5SAndroid Build Coastguard Worker 77*8975f5c5SAndroid Build Coastguard Worker 78*8975f5c5SAndroid Build Coastguard Workerclass FfxTestRunner(AbstractContextManager): 79*8975f5c5SAndroid Build Coastguard Worker """A context manager that manages a session for running a test via `ffx`. 80*8975f5c5SAndroid Build Coastguard Worker 81*8975f5c5SAndroid Build Coastguard Worker Upon entry, an instance of this class configures `ffx` to retrieve files 82*8975f5c5SAndroid Build Coastguard Worker generated by a test and prepares a directory to hold these files either in a 83*8975f5c5SAndroid Build Coastguard Worker specified directory or in tmp. On exit, any previous configuration of 84*8975f5c5SAndroid Build Coastguard Worker `ffx` is restored and the temporary directory, if used, is deleted. 85*8975f5c5SAndroid Build Coastguard Worker 86*8975f5c5SAndroid Build Coastguard Worker The prepared directory is used when invoking `ffx test run`. 87*8975f5c5SAndroid Build Coastguard Worker """ 88*8975f5c5SAndroid Build Coastguard Worker 89*8975f5c5SAndroid Build Coastguard Worker def __init__(self, results_dir: Optional[str] = None) -> None: 90*8975f5c5SAndroid Build Coastguard Worker """ 91*8975f5c5SAndroid Build Coastguard Worker Args: 92*8975f5c5SAndroid Build Coastguard Worker results_dir: Directory on the host where results should be stored. 93*8975f5c5SAndroid Build Coastguard Worker """ 94*8975f5c5SAndroid Build Coastguard Worker self._results_dir = results_dir 95*8975f5c5SAndroid Build Coastguard Worker self._custom_artifact_directory = None 96*8975f5c5SAndroid Build Coastguard Worker self._temp_results_dir = None 97*8975f5c5SAndroid Build Coastguard Worker self._debug_data_directory = None 98*8975f5c5SAndroid Build Coastguard Worker 99*8975f5c5SAndroid Build Coastguard Worker def __enter__(self): 100*8975f5c5SAndroid Build Coastguard Worker if self._results_dir: 101*8975f5c5SAndroid Build Coastguard Worker os.makedirs(self._results_dir, exist_ok=True) 102*8975f5c5SAndroid Build Coastguard Worker else: 103*8975f5c5SAndroid Build Coastguard Worker self._temp_results_dir = tempfile.TemporaryDirectory() 104*8975f5c5SAndroid Build Coastguard Worker self._results_dir = self._temp_results_dir.__enter__() 105*8975f5c5SAndroid Build Coastguard Worker return self 106*8975f5c5SAndroid Build Coastguard Worker 107*8975f5c5SAndroid Build Coastguard Worker def __exit__(self, exc_type, exc_val, exc_tb) -> bool: 108*8975f5c5SAndroid Build Coastguard Worker if self._temp_results_dir: 109*8975f5c5SAndroid Build Coastguard Worker self._temp_results_dir.__exit__(exc_type, exc_val, exc_tb) 110*8975f5c5SAndroid Build Coastguard Worker self._temp_results_dir = None 111*8975f5c5SAndroid Build Coastguard Worker 112*8975f5c5SAndroid Build Coastguard Worker # Do not suppress exceptions. 113*8975f5c5SAndroid Build Coastguard Worker return False 114*8975f5c5SAndroid Build Coastguard Worker 115*8975f5c5SAndroid Build Coastguard Worker def run_test(self, 116*8975f5c5SAndroid Build Coastguard Worker component_uri: str, 117*8975f5c5SAndroid Build Coastguard Worker test_args: Optional[Iterable[str]] = None, 118*8975f5c5SAndroid Build Coastguard Worker node_name: Optional[str] = None, 119*8975f5c5SAndroid Build Coastguard Worker test_realm: Optional[str] = None) -> subprocess.Popen: 120*8975f5c5SAndroid Build Coastguard Worker """Starts a subprocess to run a test on a target. 121*8975f5c5SAndroid Build Coastguard Worker Args: 122*8975f5c5SAndroid Build Coastguard Worker component_uri: The test component URI. 123*8975f5c5SAndroid Build Coastguard Worker test_args: Arguments to the test package, if any. 124*8975f5c5SAndroid Build Coastguard Worker node_name: The target on which to run the test. 125*8975f5c5SAndroid Build Coastguard Worker Returns: 126*8975f5c5SAndroid Build Coastguard Worker A subprocess.Popen object. 127*8975f5c5SAndroid Build Coastguard Worker """ 128*8975f5c5SAndroid Build Coastguard Worker command = [ 129*8975f5c5SAndroid Build Coastguard Worker 'test', 'run', '--output-directory', self._results_dir, 130*8975f5c5SAndroid Build Coastguard Worker ] 131*8975f5c5SAndroid Build Coastguard Worker if test_realm: 132*8975f5c5SAndroid Build Coastguard Worker command.append("--realm") 133*8975f5c5SAndroid Build Coastguard Worker command.append(test_realm) 134*8975f5c5SAndroid Build Coastguard Worker command.append(component_uri) 135*8975f5c5SAndroid Build Coastguard Worker if test_args: 136*8975f5c5SAndroid Build Coastguard Worker command.append('--') 137*8975f5c5SAndroid Build Coastguard Worker command.extend(test_args) 138*8975f5c5SAndroid Build Coastguard Worker return run_continuous_ffx_command(command, 139*8975f5c5SAndroid Build Coastguard Worker node_name, 140*8975f5c5SAndroid Build Coastguard Worker stdout=subprocess.PIPE, 141*8975f5c5SAndroid Build Coastguard Worker stderr=subprocess.STDOUT) 142*8975f5c5SAndroid Build Coastguard Worker 143*8975f5c5SAndroid Build Coastguard Worker def _parse_test_outputs(self): 144*8975f5c5SAndroid Build Coastguard Worker """Parses the output files generated by the test runner. 145*8975f5c5SAndroid Build Coastguard Worker 146*8975f5c5SAndroid Build Coastguard Worker The instance's `_custom_artifact_directory` member is set to the 147*8975f5c5SAndroid Build Coastguard Worker directory holding output files emitted by the test. 148*8975f5c5SAndroid Build Coastguard Worker 149*8975f5c5SAndroid Build Coastguard Worker This function is idempotent, and performs no work if it has already been 150*8975f5c5SAndroid Build Coastguard Worker called. 151*8975f5c5SAndroid Build Coastguard Worker """ 152*8975f5c5SAndroid Build Coastguard Worker if self._custom_artifact_directory: 153*8975f5c5SAndroid Build Coastguard Worker return 154*8975f5c5SAndroid Build Coastguard Worker 155*8975f5c5SAndroid Build Coastguard Worker run_summary_path = os.path.join(self._results_dir, 'run_summary.json') 156*8975f5c5SAndroid Build Coastguard Worker try: 157*8975f5c5SAndroid Build Coastguard Worker with open(run_summary_path) as run_summary_file: 158*8975f5c5SAndroid Build Coastguard Worker run_summary = json.load(run_summary_file) 159*8975f5c5SAndroid Build Coastguard Worker except IOError: 160*8975f5c5SAndroid Build Coastguard Worker logging.exception('Error reading run summary file.') 161*8975f5c5SAndroid Build Coastguard Worker return 162*8975f5c5SAndroid Build Coastguard Worker except ValueError: 163*8975f5c5SAndroid Build Coastguard Worker logging.exception('Error parsing run summary file %s', 164*8975f5c5SAndroid Build Coastguard Worker run_summary_path) 165*8975f5c5SAndroid Build Coastguard Worker return 166*8975f5c5SAndroid Build Coastguard Worker 167*8975f5c5SAndroid Build Coastguard Worker assert run_summary['schema_id'] == RUN_SUMMARY_SCHEMA, \ 168*8975f5c5SAndroid Build Coastguard Worker 'Unsupported version found in %s' % run_summary_path 169*8975f5c5SAndroid Build Coastguard Worker 170*8975f5c5SAndroid Build Coastguard Worker run_artifact_dir = run_summary.get('data', {})['artifact_dir'] 171*8975f5c5SAndroid Build Coastguard Worker for artifact_path, artifact in run_summary.get( 172*8975f5c5SAndroid Build Coastguard Worker 'data', {})['artifacts'].items(): 173*8975f5c5SAndroid Build Coastguard Worker if artifact['artifact_type'] == 'DEBUG': 174*8975f5c5SAndroid Build Coastguard Worker self._debug_data_directory = os.path.join( 175*8975f5c5SAndroid Build Coastguard Worker self._results_dir, run_artifact_dir, artifact_path) 176*8975f5c5SAndroid Build Coastguard Worker break 177*8975f5c5SAndroid Build Coastguard Worker 178*8975f5c5SAndroid Build Coastguard Worker if run_summary['data']['outcome'] == "NOT_STARTED": 179*8975f5c5SAndroid Build Coastguard Worker logging.critical('Test execution was interrupted. Either the ' 180*8975f5c5SAndroid Build Coastguard Worker 'emulator crashed while the tests were still ' 181*8975f5c5SAndroid Build Coastguard Worker 'running or connection to the device was lost.') 182*8975f5c5SAndroid Build Coastguard Worker sys.exit(1) 183*8975f5c5SAndroid Build Coastguard Worker 184*8975f5c5SAndroid Build Coastguard Worker # There should be precisely one suite for the test that ran. 185*8975f5c5SAndroid Build Coastguard Worker suites_list = run_summary.get('data', {}).get('suites') 186*8975f5c5SAndroid Build Coastguard Worker if not suites_list: 187*8975f5c5SAndroid Build Coastguard Worker logging.error('Missing or empty list of suites in %s', 188*8975f5c5SAndroid Build Coastguard Worker run_summary_path) 189*8975f5c5SAndroid Build Coastguard Worker return 190*8975f5c5SAndroid Build Coastguard Worker suite_summary = suites_list[0] 191*8975f5c5SAndroid Build Coastguard Worker 192*8975f5c5SAndroid Build Coastguard Worker # Get the top-level directory holding all artifacts for this suite. 193*8975f5c5SAndroid Build Coastguard Worker artifact_dir = suite_summary.get('artifact_dir') 194*8975f5c5SAndroid Build Coastguard Worker if not artifact_dir: 195*8975f5c5SAndroid Build Coastguard Worker logging.error('Failed to find suite\'s artifact_dir in %s', 196*8975f5c5SAndroid Build Coastguard Worker run_summary_path) 197*8975f5c5SAndroid Build Coastguard Worker return 198*8975f5c5SAndroid Build Coastguard Worker 199*8975f5c5SAndroid Build Coastguard Worker # Get the path corresponding to artifacts 200*8975f5c5SAndroid Build Coastguard Worker for artifact_path, artifact in suite_summary['artifacts'].items(): 201*8975f5c5SAndroid Build Coastguard Worker if artifact['artifact_type'] == 'CUSTOM': 202*8975f5c5SAndroid Build Coastguard Worker self._custom_artifact_directory = os.path.join( 203*8975f5c5SAndroid Build Coastguard Worker self._results_dir, artifact_dir, artifact_path) 204*8975f5c5SAndroid Build Coastguard Worker break 205*8975f5c5SAndroid Build Coastguard Worker 206*8975f5c5SAndroid Build Coastguard Worker def get_custom_artifact_directory(self) -> str: 207*8975f5c5SAndroid Build Coastguard Worker """Returns the full path to the directory holding custom artifacts 208*8975f5c5SAndroid Build Coastguard Worker emitted by the test or None if the directory could not be discovered. 209*8975f5c5SAndroid Build Coastguard Worker """ 210*8975f5c5SAndroid Build Coastguard Worker self._parse_test_outputs() 211*8975f5c5SAndroid Build Coastguard Worker return self._custom_artifact_directory 212*8975f5c5SAndroid Build Coastguard Worker 213*8975f5c5SAndroid Build Coastguard Worker def get_debug_data_directory(self): 214*8975f5c5SAndroid Build Coastguard Worker """Returns the full path to the directory holding debug data 215*8975f5c5SAndroid Build Coastguard Worker emitted by the test, or None if the path cannot be determined. 216*8975f5c5SAndroid Build Coastguard Worker """ 217*8975f5c5SAndroid Build Coastguard Worker self._parse_test_outputs() 218*8975f5c5SAndroid Build Coastguard Worker return self._debug_data_directory 219*8975f5c5SAndroid Build Coastguard Worker 220*8975f5c5SAndroid Build Coastguard Worker 221*8975f5c5SAndroid Build Coastguard Workerdef run_symbolizer(symbol_paths: List[str], 222*8975f5c5SAndroid Build Coastguard Worker input_fd: IO, 223*8975f5c5SAndroid Build Coastguard Worker output_fd: IO, 224*8975f5c5SAndroid Build Coastguard Worker raw_bytes: bool = False) -> subprocess.Popen: 225*8975f5c5SAndroid Build Coastguard Worker """Runs symbolizer that symbolizes |input| and outputs to |output|.""" 226*8975f5c5SAndroid Build Coastguard Worker 227*8975f5c5SAndroid Build Coastguard Worker symbolize_cmd = ([ 228*8975f5c5SAndroid Build Coastguard Worker 'debug', 'symbolize', '--', '--omit-module-lines', '--build-id-dir', 229*8975f5c5SAndroid Build Coastguard Worker os.path.join(SDK_ROOT, '.build-id') 230*8975f5c5SAndroid Build Coastguard Worker ]) 231*8975f5c5SAndroid Build Coastguard Worker for path in symbol_paths: 232*8975f5c5SAndroid Build Coastguard Worker symbolize_cmd.extend(['--ids-txt', path]) 233*8975f5c5SAndroid Build Coastguard Worker if raw_bytes: 234*8975f5c5SAndroid Build Coastguard Worker encoding = None 235*8975f5c5SAndroid Build Coastguard Worker else: 236*8975f5c5SAndroid Build Coastguard Worker encoding = 'utf-8' 237*8975f5c5SAndroid Build Coastguard Worker return run_continuous_ffx_command(symbolize_cmd, 238*8975f5c5SAndroid Build Coastguard Worker stdin=input_fd, 239*8975f5c5SAndroid Build Coastguard Worker stdout=output_fd, 240*8975f5c5SAndroid Build Coastguard Worker stderr=subprocess.STDOUT, 241*8975f5c5SAndroid Build Coastguard Worker encoding=encoding, 242*8975f5c5SAndroid Build Coastguard Worker close_fds=True) 243