xref: /aosp_15_r20/external/angle/build/fuchsia/test/ffx_integration.py (revision 8975f5c5ed3d1c378011245431ada316dfb6f244)
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