1#!/usr/bin/env python3 2# 3# Copyright 2024, The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17"""A collection of integration test cases for atest.""" 18 19import concurrent.futures 20import csv 21import dataclasses 22import functools 23import multiprocessing 24import pathlib 25from typing import Any, Optional 26import atest_integration_test 27 28 29@dataclasses.dataclass 30class _AtestCommandUsage: 31 """A class to hold the atest command and its usage frequency.""" 32 33 command: str 34 usage_count: int 35 user_count: int 36 37 @staticmethod 38 def to_json(usage: '_AtestCommandUsage') -> dict[str, Any]: 39 """Converts an _AtestCommandUsage object to a JSON dictionary.""" 40 return { 41 'command': usage.command, 42 'usage_count': usage.usage_count, 43 'user_count': usage.user_count, 44 } 45 46 @staticmethod 47 def from_json(json_dict: dict[str, Any]) -> '_AtestCommandUsage': 48 """Creates an _AtestCommandUsage object from a JSON dictionary.""" 49 return _AtestCommandUsage( 50 json_dict['command'], 51 json_dict['usage_count'], 52 json_dict['user_count'], 53 ) 54 55 56class AtestDryRunDiffTests(atest_integration_test.AtestTestCase): 57 """Tests to compare the atest dry run output between atest prod binary and dev binary.""" 58 59 def setUp(self): 60 super().setUp() 61 self.maxDiff = None 62 63 def test_dry_run_output_diff(self): 64 """Tests to compare the atest dry run output between atest prod binary and dev binary.""" 65 script = self.create_atest_script() 66 script.add_build_step(self._build_step) 67 script.add_test_step(self._test_step) 68 script.run() 69 70 def _get_atest_command_usages( 71 self, repo_root: str, dry_run_diff_test_cmd_input_file: Optional[str] 72 ) -> list[_AtestCommandUsage]: 73 """Returns the atest command usages for the dry run diff test. 74 75 Returns: 76 A list of _AtestCommandUsage objects. 77 """ 78 if not dry_run_diff_test_cmd_input_file: 79 return [ 80 _AtestCommandUsage(cmd, -1, -1) for cmd in _default_input_commands 81 ] 82 with ( 83 pathlib.Path(repo_root) 84 .joinpath(dry_run_diff_test_cmd_input_file) 85 .open() 86 ) as input_file: 87 reader = csv.reader(input_file) 88 return [_AtestCommandUsage(*row) for row in reader if row and row[0]] 89 90 def _build_step( 91 self, 92 step_in: atest_integration_test.StepInput, 93 ) -> atest_integration_test.StepOutput: 94 95 run_command = lambda use_prod, command_usage: self.run_atest_command( 96 '--dry-run -it ' + command_usage.command, 97 step_in, 98 include_device_serial=False, 99 use_prebuilt_atest_binary=use_prod, 100 pipe_to_stdin='n', 101 ) 102 get_prod_result = functools.partial(run_command, True) 103 get_dev_result = functools.partial(run_command, False) 104 105 command_usages = self._get_atest_command_usages( 106 step_in.get_repo_root(), 107 step_in.get_config().dry_run_diff_test_cmd_input_file, 108 ) 109 110 with concurrent.futures.ThreadPoolExecutor( 111 max_workers=multiprocessing.cpu_count() 112 ) as executor: 113 # Run the version command with -c to clear the cache by the prod binary. 114 self.run_atest_command( 115 '--version -c', 116 step_in, 117 include_device_serial=False, 118 use_prebuilt_atest_binary=True, 119 ) 120 cmd_results_prod = list(executor.map(get_prod_result, command_usages)) 121 # Run the version command with -c to clear the cache by the dev binary. 122 self.run_atest_command( 123 '--version -c', 124 step_in, 125 include_device_serial=False, 126 use_prebuilt_atest_binary=False, 127 ) 128 cmd_results_dev = list(executor.map(get_dev_result, command_usages)) 129 130 step_out = self.create_step_output() 131 step_out.set_snapshot_include_paths([]) 132 step_out.add_snapshot_obj( 133 'usages', list(map(_AtestCommandUsage.to_json, command_usages)) 134 ) 135 step_out.add_snapshot_obj( 136 'returncode_prod', 137 list(map(lambda result: result.get_returncode(), cmd_results_prod)), 138 ) 139 step_out.add_snapshot_obj( 140 'returncode_dev', 141 list(map(lambda result: result.get_returncode(), cmd_results_dev)), 142 ) 143 step_out.add_snapshot_obj( 144 'elapsed_time_prod', 145 list(map(lambda result: result.get_elapsed_time(), cmd_results_prod)), 146 ) 147 step_out.add_snapshot_obj( 148 'elapsed_time_dev', 149 list(map(lambda result: result.get_elapsed_time(), cmd_results_dev)), 150 ) 151 step_out.add_snapshot_obj( 152 'runner_cmd_prod', 153 list( 154 map( 155 lambda result: result.get_atest_log_values_from_prefix( 156 atest_integration_test.DRY_RUN_COMMAND_LOG_PREFIX 157 ), 158 cmd_results_prod, 159 ) 160 ), 161 ) 162 step_out.add_snapshot_obj( 163 'runner_cmd_dev', 164 list( 165 map( 166 lambda result: result.get_atest_log_values_from_prefix( 167 atest_integration_test.DRY_RUN_COMMAND_LOG_PREFIX 168 ), 169 cmd_results_dev, 170 ) 171 ), 172 ) 173 174 return step_out 175 176 def _test_step(self, step_in: atest_integration_test.StepInput) -> None: 177 usages = list(map(_AtestCommandUsage.from_json, step_in.get_obj('usages'))) 178 returncode_prod = step_in.get_obj('returncode_prod') 179 returncode_dev = step_in.get_obj('returncode_dev') 180 elapsed_time_prod = step_in.get_obj('elapsed_time_prod') 181 elapsed_time_dev = step_in.get_obj('elapsed_time_dev') 182 runner_cmd_prod = step_in.get_obj('runner_cmd_prod') 183 runner_cmd_dev = step_in.get_obj('runner_cmd_dev') 184 185 for idx in range(len(usages)): 186 impact_str = ( 187 'Potential' 188 f' impacted number of users: {usages[idx].user_count}, number of' 189 f' invocations: {usages[idx].usage_count}.' 190 ) 191 with self.subTest(name=f'{usages[idx].command}_returncode'): 192 self.assertEqual( 193 returncode_prod[idx], 194 returncode_dev[idx], 195 f'Return code mismatch for command: {usages[idx].command}. Prod:' 196 f' {returncode_prod[idx]} Dev: {returncode_dev[idx]}. {impact_str}', 197 ) 198 with self.subTest(name=f'{usages[idx].command}_elapsed_time'): 199 self.assertAlmostEqual( 200 elapsed_time_prod[idx], 201 elapsed_time_dev[idx], 202 delta=12, 203 msg=( 204 f'Elapsed time mismatch for command: {usages[idx].command}.' 205 f' Prod: {elapsed_time_prod[idx]} Dev:' 206 f' {elapsed_time_dev[idx]} {impact_str}' 207 ), 208 ) 209 with self.subTest( 210 name=f'{usages[idx].command}_runner_cmd_has_same_elements' 211 ): 212 self.assertEqual( 213 len(runner_cmd_prod[idx]), 214 len(runner_cmd_dev[idx]), 215 'Nummber of runner commands mismatch for command:' 216 ' {usages[idx].command}.', 217 ) 218 219 for cmd_idx in range(len(runner_cmd_prod[idx])): 220 sanitized_runner_cmd_prod = ( 221 atest_integration_test.sanitize_runner_command(runner_cmd_prod[idx][cmd_idx]) 222 ) 223 sanitized_runner_cmd_dev = ( 224 atest_integration_test.sanitize_runner_command(runner_cmd_dev[idx][cmd_idx]) 225 ) 226 self.assertEqual( 227 set(sanitized_runner_cmd_prod.split(' ')), 228 set(sanitized_runner_cmd_dev.split(' ')), 229 'Runner command mismatch for command:' 230 f' {usages[idx].command}.\nProd:\n' 231 f' {sanitized_runner_cmd_prod}\nDev:\n{sanitized_runner_cmd_dev}\n' 232 f' {impact_str}', 233 ) 234 235 236# A copy of the list of atest commands tested in the command verification tests. 237_default_input_commands = [ 238 'AnimatorTest', 239 'CtsAnimationTestCases:AnimatorTest', 240 'CtsSampleDeviceTestCases:android.sample.cts', 241 'CtsAnimationTestCases CtsSampleDeviceTestCases', 242 'HelloWorldTests', 243 'android.animation.cts', 244 'android.sample.cts.SampleDeviceReportLogTest', 245 'android.sample.cts.SampleDeviceTest#testSharedPreferences', 246 'hello_world_test', 247 'native-benchmark', 248 'platform_testing/tests/example/native', 249 'platform_testing/tests/example/native/Android.bp', 250 'tools/tradefederation/core/res/config/native-benchmark.xml', 251 'QuickAccessWalletRoboTests', 252 'QuickAccessWalletRoboTests --host', 253 'CtsWifiAwareTestCases', 254 'pts-bot:PAN/GN/MISC/UUID/BV-01-C', 255 'TeeUIUtilsTest', 256 'android.security.cts.PermissionMemoryFootprintTest', 257 'CtsSampleDeviceTestCases:SampleDeviceTest#testSharedPreferences', 258 'CtsSampleDeviceTestCases:android.sample.cts.SampleDeviceReportLogTest', 259 ( 260 'PerInstance/CameraHidlTest#' 261 'configureInjectionStreamsAvailableOutputs/0_internal_0' 262 ), 263 ( 264 'VtsHalCameraProviderV2_4TargetTest:PerInstance/' 265 'CameraHidlTest#configureInjectionStreamsAvailableOutputs/' 266 '0_internal_0' 267 ), 268 ( 269 'TeeUIUtilsTest#intersectTest,ConvexObjectConstruction,' 270 'ConvexObjectLineIntersection' 271 ), 272 ( 273 'CtsSecurityTestCases:android.security.cts.' 274 'ActivityManagerTest#testActivityManager_' 275 'registerUidChangeObserver_allPermission' 276 ), 277 ( 278 'cts/tests/tests/security/src/android/security/cts/' 279 'ActivityManagerTest.java#testActivityManager_' 280 'registerUidChangeObserver_allPermission' 281 ), 282 ( 283 'cts/tests/tests/security/src/android/security/cts/' 284 'PermissionMemoryFootprintTest.kt#' 285 'checkAppsCantIncreasePermissionSizeAfterCreating' 286 ), 287 ( 288 'android.security.cts.PermissionMemoryFootprintTest#' 289 'checkAppsCantIncreasePermissionSizeAfterCreating' 290 ), 291] 292 293if __name__ == '__main__': 294 atest_integration_test.main() 295