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