xref: /aosp_15_r20/system/extras/simpleperf/scripts/test/test_utils.py (revision 288bf5226967eb3dac5cce6c939ccc2a7f2b4fe5)
1*288bf522SAndroid Build Coastguard Worker#!/usr/bin/env python3
2*288bf522SAndroid Build Coastguard Worker#
3*288bf522SAndroid Build Coastguard Worker# Copyright (C) 2021 The Android Open Source Project
4*288bf522SAndroid Build Coastguard Worker#
5*288bf522SAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License");
6*288bf522SAndroid Build Coastguard Worker# you may not use this file except in compliance with the License.
7*288bf522SAndroid Build Coastguard Worker# You may obtain a copy of the License at
8*288bf522SAndroid Build Coastguard Worker#
9*288bf522SAndroid Build Coastguard Worker#      http://www.apache.org/licenses/LICENSE-2.0
10*288bf522SAndroid Build Coastguard Worker#
11*288bf522SAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software
12*288bf522SAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS,
13*288bf522SAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14*288bf522SAndroid Build Coastguard Worker# See the License for the specific language governing permissions and
15*288bf522SAndroid Build Coastguard Worker# limitations under the License.
16*288bf522SAndroid Build Coastguard Worker#
17*288bf522SAndroid Build Coastguard Worker"""test_utils.py: utils for testing.
18*288bf522SAndroid Build Coastguard Worker"""
19*288bf522SAndroid Build Coastguard Worker
20*288bf522SAndroid Build Coastguard Workerimport logging
21*288bf522SAndroid Build Coastguard Workerfrom multiprocessing.connection import Connection
22*288bf522SAndroid Build Coastguard Workerimport os
23*288bf522SAndroid Build Coastguard Workerfrom pathlib import Path
24*288bf522SAndroid Build Coastguard Workerimport re
25*288bf522SAndroid Build Coastguard Workerimport shutil
26*288bf522SAndroid Build Coastguard Workerimport sys
27*288bf522SAndroid Build Coastguard Workerimport subprocess
28*288bf522SAndroid Build Coastguard Workerimport time
29*288bf522SAndroid Build Coastguard Workerfrom typing import List, Optional, Tuple, Union
30*288bf522SAndroid Build Coastguard Workerimport unittest
31*288bf522SAndroid Build Coastguard Worker
32*288bf522SAndroid Build Coastguard Workerfrom simpleperf_utils import remove, get_script_dir, AdbHelper, is_windows, bytes_to_str
33*288bf522SAndroid Build Coastguard Worker
34*288bf522SAndroid Build Coastguard WorkerINFERNO_SCRIPT = str(Path(__file__).parents[1] / ('inferno.bat' if is_windows() else 'inferno.sh'))
35*288bf522SAndroid Build Coastguard Worker
36*288bf522SAndroid Build Coastguard Worker
37*288bf522SAndroid Build Coastguard Workerclass TestHelper:
38*288bf522SAndroid Build Coastguard Worker    """ Keep global test options. """
39*288bf522SAndroid Build Coastguard Worker
40*288bf522SAndroid Build Coastguard Worker    @classmethod
41*288bf522SAndroid Build Coastguard Worker    def init(
42*288bf522SAndroid Build Coastguard Worker            cls, test_dir: str, testdata_dir: str, use_browser: bool, ndk_path: Optional[str],
43*288bf522SAndroid Build Coastguard Worker            device_serial_number: Optional[str],
44*288bf522SAndroid Build Coastguard Worker            progress_conn: Optional[Connection]):
45*288bf522SAndroid Build Coastguard Worker        """
46*288bf522SAndroid Build Coastguard Worker            When device_serial_number is None, no Android device is used.
47*288bf522SAndroid Build Coastguard Worker            When device_serial_number is '', use the default Android device.
48*288bf522SAndroid Build Coastguard Worker            When device_serial_number is not empty, select Android device by serial number.
49*288bf522SAndroid Build Coastguard Worker        """
50*288bf522SAndroid Build Coastguard Worker        cls.script_dir = Path(__file__).resolve().parents[1]
51*288bf522SAndroid Build Coastguard Worker        cls.test_base_dir = Path(test_dir).resolve()
52*288bf522SAndroid Build Coastguard Worker        cls.test_base_dir.mkdir(parents=True, exist_ok=True)
53*288bf522SAndroid Build Coastguard Worker        cls.testdata_dir = Path(testdata_dir).resolve()
54*288bf522SAndroid Build Coastguard Worker        cls.browser_option = [] if use_browser else ['--no_browser']
55*288bf522SAndroid Build Coastguard Worker        cls.ndk_path = ndk_path
56*288bf522SAndroid Build Coastguard Worker        cls.progress_conn = progress_conn
57*288bf522SAndroid Build Coastguard Worker
58*288bf522SAndroid Build Coastguard Worker        # Logs can come from multiple processes. So use append mode to avoid overwrite.
59*288bf522SAndroid Build Coastguard Worker        cls.log_fh = open(cls.test_base_dir / 'test.log', 'a')
60*288bf522SAndroid Build Coastguard Worker        logging.getLogger().handlers.clear()
61*288bf522SAndroid Build Coastguard Worker        logging.getLogger().addHandler(logging.StreamHandler(cls.log_fh))
62*288bf522SAndroid Build Coastguard Worker        os.close(sys.stderr.fileno())
63*288bf522SAndroid Build Coastguard Worker        os.dup2(cls.log_fh.fileno(), sys.stderr.fileno())
64*288bf522SAndroid Build Coastguard Worker
65*288bf522SAndroid Build Coastguard Worker        if device_serial_number is not None:
66*288bf522SAndroid Build Coastguard Worker            if device_serial_number:
67*288bf522SAndroid Build Coastguard Worker                os.environ['ANDROID_SERIAL'] = device_serial_number
68*288bf522SAndroid Build Coastguard Worker            cls.adb = AdbHelper(enable_switch_to_root=True)
69*288bf522SAndroid Build Coastguard Worker            cls.android_version = cls.adb.get_android_version()
70*288bf522SAndroid Build Coastguard Worker            cls.device_features = None
71*288bf522SAndroid Build Coastguard Worker
72*288bf522SAndroid Build Coastguard Worker    @classmethod
73*288bf522SAndroid Build Coastguard Worker    def log(cls, s: str):
74*288bf522SAndroid Build Coastguard Worker        cls.log_fh.write(s + '\n')
75*288bf522SAndroid Build Coastguard Worker        # Child processes can also write to log file, so flush it immediately to keep the order.
76*288bf522SAndroid Build Coastguard Worker        cls.log_fh.flush()
77*288bf522SAndroid Build Coastguard Worker
78*288bf522SAndroid Build Coastguard Worker    @classmethod
79*288bf522SAndroid Build Coastguard Worker    def testdata_path(cls, testdata_name: str) -> str:
80*288bf522SAndroid Build Coastguard Worker        """ Return the path of a test data. """
81*288bf522SAndroid Build Coastguard Worker        return str(cls.testdata_dir / testdata_name)
82*288bf522SAndroid Build Coastguard Worker
83*288bf522SAndroid Build Coastguard Worker    @classmethod
84*288bf522SAndroid Build Coastguard Worker    def get_test_dir(cls, test_name: str) -> Path:
85*288bf522SAndroid Build Coastguard Worker        """ Return the dir to run a test. """
86*288bf522SAndroid Build Coastguard Worker        return cls.test_base_dir / test_name
87*288bf522SAndroid Build Coastguard Worker
88*288bf522SAndroid Build Coastguard Worker    @classmethod
89*288bf522SAndroid Build Coastguard Worker    def script_path(cls, script_name: str) -> str:
90*288bf522SAndroid Build Coastguard Worker        """ Return the dir of python scripts. """
91*288bf522SAndroid Build Coastguard Worker        return str(cls.script_dir / script_name)
92*288bf522SAndroid Build Coastguard Worker
93*288bf522SAndroid Build Coastguard Worker    @classmethod
94*288bf522SAndroid Build Coastguard Worker    def get_device_features(cls):
95*288bf522SAndroid Build Coastguard Worker        if cls.device_features is None:
96*288bf522SAndroid Build Coastguard Worker            args = [sys.executable, cls.script_path(
97*288bf522SAndroid Build Coastguard Worker                'run_simpleperf_on_device.py'), 'list', '--show-features']
98*288bf522SAndroid Build Coastguard Worker            output = subprocess.check_output(args, stderr=TestHelper.log_fh)
99*288bf522SAndroid Build Coastguard Worker            output = bytes_to_str(output)
100*288bf522SAndroid Build Coastguard Worker            cls.device_features = output.split()
101*288bf522SAndroid Build Coastguard Worker        return cls.device_features
102*288bf522SAndroid Build Coastguard Worker
103*288bf522SAndroid Build Coastguard Worker    @classmethod
104*288bf522SAndroid Build Coastguard Worker    def is_trace_offcpu_supported(cls):
105*288bf522SAndroid Build Coastguard Worker        return 'trace-offcpu' in cls.get_device_features()
106*288bf522SAndroid Build Coastguard Worker
107*288bf522SAndroid Build Coastguard Worker    @classmethod
108*288bf522SAndroid Build Coastguard Worker    def get_32bit_abi(cls):
109*288bf522SAndroid Build Coastguard Worker        return cls.adb.get_property('ro.product.cpu.abilist32').strip().split(',')[0]
110*288bf522SAndroid Build Coastguard Worker
111*288bf522SAndroid Build Coastguard Worker    @classmethod
112*288bf522SAndroid Build Coastguard Worker    def get_kernel_version(cls) -> Tuple[int]:
113*288bf522SAndroid Build Coastguard Worker        output = cls.adb.check_run_and_return_output(['shell', 'uname', '-r'])
114*288bf522SAndroid Build Coastguard Worker        m = re.search(r'^(\d+)\.(\d+)', output)
115*288bf522SAndroid Build Coastguard Worker        assert m
116*288bf522SAndroid Build Coastguard Worker        return (int(m.group(1)), int(m.group(2)))
117*288bf522SAndroid Build Coastguard Worker
118*288bf522SAndroid Build Coastguard Worker    @classmethod
119*288bf522SAndroid Build Coastguard Worker    def write_progress(cls, progress: str):
120*288bf522SAndroid Build Coastguard Worker        if cls.progress_conn:
121*288bf522SAndroid Build Coastguard Worker            cls.progress_conn.send(progress)
122*288bf522SAndroid Build Coastguard Worker
123*288bf522SAndroid Build Coastguard Worker
124*288bf522SAndroid Build Coastguard Workerclass TestBase(unittest.TestCase):
125*288bf522SAndroid Build Coastguard Worker    def setUp(self):
126*288bf522SAndroid Build Coastguard Worker        """ Run each test in a separate dir. """
127*288bf522SAndroid Build Coastguard Worker        self.test_dir = TestHelper.get_test_dir(
128*288bf522SAndroid Build Coastguard Worker            '%s.%s' % (self.__class__.__name__, self._testMethodName))
129*288bf522SAndroid Build Coastguard Worker        self.test_dir.mkdir()
130*288bf522SAndroid Build Coastguard Worker        os.chdir(self.test_dir)
131*288bf522SAndroid Build Coastguard Worker        TestHelper.log('begin test %s.%s' % (self.__class__.__name__, self._testMethodName))
132*288bf522SAndroid Build Coastguard Worker
133*288bf522SAndroid Build Coastguard Worker    def run(self, result=None):
134*288bf522SAndroid Build Coastguard Worker        start_time = time.time()
135*288bf522SAndroid Build Coastguard Worker        ret = super(TestBase, self).run(result)
136*288bf522SAndroid Build Coastguard Worker        if result.errors and result.errors[-1][0] == self:
137*288bf522SAndroid Build Coastguard Worker            status = 'FAILED'
138*288bf522SAndroid Build Coastguard Worker            err_info = result.errors[-1][1]
139*288bf522SAndroid Build Coastguard Worker        elif result.failures and result.failures[-1][0] == self:
140*288bf522SAndroid Build Coastguard Worker            status = 'FAILED'
141*288bf522SAndroid Build Coastguard Worker            err_info = result.failures[-1][1]
142*288bf522SAndroid Build Coastguard Worker        elif result.skipped and result.skipped[-1][0] == self:
143*288bf522SAndroid Build Coastguard Worker            status = 'SKIPPED'
144*288bf522SAndroid Build Coastguard Worker        else:
145*288bf522SAndroid Build Coastguard Worker            status = 'OK'
146*288bf522SAndroid Build Coastguard Worker
147*288bf522SAndroid Build Coastguard Worker        time_taken = time.time() - start_time
148*288bf522SAndroid Build Coastguard Worker        TestHelper.log(
149*288bf522SAndroid Build Coastguard Worker            'end test %s.%s %s (%.3fs)' %
150*288bf522SAndroid Build Coastguard Worker            (self.__class__.__name__, self._testMethodName, status, time_taken))
151*288bf522SAndroid Build Coastguard Worker        if status == 'FAILED':
152*288bf522SAndroid Build Coastguard Worker            TestHelper.log(err_info)
153*288bf522SAndroid Build Coastguard Worker
154*288bf522SAndroid Build Coastguard Worker        # Remove test data for passed tests to save space.
155*288bf522SAndroid Build Coastguard Worker        if status == 'OK':
156*288bf522SAndroid Build Coastguard Worker            remove(self.test_dir)
157*288bf522SAndroid Build Coastguard Worker        TestHelper.write_progress(
158*288bf522SAndroid Build Coastguard Worker            '%s.%s  %s  %.3fs' %
159*288bf522SAndroid Build Coastguard Worker            (self.__class__.__name__, self._testMethodName, status, time_taken))
160*288bf522SAndroid Build Coastguard Worker        return ret
161*288bf522SAndroid Build Coastguard Worker
162*288bf522SAndroid Build Coastguard Worker    def run_cmd(self, args: List[str], return_output=False, drop_output=True) -> str:
163*288bf522SAndroid Build Coastguard Worker        if args[0] == 'report_html.py' or args[0] == INFERNO_SCRIPT:
164*288bf522SAndroid Build Coastguard Worker            args += TestHelper.browser_option
165*288bf522SAndroid Build Coastguard Worker        if TestHelper.ndk_path:
166*288bf522SAndroid Build Coastguard Worker            if args[0] in ['app_profiler.py', 'binary_cache_builder.py', 'pprof_proto_generator.py',
167*288bf522SAndroid Build Coastguard Worker                           'report_html.py', 'annotate.py']:
168*288bf522SAndroid Build Coastguard Worker                args += ['--ndk_path', TestHelper.ndk_path]
169*288bf522SAndroid Build Coastguard Worker        if args[0].endswith('.py'):
170*288bf522SAndroid Build Coastguard Worker            args = [sys.executable, TestHelper.script_path(args[0])] + args[1:]
171*288bf522SAndroid Build Coastguard Worker        use_shell = args[0].endswith('.bat')
172*288bf522SAndroid Build Coastguard Worker        try:
173*288bf522SAndroid Build Coastguard Worker            if return_output:
174*288bf522SAndroid Build Coastguard Worker                stdout_fd = subprocess.PIPE
175*288bf522SAndroid Build Coastguard Worker                drop_output = False
176*288bf522SAndroid Build Coastguard Worker            elif drop_output:
177*288bf522SAndroid Build Coastguard Worker                stdout_fd = subprocess.DEVNULL
178*288bf522SAndroid Build Coastguard Worker            else:
179*288bf522SAndroid Build Coastguard Worker                stdout_fd = None
180*288bf522SAndroid Build Coastguard Worker
181*288bf522SAndroid Build Coastguard Worker            subproc = subprocess.Popen(args, stdout=stdout_fd,
182*288bf522SAndroid Build Coastguard Worker                                       stderr=TestHelper.log_fh, shell=use_shell)
183*288bf522SAndroid Build Coastguard Worker            stdout_data, _ = subproc.communicate()
184*288bf522SAndroid Build Coastguard Worker            output_data = bytes_to_str(stdout_data)
185*288bf522SAndroid Build Coastguard Worker            returncode = subproc.returncode
186*288bf522SAndroid Build Coastguard Worker
187*288bf522SAndroid Build Coastguard Worker        except OSError:
188*288bf522SAndroid Build Coastguard Worker            returncode = None
189*288bf522SAndroid Build Coastguard Worker        self.assertEqual(returncode, 0, msg="failed to run cmd: %s" % args)
190*288bf522SAndroid Build Coastguard Worker        if return_output:
191*288bf522SAndroid Build Coastguard Worker            return output_data
192*288bf522SAndroid Build Coastguard Worker        return ''
193*288bf522SAndroid Build Coastguard Worker
194*288bf522SAndroid Build Coastguard Worker    def check_strings_in_file(self, filename, strings: List[Union[str, re.Pattern]]):
195*288bf522SAndroid Build Coastguard Worker        self.check_exist(filename=filename)
196*288bf522SAndroid Build Coastguard Worker        with open(filename, 'r') as fh:
197*288bf522SAndroid Build Coastguard Worker            self.check_strings_in_content(fh.read(), strings)
198*288bf522SAndroid Build Coastguard Worker
199*288bf522SAndroid Build Coastguard Worker    def check_exist(self, filename=None, dirname=None):
200*288bf522SAndroid Build Coastguard Worker        if filename:
201*288bf522SAndroid Build Coastguard Worker            self.assertTrue(os.path.isfile(filename), filename)
202*288bf522SAndroid Build Coastguard Worker        if dirname:
203*288bf522SAndroid Build Coastguard Worker            self.assertTrue(os.path.isdir(dirname), dirname)
204*288bf522SAndroid Build Coastguard Worker
205*288bf522SAndroid Build Coastguard Worker    def check_strings_in_content(self, content: str, strings: List[Union[str, re.Pattern]]):
206*288bf522SAndroid Build Coastguard Worker        fulfilled = []
207*288bf522SAndroid Build Coastguard Worker        for s in strings:
208*288bf522SAndroid Build Coastguard Worker            if isinstance(s, re.Pattern):
209*288bf522SAndroid Build Coastguard Worker                fulfilled.append(s.search(content))
210*288bf522SAndroid Build Coastguard Worker            else:
211*288bf522SAndroid Build Coastguard Worker                fulfilled.append(s in content)
212*288bf522SAndroid Build Coastguard Worker        self.check_fulfilled_entries(fulfilled, strings)
213*288bf522SAndroid Build Coastguard Worker
214*288bf522SAndroid Build Coastguard Worker    def check_fulfilled_entries(self, fulfilled, entries):
215*288bf522SAndroid Build Coastguard Worker        failed_entries = []
216*288bf522SAndroid Build Coastguard Worker        for ok, entry in zip(fulfilled, entries):
217*288bf522SAndroid Build Coastguard Worker            if not ok:
218*288bf522SAndroid Build Coastguard Worker                failed_entries.append(entry)
219*288bf522SAndroid Build Coastguard Worker
220*288bf522SAndroid Build Coastguard Worker        if failed_entries:
221*288bf522SAndroid Build Coastguard Worker            self.fail('failed in below entries: %s' % (failed_entries,))
222