1#!/usr/bin/env python 2# 3# Copyright (c) 2016, The OpenThread Authors. 4# All rights reserved. 5# 6# Redistribution and use in source and binary forms, with or without 7# modification, are permitted provided that the following conditions are met: 8# 1. Redistributions of source code must retain the above copyright 9# notice, this list of conditions and the following disclaimer. 10# 2. Redistributions in binary form must reproduce the above copyright 11# notice, this list of conditions and the following disclaimer in the 12# documentation and/or other materials provided with the distribution. 13# 3. Neither the name of the copyright holder nor the 14# names of its contributors may be used to endorse or promote products 15# derived from this software without specific prior written permission. 16# 17# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 21# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 22# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 23# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 24# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 25# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27# POSSIBILITY OF SUCH DAMAGE. 28# 29 30import ConfigParser 31import json 32import logging 33import os 34import subprocess 35import re 36import time 37import unittest 38 39from selenium import webdriver 40from selenium.webdriver import ActionChains 41from selenium.webdriver.support.ui import Select 42from selenium.common.exceptions import UnexpectedAlertPresentException 43from selenium.common.exceptions import NoSuchElementException 44from functools import reduce 45 46from autothreadharness import settings 47from autothreadharness.exceptions import FailError, FatalError, GoldenDeviceNotEnoughError 48from autothreadharness.harness_controller import HarnessController 49from autothreadharness.helpers import HistoryHelper 50from autothreadharness.open_thread_controller import OpenThreadController 51from autothreadharness.pdu_controller_factory import PduControllerFactory 52from autothreadharness.rf_shield_controller import get_rf_shield_controller 53 54logger = logging.getLogger(__name__) 55 56THREAD_CHANNEL_MAX = 26 57"""Maximum channel number of thread protocol""" 58 59THREAD_CHANNEL_MIN = 11 60"""Minimum channel number of thread protocol""" 61 62DEFAULT_TIMEOUT = 2700 63"""Timeout for each test case in seconds""" 64 65 66def wait_until(what, times=-1): 67 """Wait until `what` return True 68 69 Args: 70 what (Callable[bool]): Call `wait()` again and again until it returns True 71 times (int): Maximum times of trials before giving up 72 73 Returns: 74 True if success, False if times threshold reached 75 76 """ 77 while times: 78 logger.info('Waiting times left %d', times) 79 try: 80 if what() is True: 81 return True 82 except BaseException: 83 logger.exception('Wait failed') 84 else: 85 logger.warning('Trial[%d] failed', times) 86 times -= 1 87 time.sleep(1) 88 89 return False 90 91 92class HarnessCase(unittest.TestCase): 93 """This is the case class of all automation test cases. 94 95 All test case classes MUST define properties `role`, `case` and `golden_devices_required` 96 """ 97 98 channel = settings.THREAD_CHANNEL 99 """int: Thread channel. 100 101 Thread channel ranges from 11 to 26. 102 """ 103 104 ROLE_LEADER = 1 105 ROLE_ROUTER = 2 106 ROLE_SED = 4 107 ROLE_BORDER = 8 108 ROLE_REED = 16 109 ROLE_ED = 32 110 ROLE_COMMISSIONER = 64 111 ROLE_JOINER = 128 112 ROLE_FED = 512 113 ROLE_MED = 1024 114 115 role = None 116 """int: role id. 117 118 1 119 Leader 120 2 121 Router 122 4 123 Sleepy end device 124 16 125 Router eligible end device 126 32 127 End device 128 64 129 Commissioner 130 128 131 Joiner 132 512 133 Full end device 134 1024 135 Minimal end device 136 """ 137 138 case = None 139 """str: Case id, e.g. '6 5 1'. 140 """ 141 142 golden_devices_required = 0 143 """int: Golden devices needed to finish the test 144 """ 145 146 child_timeout = settings.THREAD_CHILD_TIMEOUT 147 """int: Child timeout in seconds 148 """ 149 150 sed_polling_interval = settings.THREAD_SED_POLLING_INTERVAL 151 """int: SED polling interval in seconds 152 """ 153 154 auto_dut = settings.AUTO_DUT 155 """bool: whether use harness auto dut feature""" 156 157 timeout = hasattr(settings, 'TIMEOUT') and settings.TIMEOUT or DEFAULT_TIMEOUT 158 """number: timeout in seconds to stop running this test case""" 159 160 started = 0 161 """number: test case started timestamp""" 162 163 case_need_shield = False 164 """bool: whether needs RF-box""" 165 166 device_order = [] 167 """list: device drag order in TestHarness TestBed page""" 168 169 def __init__(self, *args, **kwargs): 170 self.dut = None 171 self._browser = None 172 self._hc = None 173 self.result_dir = '%s\\%s' % (settings.OUTPUT_PATH, self.__class__.__name__) 174 self.history = HistoryHelper() 175 self.add_all_devices = False 176 self.new_th = False 177 178 harness_info = ConfigParser.ConfigParser() 179 harness_info.read('%s\\info.ini' % settings.HARNESS_HOME) 180 if harness_info.has_option('Thread_Harness_Info', 'Version') and harness_info.has_option( 181 'Thread_Harness_Info', 'Mode'): 182 harness_version = harness_info.get('Thread_Harness_Info', 'Version').rsplit(' ', 1)[1] 183 harness_mode = harness_info.get('Thread_Harness_Info', 'Mode') 184 185 if harness_mode == 'External' and harness_version > '1.4.0': 186 self.new_th = True 187 188 if harness_mode == 'Internal' and harness_version > '49.4': 189 self.new_th = True 190 191 super(HarnessCase, self).__init__(*args, **kwargs) 192 193 def _init_devices(self): 194 """Reboot all usb devices. 195 196 Note: 197 If PDU_CONTROLLER_TYPE is not valid, usb devices is not rebooted. 198 """ 199 if not settings.PDU_CONTROLLER_TYPE: 200 if settings.AUTO_DUT: 201 return 202 203 for device in settings.GOLDEN_DEVICES: 204 port, _ = device 205 try: 206 with OpenThreadController(port) as otc: 207 logger.info('Resetting %s', port) 208 otc.reset() 209 except BaseException: 210 logger.exception('Failed to reset device %s', port) 211 self.history.mark_bad_golden_device(device) 212 213 return 214 215 tries = 3 216 pdu_factory = PduControllerFactory() 217 218 while True: 219 try: 220 pdu = pdu_factory.create_pdu_controller(settings.PDU_CONTROLLER_TYPE) 221 pdu.open(**settings.PDU_CONTROLLER_OPEN_PARAMS) 222 except EOFError: 223 logger.warning('Failed to connect to telnet') 224 tries = tries - 1 225 if tries: 226 time.sleep(10) 227 continue 228 else: 229 logger.error('Fatal error: cannot connect to apc') 230 raise 231 else: 232 pdu.reboot(**settings.PDU_CONTROLLER_REBOOT_PARAMS) 233 pdu.close() 234 break 235 236 time.sleep(len(settings.GOLDEN_DEVICES)) 237 238 def _init_harness(self): 239 """Restart harness backend service. 240 241 Please start the harness controller before running the cases, otherwise, nothing happens 242 """ 243 self._hc = HarnessController(self.result_dir) 244 self._hc.stop() 245 time.sleep(1) 246 self._hc.start() 247 time.sleep(2) 248 249 harness_config = ConfigParser.ConfigParser() 250 harness_config.read('%s\\Config\\Configuration.ini' % settings.HARNESS_HOME) 251 if harness_config.has_option('THREAD_HARNESS_CONFIG', 'BrowserAutoNavigate') and harness_config.getboolean( 252 'THREAD_HARNESS_CONFIG', 'BrowserAutoNavigate'): 253 logger.error('BrowserAutoNavigate in Configuration.ini should be False') 254 raise FailError('BrowserAutoNavigate in Configuration.ini should be False') 255 if settings.MIXED_DEVICE_TYPE: 256 if harness_config.has_option('THREAD_HARNESS_CONFIG', 257 'EnableDeviceSelection') and not harness_config.getboolean( 258 'THREAD_HARNESS_CONFIG', 'EnableDeviceSelection'): 259 logger.error('EnableDeviceSelection in Configuration.ini should be True') 260 raise FailError('EnableDeviceSelection in Configuration.ini should be True') 261 262 def _destroy_harness(self): 263 """Stop harness backend service 264 265 Stop harness service. 266 """ 267 self._hc.stop() 268 time.sleep(2) 269 270 def _init_dut(self): 271 """Initialize the DUT. 272 273 DUT will be restarted. and openthread will started. 274 """ 275 if self.auto_dut: 276 self.dut = None 277 return 278 279 dut_port = settings.DUT_DEVICE[0] 280 dut = OpenThreadController(dut_port) 281 self.dut = dut 282 283 def _destroy_dut(self): 284 self.dut = None 285 286 def _init_browser(self): 287 """Open harness web page. 288 289 Open a quiet chrome which: 290 1. disables extensions, 291 2. ignore certificate errors and 292 3. always allow notifications. 293 """ 294 try: 295 chrome_options = webdriver.ChromeOptions() 296 chrome_options.add_argument('--disable-extensions') 297 chrome_options.add_argument('--disable-infobars') 298 chrome_options.add_argument('--ignore-certificate-errors') 299 chrome_options.add_experimental_option('prefs', 300 {'profile.managed_default_content_settings.notifications': 1}) 301 302 browser = webdriver.Chrome(chrome_options=chrome_options) 303 browser.set_page_load_timeout(20) 304 browser.implicitly_wait(1) 305 browser.get(settings.HARNESS_URL) 306 browser.maximize_window() 307 self._browser = browser 308 if not wait_until(lambda: 'Thread' in browser.title, 30): 309 self.assertIn('Thread', browser.title) 310 return True 311 except Exception as e: 312 logger.info('Init chrome error: {0}'.format(type(e).__name__)) 313 return False 314 315 def _destroy_browser(self): 316 """Close the browser. 317 """ 318 if self._browser: 319 self._browser.close() 320 self._browser = None 321 322 def _init_rf_shield(self): 323 if getattr(settings, 'SHIELD_CONTROLLER_TYPE', None) and getattr(settings, 'SHIELD_CONTROLLER_PARAMS', None): 324 self.rf_shield = get_rf_shield_controller(shield_type=settings.SHIELD_CONTROLLER_TYPE, 325 params=settings.SHIELD_CONTROLLER_PARAMS) 326 else: 327 self.rf_shield = None 328 329 def _destroy_rf_shield(self): 330 self.rf_shield = None 331 332 def setUp(self): 333 """Prepare to run test case. 334 335 Start harness service, init golden devices, reset DUT and open browser. 336 """ 337 if self.__class__ is HarnessCase: 338 return 339 340 logger.info('Setting up') 341 # clear files 342 343 logger.info('Deleting all .pcapng') 344 os.system('del /q "%s\\Captures\\*.pcapng"' % settings.HARNESS_HOME) 345 logger.info('Empty files in Logs') 346 os.system('del /q "%s\\Logs\\*.*"' % settings.HARNESS_HOME) 347 348 # using temp files to fix excel downloading fail 349 if self.new_th: 350 logger.info('Empty files in Reports') 351 os.system('del /q "%s\\Reports\\*.*"' % settings.HARNESS_HOME) 352 else: 353 logger.info('Empty files in temps') 354 os.system('del /q "%s\\Thread_Harness\\temp\\*.*"' % settings.HARNESS_HOME) 355 356 # create directory 357 os.system('mkdir %s' % self.result_dir) 358 self._init_harness() 359 self._init_devices() 360 self._init_dut() 361 self._init_rf_shield() 362 363 def tearDown(self): 364 """Clean up after each case. 365 366 Stop harness service, close browser and close DUT. 367 """ 368 if self.__class__ is HarnessCase: 369 return 370 371 logger.info('Tearing down') 372 self._destroy_harness() 373 self._destroy_browser() 374 self._destroy_dut() 375 self._destroy_rf_shield() 376 377 def _setup_page(self): 378 """Do sniffer settings and general settings 379 """ 380 if not self.started: 381 self.started = time.time() 382 383 # Detect Sniffer 384 try: 385 dialog = self._browser.find_element_by_id('capture-Setup-modal') 386 except BaseException: 387 logger.exception('Failed to get dialog.') 388 else: 389 if dialog and dialog.get_attribute('aria-hidden') == 'false': 390 times = 100 391 while times: 392 status = dialog.find_element_by_class_name('status-notify').text 393 if 'Searching' in status: 394 logger.info('Still detecting..') 395 elif 'Not' in status: 396 logger.warning('Sniffer device not verified!') 397 button = dialog.find_element_by_id('snifferAutoDetectBtn') 398 button.click() 399 elif 'Verified' in status: 400 logger.info('Verified!') 401 button = dialog.find_element_by_id('saveCaptureSettings') 402 button.click() 403 break 404 else: 405 logger.warning('Unexpected sniffer verification status') 406 407 times = times - 1 408 time.sleep(1) 409 410 if not times: 411 raise Exception('Unable to detect sniffer device') 412 413 time.sleep(1) 414 415 try: 416 skip_button = self._browser.find_element_by_id('SkipPrepareDevice') 417 if skip_button.is_enabled(): 418 skip_button.click() 419 time.sleep(1) 420 except BaseException: 421 logger.info('Still detecting sniffers') 422 423 try: 424 next_button = self._browser.find_element_by_id('nextButton') 425 except BaseException: 426 logger.exception('Failed to finish setup') 427 return 428 429 if not next_button.is_enabled(): 430 logger.info('Harness is still not ready') 431 return 432 433 # General Setup 434 try: 435 if self.child_timeout or self.sed_polling_interval: 436 logger.info('finding general Setup button') 437 button = self._browser.find_element_by_id('general-Setup') 438 button.click() 439 time.sleep(2) 440 441 dialog = self._browser.find_element_by_id('general-Setup-modal') 442 if dialog.get_attribute('aria-hidden') != 'false': 443 raise Exception('Missing General Setup dialog') 444 445 field = dialog.find_element_by_id('inp_general_child_update_wait_time') 446 field.clear() 447 if self.child_timeout: 448 field.send_keys(str(self.child_timeout)) 449 450 field = dialog.find_element_by_id('inp_general_sed_polling_rate') 451 field.clear() 452 if self.sed_polling_interval: 453 field.send_keys(str(self.sed_polling_interval)) 454 455 button = dialog.find_element_by_id('saveGeneralSettings') 456 button.click() 457 time.sleep(1) 458 459 except BaseException: 460 logger.info('general setup exception') 461 logger.exception('Failed to do general setup') 462 return 463 464 # Finish this page 465 next_button.click() 466 time.sleep(1) 467 468 def _connect_devices(self): 469 connect_all = self._browser.find_element_by_link_text('Connect All') 470 connect_all.click() 471 472 def _add_device(self, port, device_type_id): 473 browser = self._browser 474 test_bed = browser.find_element_by_id('test-bed') 475 device = browser.find_element_by_id(device_type_id) 476 # drag 477 action_chains = ActionChains(browser) 478 action_chains.click_and_hold(device) 479 action_chains.move_to_element(test_bed).perform() 480 time.sleep(1) 481 482 # drop 483 drop_hw = browser.find_element_by_class_name('drop-hw') 484 action_chains = ActionChains(browser) 485 action_chains.move_to_element(drop_hw) 486 action_chains.release(drop_hw).perform() 487 488 time.sleep(0.5) 489 selected_hw = browser.find_element_by_class_name('selected-hw') 490 form_inputs = selected_hw.find_elements_by_tag_name('input') 491 form_port = form_inputs[0] 492 form_port.clear() 493 form_port.send_keys(port) 494 495 def _test_bed(self): 496 """Set up the test bed. 497 498 Connect number of golden devices required by each case. 499 """ 500 browser = self._browser 501 test_bed = browser.find_element_by_id('test-bed') 502 time.sleep(3) 503 selected_hw_set = test_bed.find_elements_by_class_name('selected-hw') 504 selected_hw_num = len(selected_hw_set) 505 506 while selected_hw_num: 507 remove_button = selected_hw_set[selected_hw_num - 1].find_element_by_class_name('removeSelectedDevice') 508 remove_button.click() 509 selected_hw_num = selected_hw_num - 1 510 511 devices = [ 512 device for device in settings.GOLDEN_DEVICES if not self.history.is_bad_golden_device(device[0]) and 513 not (settings.DUT_DEVICE and device[0] == settings.DUT_DEVICE[0]) 514 ] 515 logger.info('Available golden devices: %s', json.dumps(devices, indent=2)) 516 517 shield_devices = [ 518 shield_device for shield_device in settings.SHIELD_GOLDEN_DEVICES 519 if not self.history.is_bad_golden_device(shield_device[0]) and 520 not (settings.DUT2_DEVICE and shield_device[0] == settings.DUT2_DEVICE[0]) 521 ] 522 logger.info('Available shield golden devices: %s', json.dumps(shield_devices, indent=2)) 523 golden_devices_required = self.golden_devices_required 524 525 dut_device = () 526 if settings.DUT_DEVICE: 527 dut_device = settings.DUT_DEVICE 528 """check if test case needs to use RF-shield box and its device order in Testbed page 529 Two parameters case_need_shield & device_order should be set in the case script 530 according to the requires: https://openthread.io/certification/test-cases#rf_shielding 531 Example: 532 In case script leader_9_2_9.py: 533 case_need_shield = True 534 device_order = [('Router_2', False), ('Commissioner', True), ('Router_1', False), ('DUT', True)] 535 On the TestBed page of the Test Harness, the device sort order for Leader_9_2_9 536 should be like: 537 Router_2 538 Commissioner 539 Router_1 540 DUT 541 The ('Commissioner', True) and ('DUT', True) indicate Commissioner device and DUT2 device should 542 be in the RF-box and choose from SHIELD_GOLDEN_DEVICES and DUT2_DEVICE. Otherwise ('DUT', False) means 543 DUT device is not in RF-box and use DUT_DEVICE. The other roles devices with False should be selected 544 from GOLDEN_DEVICES. 545 546 In case script med_6_3_2.py: 547 case_need_shield = True 548 device_order = [] # or not defined 549 means no device drag order. DUT2_DEVICE should be applied as DUT and the other golden devices 550 are from GOLDEN_DEVICES. 551 """ 552 if self.case_need_shield: 553 if not settings.DUT2_DEVICE: 554 logger.info('Must set DUT2_DEVICE') 555 raise FailError('DUT2_DEVICE must be set in settings.py') 556 if isinstance(self.device_order, list) and self.device_order: 557 logger.info('case %s devices ordered by %s ', self.case, self.device_order) 558 else: 559 logger.info('case %s uses %s as DUT', self.case, settings.DUT2_DEVICE) 560 561 # for test bed with multi-vendor devices 562 if settings.MIXED_DEVICE_TYPE: 563 topo_file = settings.HARNESS_HOME + "\\Thread_Harness\\TestScripts\\TopologyConfig.txt" 564 try: 565 f_topo = open(topo_file, 'r') 566 except IOError: 567 logger.info('%s can NOT be found', topo_file) 568 raise GoldenDeviceNotEnoughError() 569 topo_mixed_devices = [] 570 try: 571 while True: 572 topo_line = f_topo.readline().strip() 573 if re.match(r'#.*', topo_line): 574 continue 575 match_line = re.match(r'(.*)-(.*)', topo_line, re.M | re.I) 576 if not match_line: 577 continue 578 case_id = match_line.group(1) 579 580 if re.sub(r'\.', ' ', case_id) == self.case: 581 logger.info('Get line by case %s: %s', case_id, topo_line) 582 topo_device_list = re.split(',', match_line.group(2)) 583 for i in range(len(topo_device_list)): 584 topo_device = re.split(':', topo_device_list[i]) 585 topo_mixed_devices.append(tuple(topo_device)) 586 break 587 else: 588 continue 589 except Exception as e: 590 logger.info('Get devices from topology config file error: %s', e) 591 raise GoldenDeviceNotEnoughError() 592 logger.info('Golden devices in topology config file for case %s: %s', case_id, topo_mixed_devices) 593 f_topo.close() 594 golden_device_candidates = [] 595 missing_golden_devices = topo_mixed_devices[:] 596 597 # mapping topology config devices with golden devices by device order 598 if self.case_need_shield and self.device_order: 599 matched_dut = False 600 for device_order_item in self.device_order: 601 matched = False 602 for mixed_device_item in topo_mixed_devices: 603 # mapping device in device_order which needs to be shielded 604 if device_order_item[1]: 605 if 'DUT' in device_order_item[0]: 606 golden_device_candidates.append(settings.DUT2_DEVICE) 607 dut_device = settings.DUT2_DEVICE 608 matched_dut = True 609 matched = True 610 break 611 for device_item in shield_devices: 612 if (device_order_item[0] == mixed_device_item[0] and 613 mixed_device_item[1] == device_item[1]): 614 golden_device_candidates.append(device_item) 615 shield_devices.remove(device_item) 616 matched = True 617 break 618 # mapping device in device_order which does not need to be shielded 619 else: 620 if 'DUT' in device_order_item[0]: 621 golden_device_candidates.append(settings.DUT_DEVICE) 622 matched_dut = True 623 matched = True 624 break 625 for device_item in devices: 626 if (device_order_item[0] == mixed_device_item[0] and 627 mixed_device_item[1] == device_item[1]): 628 golden_device_candidates.append(device_item) 629 devices.remove(device_item) 630 matched = True 631 break 632 if not matched: 633 logger.info('Golden device not enough in : no %s', device_order_item) 634 raise GoldenDeviceNotEnoughError() 635 if not matched_dut: 636 raise FailError('Failed to find DUT in device_order') 637 devices = golden_device_candidates 638 self.add_all_devices = True 639 else: 640 for mixed_device_item in topo_mixed_devices: 641 for device_item in devices: 642 if mixed_device_item[1] == device_item[1]: 643 golden_device_candidates.append(device_item) 644 devices.remove(device_item) 645 missing_golden_devices.remove(mixed_device_item) 646 break 647 logger.info('Golden devices in topology config file mapped in settings : %s', golden_device_candidates) 648 if len(topo_mixed_devices) != len(golden_device_candidates): 649 device_dict = dict() 650 for missing_device in missing_golden_devices: 651 if missing_device[1] in device_dict: 652 device_dict[missing_device[1]] += 1 653 else: 654 device_dict[missing_device[1]] = 1 655 logger.info('Missing Devices: %s', device_dict) 656 raise GoldenDeviceNotEnoughError() 657 else: 658 devices = golden_device_candidates 659 golden_devices_required = len(devices) 660 logger.info('All case-needed golden devices: %s', json.dumps(devices, indent=2)) 661 # for test bed with single vendor devices 662 else: 663 golden_device_candidates = [] 664 if self.case_need_shield and self.device_order: 665 matched_dut = False 666 for device_order_item in self.device_order: 667 matched = False 668 # choose device which needs to be shielded 669 if device_order_item[1]: 670 if 'DUT' in device_order_item[0]: 671 golden_device_candidates.append(settings.DUT2_DEVICE) 672 dut_device = settings.DUT2_DEVICE 673 matched_dut = True 674 matched = True 675 else: 676 for device_item in shield_devices: 677 golden_device_candidates.append(device_item) 678 shield_devices.remove(device_item) 679 matched = True 680 break 681 # choose device which does not need to be shielded 682 else: 683 if 'DUT' in device_order_item[0]: 684 golden_device_candidates.append(settings.DUT_DEVICE) 685 matched_dut = True 686 matched = True 687 else: 688 for device_item in devices: 689 golden_device_candidates.append(device_item) 690 devices.remove(device_item) 691 matched = True 692 break 693 if not matched: 694 logger.info('Golden device not enough in : no %s', device_order_item) 695 raise GoldenDeviceNotEnoughError() 696 if not matched_dut: 697 raise FailError('Failed to find DUT in device_order') 698 devices = golden_device_candidates 699 self.add_all_devices = True 700 701 if self.auto_dut and not settings.DUT_DEVICE: 702 if settings.MIXED_DEVICE_TYPE: 703 logger.info('Must set DUT_DEVICE') 704 raise FailError('DUT_DEVICE must be set for mixed testbed') 705 golden_devices_required += 1 706 707 if len(devices) < golden_devices_required: 708 raise GoldenDeviceNotEnoughError() 709 710 # add golden devices 711 number_of_devices_to_add = len(devices) if self.add_all_devices else golden_devices_required 712 for i in range(number_of_devices_to_add): 713 self._add_device(*devices.pop()) 714 715 # add DUT 716 if self.case_need_shield: 717 if not self.device_order: 718 self._add_device(*settings.DUT2_DEVICE) 719 else: 720 if settings.DUT_DEVICE: 721 self._add_device(*settings.DUT_DEVICE) 722 723 # enable AUTO DUT 724 if self.auto_dut: 725 checkbox_auto_dut = browser.find_element_by_id('EnableAutoDutSelection') 726 if not checkbox_auto_dut.is_selected(): 727 checkbox_auto_dut.click() 728 time.sleep(1) 729 730 if settings.DUT_DEVICE: 731 radio_auto_dut = browser.find_element_by_class_name('AutoDUT_RadBtns') 732 if not radio_auto_dut.is_selected() and not self.device_order: 733 radio_auto_dut.click() 734 735 if self.device_order: 736 selected_hw_set = test_bed.find_elements_by_class_name('selected-hw') 737 for selected_hw in selected_hw_set: 738 form_inputs = selected_hw.find_elements_by_tag_name('input') 739 form_port = form_inputs[0] 740 port = form_port.get_attribute('value').encode('utf8') 741 if port == dut_device[0]: 742 radio_auto_dut = selected_hw.find_element_by_class_name('AutoDUT_RadBtns') 743 if not radio_auto_dut.is_selected(): 744 radio_auto_dut.click() 745 746 while True: 747 try: 748 self._connect_devices() 749 button_next = browser.find_element_by_id('nextBtn') 750 if not wait_until( 751 lambda: 'disabled' not in button_next.get_attribute('class'), 752 times=(30 + 4 * number_of_devices_to_add), 753 ): 754 bad_ones = [] 755 selected_hw_set = test_bed.find_elements_by_class_name('selected-hw') 756 for selected_hw in selected_hw_set: 757 form_inputs = selected_hw.find_elements_by_tag_name('input') 758 form_port = form_inputs[0] 759 if form_port.is_enabled(): 760 bad_ones.append(selected_hw) 761 762 for selected_hw in bad_ones: 763 form_inputs = selected_hw.find_elements_by_tag_name('input') 764 form_port = form_inputs[0] 765 port = form_port.get_attribute('value').encode('utf8') 766 if port == dut_device[0]: 767 if settings.PDU_CONTROLLER_TYPE is None: 768 # connection error cannot recover without power 769 # cycling 770 raise FatalError('Failed to connect to DUT') 771 else: 772 raise FailError('Failed to connect to DUT') 773 774 if settings.PDU_CONTROLLER_TYPE is None: 775 # port cannot recover without power cycling 776 self.history.mark_bad_golden_device(port) 777 778 # remove the bad one 779 selected_hw.find_element_by_class_name('removeSelectedDevice').click() 780 time.sleep(0.1) 781 782 if len(devices): 783 self._add_device(*devices.pop()) 784 else: 785 devices = None 786 787 if devices is None: 788 logger.warning('Golden devices not enough') 789 raise GoldenDeviceNotEnoughError() 790 else: 791 logger.info('Try again with new golden devices') 792 continue 793 794 if self.auto_dut and not settings.DUT_DEVICE: 795 radio_auto_dut = browser.find_element_by_class_name('AutoDUT_RadBtns') 796 if not radio_auto_dut.is_selected(): 797 radio_auto_dut.click() 798 799 time.sleep(5) 800 801 button_next.click() 802 if not wait_until(lambda: self._browser.current_url.endswith('TestExecution.html'), 20): 803 raise Exception('Failed to load TestExecution page') 804 except FailError: 805 raise 806 except BaseException: 807 logger.exception('Unexpected error') 808 else: 809 break 810 811 def _select_case(self, role, case): 812 """Select the test case. 813 """ 814 # select the case 815 elem = Select(self._browser.find_element_by_id('select-dut')) 816 elem.select_by_value(str(role)) 817 time.sleep(1) 818 819 checkbox = None 820 wait_until(lambda: self._browser.find_elements_by_css_selector('.tree-node .tree-title') and True) 821 elems = self._browser.find_elements_by_css_selector('.tree-node .tree-title') 822 finder = re.compile(r'.*\b' + case + r'\b') 823 finder_dotted = re.compile(r'.*\b' + case.replace(' ', r'\.') + r'\b') 824 for elem in elems: 825 # elem.txt might be null when the required reference devices could 826 # not be met (either due to the quantity or the type) for specific test. 827 # perform() will throw exceptions if elem.text is null since Chrome 828 # and chromedriver 80. If elem is not shown in current case list 829 # window, move_to_element() will not work either. 830 if not elem.text: 831 continue 832 # execute a javascript to scroll the window to the elem 833 self._browser.execute_script('arguments[0].scrollIntoView();', elem) 834 action_chains = ActionChains(self._browser) 835 action_chains.move_to_element(elem) 836 action_chains.perform() 837 logger.debug(elem.text) 838 if finder.match(elem.text) or finder_dotted.match(elem.text): 839 parent = elem.find_element_by_xpath('..') 840 checkbox = parent.find_element_by_class_name('tree-checkbox') 841 break 842 843 if not checkbox: 844 time.sleep(5) 845 raise Exception('Failed to find the case') 846 847 self._browser.execute_script("$('.overview').css('left', '0')") 848 checkbox.click() 849 time.sleep(1) 850 851 elem = self._browser.find_element_by_id('runTest') 852 elem.click() 853 if not wait_until(lambda: self._browser.find_element_by_id('stopTest') and True, 10): 854 raise Exception('Failed to start test case') 855 856 def _collect_result(self): 857 """Collect test result. 858 859 Copy PDF and pcap file to result directory 860 """ 861 862 if self.new_th: 863 os.system('copy "%s\\Reports\\*.*" "%s"' % (settings.HARNESS_HOME, self.result_dir)) 864 else: 865 os.system('copy "%s\\Thread_Harness\\temp\\*.*" "%s"' % (settings.HARNESS_HOME, self.result_dir)) 866 867 os.system('copy "%s\\Captures\\*.pcapng" %s\\' % (settings.HARNESS_HOME, self.result_dir)) 868 869 def _wait_dialog(self): 870 """Wait for dialogs and handle them until done. 871 """ 872 logger.debug('waiting for dialog') 873 done = False 874 error = False 875 876 logger.info('self timeout %d', self.timeout) 877 while not done and self.timeout: 878 try: 879 dialog = self._browser.find_element_by_id('RemoteConfirm') 880 except BaseException: 881 logger.exception('Failed to get dialog.') 882 else: 883 if dialog and dialog.get_attribute('aria-hidden') == 'false': 884 title = dialog.find_element_by_class_name('modal-title').text 885 time.sleep(1) 886 logger.info('Handling dialog[%s]', title) 887 888 try: 889 done = self._handle_dialog(dialog, title) 890 except BaseException: 891 logger.exception('Error handling dialog: %s', title) 892 error = True 893 894 if done is None: 895 raise FailError('Unexpected dialog occurred') 896 897 dialog.find_element_by_id('ConfirmOk').click() 898 899 time.sleep(1) 900 901 try: 902 stop_button = self._browser.find_element_by_id('stopTest') 903 if done: 904 stop_button.click() 905 # wait for stop procedure end 906 time.sleep(10) 907 except NoSuchElementException: 908 logger.info('Test stopped') 909 time.sleep(5) 910 done = True 911 912 self.timeout -= 1 913 914 # check if already ended capture 915 if self.timeout % 10 == 0: 916 lines = self._hc.tail() 917 if 'SUCCESS: The process "dumpcap.exe" with PID ' in lines: 918 logger.info('Tshark should be ended now, lets wait at most 30 seconds.') 919 if not wait_until(lambda: 'tshark.exe' not in subprocess.check_output('tasklist'), 30): 920 res = subprocess.check_output('taskkill /t /f /im tshark.exe', 921 stderr=subprocess.STDOUT, 922 shell=True) 923 logger.info(res) 924 925 # Wait until case really stopped 926 wait_until(lambda: self._browser.find_element_by_id('runTest') and True, 30) 927 928 if error: 929 raise FailError('Fail for previous exceptions') 930 931 def _handle_dialog(self, dialog, title): 932 """Handle a dialog. 933 934 Returns: 935 bool True if no more dialogs expected, 936 False if more dialogs needed, and 937 None if not handled 938 """ 939 done = self.on_dialog(dialog, title) 940 if isinstance(done, bool): 941 return done 942 943 if title.startswith('Start DUT'): 944 body = dialog.find_element_by_id('cnfrmMsg').text 945 if 'Sleepy End Device' in body: 946 self.dut.mode = 's' 947 self.dut.child_timeout = self.child_timeout 948 elif 'End Device' in body: 949 self.dut.mode = 'rn' 950 self.dut.child_timeout = self.child_timeout 951 else: 952 self.dut.mode = 'rdn' 953 954 if 'at channel' in body: 955 self.channel = int(body.split(':')[1]) 956 957 self.dut.channel = self.channel 958 self.dut.panid = settings.THREAD_PANID 959 self.dut.networkname = settings.THREAD_NETWORKNAME 960 self.dut.extpanid = settings.THREAD_EXTPANID 961 self.dut.start() 962 963 elif title.startswith('MAC Address Required') or title.startswith('DUT Random Extended MAC Address Required'): 964 mac = self.dut.mac 965 inp = dialog.find_element_by_id('cnfrmInpText') 966 inp.clear() 967 inp.send_keys('0x%s' % mac) 968 969 elif title.startswith('LL64 Address'): 970 ll64 = None 971 for addr in self.dut.addrs: 972 addr = addr.lower() 973 if addr.startswith('fe80') and not re.match('.+ff:fe00:[0-9a-f]{0,4}$', addr): 974 ll64 = addr 975 break 976 977 if not ll64: 978 raise FailError('No link local address found') 979 980 logger.info('Link local address is %s', ll64) 981 inp = dialog.find_element_by_id('cnfrmInpText') 982 inp.clear() 983 inp.send_keys(ll64) 984 985 elif title.startswith('Enter Channel'): 986 self.dut.channel = self.channel 987 inp = dialog.find_element_by_id('cnfrmInpText') 988 inp.clear() 989 inp.send_keys(str(self.dut.channel)) 990 991 elif title.startswith('User Action Needed'): 992 body = dialog.find_element_by_id('cnfrmMsg').text 993 if body.startswith('Power Down the DUT'): 994 self.dut.stop() 995 return True 996 997 elif title.startswith('Short Address'): 998 short_addr = '0x%s' % self.dut.short_addr 999 inp = dialog.find_element_by_id('cnfrmInpText') 1000 inp.clear() 1001 inp.send_keys(short_addr) 1002 1003 elif title.startswith('ML64 Address'): 1004 ml64 = None 1005 for addr in self.dut.addrs: 1006 if addr.startswith('fd') and not re.match('.+ff:fe00:[0-9a-f]{0,4}$', addr): 1007 ml64 = addr 1008 break 1009 1010 if not ml64: 1011 raise Exception('No mesh local address found') 1012 1013 logger.info('Mesh local address is %s', ml64) 1014 inp = dialog.find_element_by_id('cnfrmInpText') 1015 inp.clear() 1016 inp.send_keys(ml64) 1017 1018 elif title.startswith('Shield Devices') or title.startswith('Shield DUT'): 1019 time.sleep(2) 1020 if self.rf_shield: 1021 logger.info('Shielding devices') 1022 with self.rf_shield: 1023 self.rf_shield.shield() 1024 elif self.dut and settings.SHIELD_SIMULATION: 1025 self.dut.channel = (self.channel == THREAD_CHANNEL_MAX and THREAD_CHANNEL_MIN) or (self.channel + 1) 1026 else: 1027 input('Shield DUT and press enter to continue..') 1028 1029 elif title.startswith('Unshield Devices') or title.startswith('Bring DUT back to network'): 1030 time.sleep(5) 1031 if self.rf_shield: 1032 logger.info('Unshielding devices') 1033 with self.rf_shield: 1034 self.rf_shield.unshield() 1035 elif self.dut and settings.SHIELD_SIMULATION: 1036 self.dut.channel = self.channel 1037 else: 1038 input('Bring DUT and press enter to continue..') 1039 1040 elif title.startswith('Configure Prefix on DUT'): 1041 body = dialog.find_element_by_id('cnfrmMsg').text 1042 body = body.split(': ')[1] 1043 params = reduce( 1044 lambda params, param: params.update(((param[0].strip(' '), param[1]),)) or params, 1045 [it.split('=') for it in body.split(', ')], 1046 {}, 1047 ) 1048 prefix = params['P_Prefix'].strip('\0\r\n\t ') 1049 flags = [] 1050 if params.get('P_slaac_preferred', 0) == '1': 1051 flags.append('p') 1052 flags.append('ao') 1053 if params.get('P_stable', 0) == '1': 1054 flags.append('s') 1055 if params.get('P_default', 0) == '1': 1056 flags.append('r') 1057 prf = 'high' 1058 self.dut.add_prefix(prefix, ''.join(flags), prf) 1059 1060 return False 1061 1062 def test(self): 1063 """This method will only start test case in child class""" 1064 if self.__class__ is HarnessCase: 1065 logger.warning('Skip this harness itself') 1066 return 1067 1068 logger.info('Testing role[%d] case[%s]', self.role, self.case) 1069 1070 init_browser_times = 5 1071 while True: 1072 if self._init_browser(): 1073 break 1074 elif init_browser_times > 0: 1075 init_browser_times -= 1 1076 self._destroy_browser() 1077 else: 1078 raise SystemExit() 1079 1080 try: 1081 # prepare test case 1082 while True: 1083 url = self._browser.current_url 1084 if url.endswith('SetupPage.html'): 1085 self._setup_page() 1086 elif url.endswith('TestBed.html'): 1087 self._test_bed() 1088 elif url.endswith('TestExecution.html'): 1089 logger.info('Ready to handle dialogs') 1090 break 1091 time.sleep(2) 1092 except UnexpectedAlertPresentException: 1093 logger.exception('Failed to connect to harness server') 1094 raise SystemExit() 1095 except FatalError: 1096 logger.exception('Test stopped for fatal error') 1097 raise SystemExit() 1098 except FailError: 1099 logger.exception('Test failed') 1100 raise 1101 except BaseException: 1102 logger.exception('Something wrong') 1103 1104 self._select_case(self.role, self.case) 1105 1106 logger.info('start to wait test process end') 1107 self._wait_dialog() 1108 1109 try: 1110 self._collect_result() 1111 except BaseException: 1112 logger.exception('Failed to collect results') 1113 raise 1114 1115 # get case result 1116 status = self._browser.find_element_by_class_name('title-test').get_attribute('innerText') 1117 logger.info(status) 1118 success = 'Pass' in status 1119 self.assertTrue(success) 1120