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