1# Copyright 2014 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 json 16import logging 17import os 18import os.path 19import subprocess 20import sys 21import tempfile 22import time 23import yaml 24 25import capture_request_utils 26import camera_properties_utils 27import image_processing_utils 28import its_session_utils 29 30import numpy as np 31 32YAML_FILE_DIR = os.environ['CAMERA_ITS_TOP'] 33CONFIG_FILE = os.path.join(YAML_FILE_DIR, 'config.yml') 34TEST_KEY_TABLET = 'tablet' 35TEST_KEY_SENSOR_FUSION = 'sensor_fusion' 36LOAD_SCENE_DELAY = 1 # seconds 37ACTIVITY_START_WAIT = 1.5 # seconds 38 39NUM_TRIES = 2 40RESULT_PASS = 'PASS' 41RESULT_FAIL = 'FAIL' 42RESULT_NOT_EXECUTED = 'NOT_EXECUTED' 43RESULT_KEY = 'result' 44SUMMARY_KEY = 'summary' 45RESULT_VALUES = {RESULT_PASS, RESULT_FAIL, RESULT_NOT_EXECUTED} 46ITS_TEST_ACTIVITY = 'com.android.cts.verifier/.camera.its.ItsTestActivity' 47ACTION_ITS_RESULT = 'com.android.cts.verifier.camera.its.ACTION_ITS_RESULT' 48EXTRA_VERSION = 'camera.its.extra.VERSION' 49CURRENT_ITS_VERSION = '1.0' # version number to sync with CtsVerifier 50EXTRA_CAMERA_ID = 'camera.its.extra.CAMERA_ID' 51EXTRA_RESULTS = 'camera.its.extra.RESULTS' 52TIME_KEY_START = 'start' 53TIME_KEY_END = 'end' 54VALID_CONTROLLERS = ('arduino', 'canakit') 55_INT_STR_DICT = {'11': '1_1', '12': '1_2'} # recover replaced '_' in scene def 56 57# All possible scenes 58# Notes on scene names: 59# scene*_1/2/... are same scene split to load balance run times for scenes 60# scene*_a/b/... are similar scenes that share one or more tests 61_ALL_SCENES = [ 62 'scene0', 'scene1_1', 'scene1_2', 'scene2_a', 'scene2_b', 'scene2_c', 63 'scene2_d', 'scene2_e', 'scene3', 'scene4', 'scene5', 'scene6', 64 'sensor_fusion', 'scene_change' 65] 66 67# Scenes that can be automated through tablet display 68_AUTO_SCENES = [ 69 'scene0', 'scene1_1', 'scene1_2', 'scene2_a', 'scene2_b', 'scene2_c', 70 'scene2_d', 'scene2_e', 'scene3', 'scene4', 'scene6', 'scene_change' 71] 72 73# Scenes that are logically grouped and can be called as group 74_GROUPED_SCENES = { 75 'scene1': ['scene1_1', 'scene1_2'], 76 'scene2': ['scene2_a', 'scene2_b', 'scene2_c', 'scene2_d', 'scene2_e'] 77} 78 79# Scenes that have to be run manually regardless of configuration 80_MANUAL_SCENES = ['scene5'] 81 82# Scene requirements for manual testing. 83_SCENE_REQ = { 84 'scene0': None, 85 'scene1_1': 'A grey card covering at least the middle 30% of the scene', 86 'scene1_2': 'A grey card covering at least the middle 30% of the scene', 87 'scene2_a': 'The picture with 3 faces in tests/scene2_a/scene2_a.png', 88 'scene2_b': 'The picture with 3 faces in tests/scene2_b/scene2_b.png', 89 'scene2_c': 'The picture with 3 faces in tests/scene2_c/scene2_c.png', 90 'scene2_d': 'The picture with 3 faces in tests/scene2_d/scene2_d.png', 91 'scene2_e': 'The picture with 3 faces in tests/scene2_e/scene2_e.png', 92 'scene3': 'The ISO12233 chart', 93 'scene4': 'A test chart of a circle covering at least the middle 50% of ' 94 'the scene. See tests/scene4/scene4.png', 95 'scene5': 'Capture images with a diffuser attached to the camera. ' 96 'See CameraITS.pdf section 2.3.4 for more details', 97 'scene6': 'A grid of black circles on a white background. ' 98 'See tests/scene6/scene6.png', 99 'sensor_fusion': 'A checkerboard pattern for phone to rotate in front of ' 100 'in tests/sensor_fusion/checkerboard.pdf\n' 101 'See tests/sensor_fusion/SensorFusion.pdf for detailed ' 102 'instructions.\nNote that this test will be skipped ' 103 'on devices not supporting REALTIME camera timestamp.', 104 'scene_change': 'The picture with 3 faces in tests/scene2_e/scene2_e.png', 105} 106 107 108SUB_CAMERA_TESTS = { 109 'scene0': [ 110 'test_burst_capture', 111 'test_jitter', 112 'test_metadata', 113 'test_read_write', 114 'test_sensor_events', 115 'test_solid_color_test_pattern', 116 'test_unified_timestamps', 117 ], 118 'scene1_1': [ 119 'test_burst_sameness_manual', 120 'test_dng_noise_model', 121 'test_exposure', 122 'test_linearity', 123 ], 124 'scene1_2': [ 125 'test_raw_exposure', 126 'test_raw_sensitivity', 127 'test_yuv_plus_raw', 128 ], 129 'scene2_a': [ 130 'test_faces', 131 'test_num_faces', 132 ], 133 'scene4': [ 134 'test_aspect_ratio_and_crop', 135 ], 136 'sensor_fusion': [ 137 'test_sensor_fusion', 138 ], 139} 140 141_DST_SCENE_DIR = '/mnt/sdcard/Download/' 142MOBLY_TEST_SUMMARY_TXT_FILE = 'test_mobly_summary.txt' 143 144 145def run(cmd): 146 """Replaces os.system call, while hiding stdout+stderr messages.""" 147 with open(os.devnull, 'wb') as devnull: 148 subprocess.check_call(cmd.split(), stdout=devnull, stderr=subprocess.STDOUT) 149 150 151def report_result(device_id, camera_id, results): 152 """Sends a pass/fail result to the device, via an intent. 153 154 Args: 155 device_id: The ID string of the device to report the results to. 156 camera_id: The ID string of the camera for which to report pass/fail. 157 results: a dictionary contains all ITS scenes as key and result/summary of 158 current ITS run. See test_report_result unit test for an example. 159 """ 160 adb = f'adb -s {device_id}' 161 162 # Start ItsTestActivity to receive test results 163 cmd = f'{adb} shell am start {ITS_TEST_ACTIVITY} --activity-brought-to-front' 164 run(cmd) 165 time.sleep(ACTIVITY_START_WAIT) 166 167 # Validate/process results argument 168 for scene in results: 169 if RESULT_KEY not in results[scene]: 170 raise ValueError(f'ITS result not found for {scene}') 171 if results[scene][RESULT_KEY] not in RESULT_VALUES: 172 raise ValueError(f'Unknown ITS result for {scene}: {results[RESULT_KEY]}') 173 if SUMMARY_KEY in results[scene]: 174 device_summary_path = f'/sdcard/its_camera{camera_id}_{scene}.txt' 175 run('%s push %s %s' % 176 (adb, results[scene][SUMMARY_KEY], device_summary_path)) 177 results[scene][SUMMARY_KEY] = device_summary_path 178 179 json_results = json.dumps(results) 180 cmd = (f"{adb} shell am broadcast -a {ACTION_ITS_RESULT} --es {EXTRA_VERSION}" 181 f" {CURRENT_ITS_VERSION} --es {EXTRA_CAMERA_ID} {camera_id} --es " 182 f"{EXTRA_RESULTS} \'{json_results}\'") 183 if len(cmd) > 8000: 184 logging.info('ITS command string might be too long! len:%s', len(cmd)) 185 run(cmd) 186 187 188def load_scenes_on_tablet(scene, tablet_id): 189 """Copies scenes onto the tablet before running the tests. 190 191 Args: 192 scene: Name of the scene to copy image files. 193 tablet_id: adb id of tablet 194 """ 195 logging.info('Copying files to tablet: %s', tablet_id) 196 scene_dir = os.listdir( 197 os.path.join(os.environ['CAMERA_ITS_TOP'], 'tests', scene)) 198 for file_name in scene_dir: 199 if file_name.endswith('.png'): 200 src_scene_file = os.path.join(os.environ['CAMERA_ITS_TOP'], 'tests', 201 scene, file_name) 202 cmd = f'adb -s {tablet_id} push {src_scene_file} {_DST_SCENE_DIR}' 203 subprocess.Popen(cmd.split()) 204 time.sleep(LOAD_SCENE_DELAY) 205 logging.info('Finished copying files to tablet.') 206 207 208def check_manual_scenes(device_id, camera_id, scene, out_path): 209 """Halt run to change scenes. 210 211 Args: 212 device_id: id of device 213 camera_id: id of camera 214 scene: Name of the scene to copy image files. 215 out_path: output file location 216 """ 217 with its_session_utils.ItsSession( 218 device_id=device_id, 219 camera_id=camera_id) as cam: 220 props = cam.get_camera_properties() 221 props = cam.override_with_hidden_physical_camera_props(props) 222 223 while True: 224 input(f'\n Press <ENTER> after positioning camera {camera_id} with ' 225 f'{scene}.\n The scene setup should be: \n {_SCENE_REQ[scene]}\n') 226 # Converge 3A prior to capture 227 if scene == 'scene5': 228 cam.do_3a(do_af=False, lock_ae=camera_properties_utils.ae_lock(props), 229 lock_awb=camera_properties_utils.awb_lock(props)) 230 else: 231 cam.do_3a() 232 req, fmt = capture_request_utils.get_fastest_auto_capture_settings(props) 233 logging.info('Capturing an image to check the test scene') 234 cap = cam.do_capture(req, fmt) 235 img = image_processing_utils.convert_capture_to_rgb_image(cap) 236 img_name = os.path.join(out_path, f'test_{scene}.jpg') 237 logging.info('Please check scene setup in %s', img_name) 238 image_processing_utils.write_image(img, img_name) 239 choice = input('Is the image okay for ITS {scene}? (Y/N)').lower() 240 if choice == 'y': 241 break 242 243 244def get_config_file_contents(): 245 """Read the config file contents from a YML file. 246 247 Args: 248 None 249 250 Returns: 251 config_file_contents: a dict read from config.yml 252 """ 253 with open(CONFIG_FILE) as file: 254 config_file_contents = yaml.load(file, yaml.FullLoader) 255 return config_file_contents 256 257 258def get_test_params(config_file_contents): 259 """Reads the config file parameters. 260 261 Args: 262 config_file_contents: dict read from config.yml file 263 264 Returns: 265 dict of test parameters 266 """ 267 test_params = None 268 for _, j in config_file_contents.items(): 269 for datadict in j: 270 test_params = datadict.get('TestParams') 271 return test_params 272 273 274def get_device_serial_number(device, config_file_contents): 275 """Returns the serial number of the device with label from the config file. 276 277 The config file contains TestBeds dictionary which contains Controllers and 278 Android Device dicts.The two devices used by the test per box are listed 279 here labels dut and tablet. Parse through the nested TestBeds dict to get 280 the Android device details. 281 282 Args: 283 device: String device label as specified in config file.dut/tablet 284 config_file_contents: dict read from config.yml file 285 """ 286 287 for _, j in config_file_contents.items(): 288 for datadict in j: 289 android_device_contents = datadict.get('Controllers') 290 for device_dict in android_device_contents.get('AndroidDevice'): 291 for _, label in device_dict.items(): 292 if label == 'tablet': 293 tablet_device_id = device_dict.get('serial') 294 if label == 'dut': 295 dut_device_id = device_dict.get('serial') 296 if device == 'tablet': 297 return tablet_device_id 298 else: 299 return dut_device_id 300 301 302def get_updated_yml_file(yml_file_contents): 303 """Create a new yml file and write the testbed contents in it. 304 305 This testbed file is per box and contains all the parameters and 306 device id used by the mobly tests. 307 308 Args: 309 yml_file_contents: Data to write in yml file. 310 311 Returns: 312 Updated yml file contents. 313 """ 314 os.chmod(YAML_FILE_DIR, 0o755) 315 _, new_yaml_file = tempfile.mkstemp( 316 suffix='.yml', prefix='config_', dir=YAML_FILE_DIR) 317 with open(new_yaml_file, 'w') as f: 318 yaml.dump(yml_file_contents, stream=f, default_flow_style=False) 319 new_yaml_file_name = os.path.basename(new_yaml_file) 320 return new_yaml_file_name 321 322 323def enable_external_storage(device_id): 324 """Override apk mode to allow write to external storage. 325 326 Args: 327 device_id: Serial number of the device. 328 329 """ 330 cmd = (f'adb -s {device_id} shell appops ' 331 'set com.android.cts.verifier MANAGE_EXTERNAL_STORAGE allow') 332 run(cmd) 333 334 335def main(): 336 """Run all the Camera ITS automated tests. 337 338 Script should be run from the top-level CameraITS directory. 339 340 Command line arguments: 341 camera: the camera(s) to be tested. Use comma to separate multiple 342 camera Ids. Ex: "camera=0,1" or "camera=1" 343 scenes: the test scene(s) to be executed. Use comma to separate 344 multiple scenes. Ex: "scenes=scene0,scene1_1" or 345 "scenes=0,1_1,sensor_fusion" (sceneX can be abbreviated by X 346 where X is scene name minus 'scene') 347 """ 348 logging.basicConfig(level=logging.INFO) 349 # Make output directories to hold the generated files. 350 topdir = tempfile.mkdtemp(prefix='CameraITS_') 351 subprocess.call(['chmod', 'g+rx', topdir]) 352 logging.info('Saving output files to: %s', topdir) 353 354 scenes = [] 355 camera_id_combos = [] 356 # Override camera & scenes with cmd line values if available 357 for s in list(sys.argv[1:]): 358 if 'scenes=' in s: 359 scenes = s.split('=')[1].split(',') 360 elif 'camera=' in s: 361 camera_id_combos = s.split('=')[1].split(',') 362 363 # Read config file and extract relevant TestBed 364 config_file_contents = get_config_file_contents() 365 for i in config_file_contents['TestBeds']: 366 if scenes == ['sensor_fusion']: 367 if TEST_KEY_SENSOR_FUSION not in i['Name'].lower(): 368 config_file_contents['TestBeds'].remove(i) 369 else: 370 if TEST_KEY_SENSOR_FUSION in i['Name'].lower(): 371 config_file_contents['TestBeds'].remove(i) 372 373 # Get test parameters from config file 374 test_params_content = get_test_params(config_file_contents) 375 if not camera_id_combos: 376 camera_id_combos = str(test_params_content['camera']).split(',') 377 if not scenes: 378 scenes = str(test_params_content['scene']).split(',') 379 scenes = [_INT_STR_DICT.get(n, n) for n in scenes] # recover '1_1' & '1_2' 380 381 device_id = get_device_serial_number('dut', config_file_contents) 382 # Enable external storage on DUT to send summary report to CtsVerifier.apk 383 enable_external_storage(device_id) 384 385 config_file_test_key = config_file_contents['TestBeds'][0]['Name'].lower() 386 if TEST_KEY_TABLET in config_file_test_key: 387 tablet_id = get_device_serial_number('tablet', config_file_contents) 388 else: 389 tablet_id = None 390 391 testing_sensor_fusion_with_controller = False 392 if TEST_KEY_SENSOR_FUSION in config_file_test_key: 393 if test_params_content['rotator_cntl'].lower() in VALID_CONTROLLERS: 394 testing_sensor_fusion_with_controller = True 395 396 # Prepend 'scene' if not specified at cmd line 397 for i, s in enumerate(scenes): 398 if (not s.startswith('scene') and 399 not s.startswith(('sensor_fusion', '<scene-name>'))): 400 scenes[i] = f'scene{s}' 401 402 # Expand GROUPED_SCENES and remove any duplicates 403 scenes = [_GROUPED_SCENES[s] if s in _GROUPED_SCENES else s for s in scenes] 404 scenes = np.hstack(scenes).tolist() 405 scenes = sorted(set(scenes), key=scenes.index) 406 407 logging.info('Running ITS on device: %s, camera(s): %s, scene(s): %s', 408 device_id, camera_id_combos, scenes) 409 410 # Determine if manual run 411 if tablet_id is not None and not set(scenes).intersection(_MANUAL_SCENES): 412 auto_scene_switch = True 413 else: 414 auto_scene_switch = False 415 logging.info('Manual testing: no tablet defined or testing scene5.') 416 417 for camera_id in camera_id_combos: 418 test_params_content['camera'] = camera_id 419 results = {} 420 421 # Run through all scenes if user does not supply one and config file doesn't 422 # have specific scene name listed. 423 if its_session_utils.SUB_CAMERA_SEPARATOR in camera_id: 424 possible_scenes = list(SUB_CAMERA_TESTS.keys()) 425 if auto_scene_switch: 426 possible_scenes.remove('sensor_fusion') 427 else: 428 possible_scenes = _AUTO_SCENES if auto_scene_switch else _ALL_SCENES 429 430 if '<scene-name>' in scenes: 431 per_camera_scenes = possible_scenes 432 else: 433 # Validate user input scene names 434 per_camera_scenes = [] 435 for s in scenes: 436 if s in possible_scenes: 437 per_camera_scenes.append(s) 438 if not per_camera_scenes: 439 raise ValueError('No valid scene specified for this camera.') 440 441 logging.info('camera: %s, scene(s): %s', camera_id, per_camera_scenes) 442 for s in _ALL_SCENES: 443 results[s] = {RESULT_KEY: RESULT_NOT_EXECUTED} 444 # A subdir in topdir will be created for each camera_id. All scene test 445 # output logs for each camera id will be stored in this subdir. 446 # This output log path is a mobly param : LogPath 447 cam_id_string = 'cam_id_%s' % ( 448 camera_id.replace(its_session_utils.SUB_CAMERA_SEPARATOR, '_')) 449 mobly_output_logs_path = os.path.join(topdir, cam_id_string) 450 os.mkdir(mobly_output_logs_path) 451 tot_pass = 0 452 for s in per_camera_scenes: 453 test_params_content['scene'] = s 454 results[s]['TEST_STATUS'] = [] 455 456 # unit is millisecond for execution time record in CtsVerifier 457 scene_start_time = int(round(time.time() * 1000)) 458 scene_test_summary = f'Cam{camera_id} {s}' + '\n' 459 mobly_scene_output_logs_path = os.path.join(mobly_output_logs_path, s) 460 461 if auto_scene_switch: 462 # Copy scene images onto the tablet 463 if s not in ['scene0']: 464 load_scenes_on_tablet(s, tablet_id) 465 else: 466 # Check manual scenes for correctness 467 if s not in ['scene0'] and not testing_sensor_fusion_with_controller: 468 check_manual_scenes(device_id, camera_id, s, mobly_output_logs_path) 469 470 scene_test_list = [] 471 config_file_contents['TestBeds'][0]['TestParams'] = test_params_content 472 # Add the MoblyParams to config.yml file with the path to store camera_id 473 # test results. This is a separate dict other than TestBeds. 474 mobly_params_dict = { 475 'MoblyParams': { 476 'LogPath': mobly_scene_output_logs_path 477 } 478 } 479 config_file_contents.update(mobly_params_dict) 480 logging.debug('Final config file contents: %s', config_file_contents) 481 new_yml_file_name = get_updated_yml_file(config_file_contents) 482 logging.info('Using %s as temporary config yml file', new_yml_file_name) 483 if camera_id.rfind(its_session_utils.SUB_CAMERA_SEPARATOR) == -1: 484 scene_dir = os.listdir( 485 os.path.join(os.environ['CAMERA_ITS_TOP'], 'tests', s)) 486 for file_name in scene_dir: 487 if file_name.endswith('.py') and 'test' in file_name: 488 scene_test_list.append(file_name) 489 else: # sub-camera 490 if SUB_CAMERA_TESTS.get(s): 491 scene_test_list = [f'{test}.py' for test in SUB_CAMERA_TESTS[s]] 492 else: 493 scene_test_list = [] 494 scene_test_list.sort() 495 496 # Run tests for scene 497 logging.info('Running tests for %s with camera %s', s, camera_id) 498 num_pass = 0 499 num_skip = 0 500 num_not_mandated_fail = 0 501 num_fail = 0 502 for test in scene_test_list: 503 # Handle repeated test 504 if 'tests/' in test: 505 cmd = [ 506 'python3', 507 os.path.join(os.environ['CAMERA_ITS_TOP'], test), '-c', 508 '%s' % new_yml_file_name 509 ] 510 else: 511 cmd = [ 512 'python3', 513 os.path.join(os.environ['CAMERA_ITS_TOP'], 'tests', s, test), 514 '-c', 515 '%s' % new_yml_file_name 516 ] 517 for num_try in range(NUM_TRIES): 518 # pylint: disable=subprocess-run-check 519 with open(MOBLY_TEST_SUMMARY_TXT_FILE, 'w') as fp: 520 output = subprocess.run(cmd, stdout=fp) 521 # pylint: enable=subprocess-run-check 522 523 # Parse mobly logs to determine SKIP, NOT_YET_MANDATED, and 524 # socket FAILs. 525 with open(MOBLY_TEST_SUMMARY_TXT_FILE, 'r') as file: 526 test_code = output.returncode 527 test_failed = False 528 test_skipped = False 529 test_not_yet_mandated = False 530 line = file.read() 531 if 'Test skipped' in line: 532 return_string = 'SKIP ' 533 num_skip += 1 534 test_skipped = True 535 break 536 537 if 'Not yet mandated test' in line: 538 return_string = 'FAIL*' 539 num_not_mandated_fail += 1 540 test_not_yet_mandated = True 541 break 542 543 if test_code == 0 and not test_skipped: 544 return_string = 'PASS ' 545 num_pass += 1 546 break 547 548 if test_code == 1 and not test_not_yet_mandated: 549 return_string = 'FAIL ' 550 if 'Problem with socket' in line and num_try != NUM_TRIES-1: 551 logging.info('Retry %s/%s', s, test) 552 else: 553 num_fail += 1 554 test_failed = True 555 break 556 os.remove(MOBLY_TEST_SUMMARY_TXT_FILE) 557 logging.info('%s %s/%s', return_string, s, test) 558 test_name = test.split('/')[-1].split('.')[0] 559 results[s]['TEST_STATUS'].append({'test':test_name,'status':return_string.strip()}) 560 msg_short = '%s %s' % (return_string, test) 561 scene_test_summary += msg_short + '\n' 562 563 # unit is millisecond for execution time record in CtsVerifier 564 scene_end_time = int(round(time.time() * 1000)) 565 skip_string = '' 566 tot_tests = len(scene_test_list) 567 if num_skip > 0: 568 skipstr = f",{num_skip} test{'s' if num_skip > 1 else ''} skipped" 569 test_result = '%d / %d tests passed (%.1f%%)%s' % ( 570 num_pass + num_not_mandated_fail, len(scene_test_list) - num_skip, 571 100.0 * float(num_pass + num_not_mandated_fail) / 572 (len(scene_test_list) - num_skip) 573 if len(scene_test_list) != num_skip else 100.0, skip_string) 574 logging.info(test_result) 575 if num_not_mandated_fail > 0: 576 logging.info('(*) %s not_yet_mandated tests failed', 577 num_not_mandated_fail) 578 579 tot_pass += num_pass 580 logging.info('scene tests: %s, Total tests passed: %s', tot_tests, 581 tot_pass) 582 if tot_tests > 0: 583 logging.info('%s compatibility score: %.f/100\n', 584 s, 100 * num_pass / tot_tests) 585 scene_test_summary_path = os.path.join(mobly_scene_output_logs_path, 586 'scene_test_summary.txt') 587 with open(scene_test_summary_path, 'w') as f: 588 f.write(scene_test_summary) 589 results[s][RESULT_KEY] = (RESULT_PASS if num_fail == 0 else RESULT_FAIL) 590 results[s][SUMMARY_KEY] = scene_test_summary_path 591 results[s][TIME_KEY_START] = scene_start_time 592 results[s][TIME_KEY_END] = scene_end_time 593 else: 594 logging.info('%s compatibility score: 0/100\n') 595 596 # Delete temporary yml file after scene run. 597 new_yaml_file_path = os.path.join(YAML_FILE_DIR, new_yml_file_name) 598 os.remove(new_yaml_file_path) 599 600 # Log results per camera 601 logging.info('Reporting camera %s ITS results to CtsVerifier', camera_id) 602 report_result(device_id, camera_id, results) 603 604 logging.info('Test execution completed.') 605 606 # Power down tablet 607 cmd = f'adb -s {tablet_id} shell input keyevent KEYCODE_POWER' 608 subprocess.Popen(cmd.split()) 609 610if __name__ == '__main__': 611 main() 612