1# Copyright 2024 The Android Open Source Project 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15import argparse 16import json 17import logging 18import multi_device_utils 19import os 20import os.path 21from pathlib import Path 22import re 23import subprocess 24import tempfile 25import time 26import yaml 27 28 29RESULT_KEY = 'result' 30RESULT_PASS = 'PASS' 31RESULT_FAIL = 'FAIL' 32CONFIG_FILE = os.path.join(os.getcwd(), 'config.yml') 33TESTS_DIR = os.path.join(os.getcwd(), 'tests') 34CTS_VERIFIER_PACKAGE_NAME = 'com.android.cts.verifier' 35MOBLY_TEST_SUMMARY_TXT_FILE = 'test_mobly_summary.txt' 36MULTI_DEVICE_TEST_ACTIVITY = ( 37 'com.android.cts.verifier/.multidevice.MultiDeviceTestsActivity' 38) 39ACTION_HOST_TEST_RESULT = 'com.android.cts.verifier.ACTION_HOST_TEST_RESULT' 40EXTRA_VERSION = 'com.android.cts.verifier.extra.HOST_TEST_RESULT' 41ACTIVITY_START_WAIT = 2 # seconds 42 43 44def get_config_file_contents(): 45 """Read the config file contents from a YML file. 46 47 Args: None 48 49 Returns: 50 config_file_contents: a dict read from config.yml 51 """ 52 with open(CONFIG_FILE) as file: 53 config_file_contents = yaml.safe_load(file) 54 return config_file_contents 55 56 57def get_device_serial_number(config_file_contents): 58 """Returns the serial number of the dut devices. 59 60 Args: 61 config_file_contents: dict read from config.yml file. 62 63 Returns: 64 The serial numbers (str) or None if the device is not found. 65 """ 66 67 device_serial_numbers = [] 68 for _, testbed_data in config_file_contents.items(): 69 for data_dict in testbed_data: 70 android_devices = data_dict.get('Controllers', {}).get( 71 'AndroidDevice', [] 72 ) 73 74 for device_dict in android_devices: 75 device_serial_numbers.append(device_dict.get('serial')) 76 return device_serial_numbers 77 78 79def report_result(device_id, results): 80 """Sends a pass/fail result to the device, via an intent. 81 82 Args: 83 device_id: the serial number of the device. 84 results: a dictionary contains all multi-device test names as key and 85 result/summary of current test run. 86 """ 87 adb = f'adb -s {device_id}' 88 89 # Start MultiDeviceTestsActivity to receive test results 90 cmd = ( 91 f'{adb} shell am start' 92 f' {MULTI_DEVICE_TEST_ACTIVITY} --activity-brought-to-front' 93 ) 94 multi_device_utils.run(cmd) 95 time.sleep(ACTIVITY_START_WAIT) 96 97 json_results = json.dumps(results) 98 cmd = ( 99 f'{adb} shell am broadcast -a {ACTION_HOST_TEST_RESULT} --es' 100 f" {EXTRA_VERSION} '{json_results}'" 101 ) 102 if len(cmd) > 4095: 103 logging.info('Command string might be too long! len:%s', len(cmd)) 104 multi_device_utils.run(cmd) 105 106 107def main(): 108 """Run all Multi-device Mobly tests and collect results.""" 109 110 logging.basicConfig(level=logging.INFO) 111 topdir = tempfile.mkdtemp(prefix='MultiDevice_') 112 subprocess.call(['chmod', 'g+rx', topdir]) # Add permissions 113 114 # Parse command-line arguments 115 parser = argparse.ArgumentParser() 116 parser.add_argument( 117 '--test_cases', 118 nargs='+', 119 help='Specific test cases to run (space-separated)') 120 parser.add_argument( 121 '--test_files', 122 nargs='+', 123 help='Filter test files by name (substring match, space-separated)') 124 args = parser.parse_args() 125 126 config_file_contents = get_config_file_contents() 127 device_ids = get_device_serial_number(config_file_contents) 128 129 test_results = {} 130 test_summary_file_list = [] 131 132 # Run tests 133 for root, _, files in os.walk(TESTS_DIR): 134 for test_file in files: 135 if test_file.endswith('-py-ctsv') and ( 136 args.test_files is None or 137 test_file in args.test_files 138 ): 139 test_file_path = os.path.join(root, test_file) 140 logging.info('Start running test: %s', test_file) 141 cmd = [ 142 test_file_path, # Use the full path to the test file 143 '-c', 144 CONFIG_FILE, 145 '--testbed', 146 test_file, 147 ] 148 149 if args.test_cases: 150 cmd.extend(['--tests']) 151 for test_case in args.test_cases: 152 cmd.extend([test_case]) 153 154 summary_file_path = os.path.join(topdir, MOBLY_TEST_SUMMARY_TXT_FILE) 155 156 test_completed = False 157 with open(summary_file_path, 'w+') as fp: 158 subprocess.run(cmd, stdout=fp, check=False) 159 fp.seek(0) 160 for line in fp: 161 if line.startswith('Test summary saved in'): 162 match = re.search(r'"(.*?)"', line) # Get test artifacts file path 163 if match: 164 test_summary = Path(match.group(1)) 165 test_artifact = test_summary.parent 166 logging.info( 167 'Please check the test artifacts of %s under: %s', test_file, test_artifact 168 ) 169 if test_summary.exists(): 170 test_summary_file_list.append(test_summary) 171 test_completed = True 172 break 173 if not test_completed: 174 logging.error('Failed to get test summary file path') 175 os.remove(os.path.join(topdir, MOBLY_TEST_SUMMARY_TXT_FILE)) 176 177 # Parse test summary files 178 for test_summary_file in test_summary_file_list: 179 with open(test_summary_file) as file: 180 test_summary_content = yaml.safe_load_all(file) 181 for doc in test_summary_content: 182 if doc['Type'] == 'Record': 183 test_key = f"{doc['Test Class']}#{doc['Test Name']}" 184 result = ( 185 RESULT_PASS if doc['Result'] in ('PASS', 'SKIP') else RESULT_FAIL 186 ) 187 test_results.setdefault(test_key, {RESULT_KEY: result}) 188 189 for device_id in device_ids: 190 report_result(device_id, test_results) 191 192 logging.info('Test execution completed. Results: %s', test_results) 193 194 195if __name__ == '__main__': 196 main() 197