xref: /aosp_15_r20/external/cronet/build/fuchsia/test/ffx_integration.py (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
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