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