1#!/usr/bin/env python3
2#
3#   Copyright 2017 - Google
4#
5#   Licensed under the Apache License, Version 2.0 (the "License");
6#   you may not use this file except in compliance with the License.
7#   You may obtain a copy of the License at
8#
9#       http://www.apache.org/licenses/LICENSE-2.0
10#
11#   Unless required by applicable law or agreed to in writing, software
12#   distributed under the License is distributed on an "AS IS" BASIS,
13#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14#   See the License for the specific language governing permissions and
15#   limitations under the License.
16"""
17    Base Class for Defining Common WiFi Test Functionality
18"""
19
20import copy
21import itertools
22import time
23
24import acts.controllers.access_point as ap
25
26from acts import asserts
27from acts import signals
28from acts import utils
29from acts.base_test import BaseTestClass
30from acts.signals import TestSignal
31from acts.controllers import android_device
32from acts.controllers.access_point import AccessPoint
33from acts.controllers.ap_lib import hostapd_ap_preset
34from acts.controllers.ap_lib import hostapd_bss_settings
35from acts.controllers.ap_lib import hostapd_constants
36from acts.controllers.ap_lib import hostapd_security
37
38AP_1 = 0
39AP_2 = 1
40MAX_AP_COUNT = 2
41
42
43class WifiBaseTest(BaseTestClass):
44    def setup_class(self):
45        if hasattr(self, 'attenuators') and self.attenuators:
46            for attenuator in self.attenuators:
47                attenuator.set_atten(0)
48
49    def get_psk_network(
50            self,
51            mirror_ap,
52            reference_networks,
53            hidden=False,
54            same_ssid=False,
55            security_mode=hostapd_constants.WPA2_STRING,
56            ssid_length_2g=hostapd_constants.AP_SSID_LENGTH_2G,
57            ssid_length_5g=hostapd_constants.AP_SSID_LENGTH_5G,
58            passphrase_length_2g=hostapd_constants.AP_PASSPHRASE_LENGTH_2G,
59            passphrase_length_5g=hostapd_constants.AP_PASSPHRASE_LENGTH_5G):
60        """Generates SSID and passphrase for a WPA2 network using random
61           generator.
62
63           Args:
64               mirror_ap: Boolean, determines if both APs use the same hostapd
65                          config or different configs.
66               reference_networks: List of PSK networks.
67               same_ssid: Boolean, determines if both bands on AP use the same
68                          SSID.
69               ssid_length_2gecond AP Int, number of characters to use for 2G SSID.
70               ssid_length_5g: Int, number of characters to use for 5G SSID.
71               passphrase_length_2g: Int, length of password for 2G network.
72               passphrase_length_5g: Int, length of password for 5G network.
73
74           Returns: A dict of 2G and 5G network lists for hostapd configuration.
75
76        """
77        network_dict_2g = {}
78        network_dict_5g = {}
79        ref_5g_security = security_mode
80        ref_2g_security = security_mode
81
82        if same_ssid:
83            ref_2g_ssid = 'xg_%s' % utils.rand_ascii_str(ssid_length_2g)
84            ref_5g_ssid = ref_2g_ssid
85
86            ref_2g_passphrase = utils.rand_ascii_str(passphrase_length_2g)
87            ref_5g_passphrase = ref_2g_passphrase
88
89        else:
90            ref_2g_ssid = '2g_%s' % utils.rand_ascii_str(ssid_length_2g)
91            ref_2g_passphrase = utils.rand_ascii_str(passphrase_length_2g)
92
93            ref_5g_ssid = '5g_%s' % utils.rand_ascii_str(ssid_length_5g)
94            ref_5g_passphrase = utils.rand_ascii_str(passphrase_length_5g)
95
96        network_dict_2g = {
97            "SSID": ref_2g_ssid,
98            "security": ref_2g_security,
99            "password": ref_2g_passphrase,
100            "hiddenSSID": hidden
101        }
102
103        network_dict_5g = {
104            "SSID": ref_5g_ssid,
105            "security": ref_5g_security,
106            "password": ref_5g_passphrase,
107            "hiddenSSID": hidden
108        }
109
110        ap = 0
111        for ap in range(MAX_AP_COUNT):
112            reference_networks.append({
113                "2g": copy.copy(network_dict_2g),
114                "5g": copy.copy(network_dict_5g)
115            })
116            if not mirror_ap:
117                break
118        return {"2g": network_dict_2g, "5g": network_dict_5g}
119
120    def get_open_network(self,
121                         mirror_ap,
122                         open_network,
123                         hidden=False,
124                         same_ssid=False,
125                         ssid_length_2g=hostapd_constants.AP_SSID_LENGTH_2G,
126                         ssid_length_5g=hostapd_constants.AP_SSID_LENGTH_5G):
127        """Generates SSIDs for a open network using a random generator.
128
129        Args:
130            mirror_ap: Boolean, determines if both APs use the same hostapd
131                       config or different configs.
132            open_network: List of open networks.
133            same_ssid: Boolean, determines if both bands on AP use the same
134                       SSID.
135            ssid_length_2g: Int, number of characters to use for 2G SSID.
136            ssid_length_5g: Int, number of characters to use for 5G SSID.
137
138        Returns: A dict of 2G and 5G network lists for hostapd configuration.
139
140        """
141        network_dict_2g = {}
142        network_dict_5g = {}
143
144        if same_ssid:
145            open_2g_ssid = 'xg_%s' % utils.rand_ascii_str(ssid_length_2g)
146            open_5g_ssid = open_2g_ssid
147
148        else:
149            open_2g_ssid = '2g_%s' % utils.rand_ascii_str(ssid_length_2g)
150            open_5g_ssid = '5g_%s' % utils.rand_ascii_str(ssid_length_5g)
151
152        network_dict_2g = {
153            "SSID": open_2g_ssid,
154            "security": 'none',
155            "hiddenSSID": hidden
156        }
157
158        network_dict_5g = {
159            "SSID": open_5g_ssid,
160            "security": 'none',
161            "hiddenSSID": hidden
162        }
163
164        ap = 0
165        for ap in range(MAX_AP_COUNT):
166            open_network.append({
167                "2g": copy.copy(network_dict_2g),
168                "5g": copy.copy(network_dict_5g)
169            })
170            if not mirror_ap:
171                break
172        return {"2g": network_dict_2g, "5g": network_dict_5g}
173
174    def get_wep_network(
175            self,
176            mirror_ap,
177            networks,
178            hidden=False,
179            same_ssid=False,
180            ssid_length_2g=hostapd_constants.AP_SSID_LENGTH_2G,
181            ssid_length_5g=hostapd_constants.AP_SSID_LENGTH_5G,
182            passphrase_length_2g=hostapd_constants.AP_PASSPHRASE_LENGTH_2G,
183            passphrase_length_5g=hostapd_constants.AP_PASSPHRASE_LENGTH_5G):
184        """Generates SSID and passphrase for a WEP network using random
185           generator.
186
187           Args:
188               mirror_ap: Boolean, determines if both APs use the same hostapd
189                          config or different configs.
190               networks: List of WEP networks.
191               same_ssid: Boolean, determines if both bands on AP use the same
192                          SSID.
193               ssid_length_2gecond AP Int, number of characters to use for 2G SSID.
194               ssid_length_5g: Int, number of characters to use for 5G SSID.
195               passphrase_length_2g: Int, length of password for 2G network.
196               passphrase_length_5g: Int, length of password for 5G network.
197
198           Returns: A dict of 2G and 5G network lists for hostapd configuration.
199
200        """
201        network_dict_2g = {}
202        network_dict_5g = {}
203        ref_5g_security = hostapd_constants.WEP_STRING
204        ref_2g_security = hostapd_constants.WEP_STRING
205
206        if same_ssid:
207            ref_2g_ssid = 'xg_%s' % utils.rand_ascii_str(ssid_length_2g)
208            ref_5g_ssid = ref_2g_ssid
209
210            ref_2g_passphrase = utils.rand_hex_str(passphrase_length_2g)
211            ref_5g_passphrase = ref_2g_passphrase
212
213        else:
214            ref_2g_ssid = '2g_%s' % utils.rand_ascii_str(ssid_length_2g)
215            ref_2g_passphrase = utils.rand_hex_str(passphrase_length_2g)
216
217            ref_5g_ssid = '5g_%s' % utils.rand_ascii_str(ssid_length_5g)
218            ref_5g_passphrase = utils.rand_hex_str(passphrase_length_5g)
219
220        network_dict_2g = {
221            "SSID": ref_2g_ssid,
222            "security": ref_2g_security,
223            "wepKeys": [ref_2g_passphrase] * 4,
224            "hiddenSSID": hidden
225        }
226
227        network_dict_5g = {
228            "SSID": ref_5g_ssid,
229            "security": ref_5g_security,
230            "wepKeys": [ref_2g_passphrase] * 4,
231            "hiddenSSID": hidden
232        }
233
234        ap = 0
235        for ap in range(MAX_AP_COUNT):
236            networks.append({
237                "2g": copy.copy(network_dict_2g),
238                "5g": copy.copy(network_dict_5g)
239            })
240            if not mirror_ap:
241                break
242        return {"2g": network_dict_2g, "5g": network_dict_5g}
243
244    def update_bssid(self, ap_instance, ap, network, band):
245        """Get bssid and update network dictionary.
246
247        Args:
248            ap_instance: Accesspoint index that was configured.
249            ap: Accesspoint object corresponding to ap_instance.
250            network: Network dictionary.
251            band: Wifi networks' band.
252
253        """
254        bssid = ap.get_bssid_from_ssid(network["SSID"], band)
255
256        if network["security"] == hostapd_constants.WPA2_STRING:
257            # TODO:(bamahadev) Change all occurances of reference_networks
258            # to wpa_networks.
259            self.reference_networks[ap_instance][band]["bssid"] = bssid
260        if network["security"] == hostapd_constants.WPA_STRING:
261            self.wpa_networks[ap_instance][band]["bssid"] = bssid
262        if network["security"] == hostapd_constants.WEP_STRING:
263            self.wep_networks[ap_instance][band]["bssid"] = bssid
264        if network["security"] == hostapd_constants.ENT_STRING:
265            if "bssid" not in self.ent_networks[ap_instance][band]:
266                self.ent_networks[ap_instance][band]["bssid"] = bssid
267            else:
268                self.ent_networks_pwd[ap_instance][band]["bssid"] = bssid
269        if network["security"] == 'none':
270            self.open_network[ap_instance][band]["bssid"] = bssid
271
272    def populate_bssid(self, ap_instance, ap, networks_5g, networks_2g):
273        """Get bssid for a given SSID and add it to the network dictionary.
274
275        Args:
276            ap_instance: Accesspoint index that was configured.
277            ap: Accesspoint object corresponding to ap_instance.
278            networks_5g: List of 5g networks configured on the APs.
279            networks_2g: List of 2g networks configured on the APs.
280
281        """
282
283        if not (networks_5g or networks_2g):
284            return
285
286        for network in networks_5g:
287            if 'channel' in network:
288                continue
289            self.update_bssid(ap_instance, ap, network,
290                hostapd_constants.BAND_5G)
291
292        for network in networks_2g:
293            if 'channel' in network:
294                continue
295            self.update_bssid(ap_instance, ap, network,
296                hostapd_constants.BAND_2G)
297
298    def legacy_configure_ap_and_start(
299            self,
300            channel_5g=hostapd_constants.AP_DEFAULT_CHANNEL_5G,
301            channel_2g=hostapd_constants.AP_DEFAULT_CHANNEL_2G,
302            max_2g_networks=hostapd_constants.AP_DEFAULT_MAX_SSIDS_2G,
303            max_5g_networks=hostapd_constants.AP_DEFAULT_MAX_SSIDS_5G,
304            ap_ssid_length_2g=hostapd_constants.AP_SSID_LENGTH_2G,
305            ap_passphrase_length_2g=hostapd_constants.AP_PASSPHRASE_LENGTH_2G,
306            ap_ssid_length_5g=hostapd_constants.AP_SSID_LENGTH_5G,
307            ap_passphrase_length_5g=hostapd_constants.AP_PASSPHRASE_LENGTH_5G,
308            hidden=False,
309            same_ssid=False,
310            mirror_ap=True,
311            wpa_network=False,
312            wep_network=False,
313            ent_network=False,
314            radius_conf_2g=None,
315            radius_conf_5g=None,
316            ent_network_pwd=False,
317            radius_conf_pwd=None,
318            ap_count=1):
319        asserts.assert_true(
320            len(self.user_params["AccessPoint"]) == 2,
321            "Exactly two access points must be specified. \
322             Each access point has 2 radios, one each for 2.4GHZ \
323             and 5GHz. A test can choose to use one or both APs.")
324
325        config_count = 1
326        count = 0
327
328        # For example, the NetworkSelector tests use 2 APs and require that
329        # both APs are not mirrored.
330        if not mirror_ap and ap_count == 1:
331             raise ValueError("ap_count cannot be 1 if mirror_ap is False.")
332
333        if not mirror_ap:
334            config_count = ap_count
335
336        self.user_params["reference_networks"] = []
337        self.user_params["open_network"] = []
338        if wpa_network:
339            self.user_params["wpa_networks"] = []
340        if wep_network:
341            self.user_params["wep_networks"] = []
342        if ent_network:
343            self.user_params["ent_networks"] = []
344        if ent_network_pwd:
345            self.user_params["ent_networks_pwd"] = []
346
347        # kill hostapd & dhcpd if the cleanup was not successful
348        for i in range(len(self.access_points)):
349            self.log.debug("Check ap state and cleanup")
350            self._cleanup_hostapd_and_dhcpd(i)
351
352        for count in range(config_count):
353
354            network_list_2g = []
355            network_list_5g = []
356
357            orig_network_list_2g = []
358            orig_network_list_5g = []
359
360            network_list_2g.append({"channel": channel_2g})
361            network_list_5g.append({"channel": channel_5g})
362
363            networks_dict = self.get_psk_network(
364                                mirror_ap,
365                                self.user_params["reference_networks"],
366                                hidden=hidden,
367                                same_ssid=same_ssid)
368            self.reference_networks = self.user_params["reference_networks"]
369
370            network_list_2g.append(networks_dict["2g"])
371            network_list_5g.append(networks_dict["5g"])
372
373            # When same_ssid is set, only configure one set of WPA networks.
374            # We cannot have more than one set because duplicate interface names
375            # are not allowed.
376            # TODO(bmahadev): Provide option to select the type of network,
377            # instead of defaulting to WPA.
378            if not same_ssid:
379                networks_dict = self.get_open_network(
380                                    mirror_ap,
381                                    self.user_params["open_network"],
382                                    hidden=hidden,
383                                    same_ssid=same_ssid)
384                self.open_network = self.user_params["open_network"]
385
386                network_list_2g.append(networks_dict["2g"])
387                network_list_5g.append(networks_dict["5g"])
388
389                if wpa_network:
390                    networks_dict = self.get_psk_network(
391                                        mirror_ap,
392                                        self.user_params["wpa_networks"],
393                                        hidden=hidden,
394                                        same_ssid=same_ssid,
395                                        security_mode=hostapd_constants.WPA_STRING)
396                    self.wpa_networks = self.user_params["wpa_networks"]
397
398                    network_list_2g.append(networks_dict["2g"])
399                    network_list_5g.append(networks_dict["5g"])
400
401                if wep_network:
402                    networks_dict = self.get_wep_network(
403                                        mirror_ap,
404                                        self.user_params["wep_networks"],
405                                        hidden=hidden,
406                                        same_ssid=same_ssid)
407                    self.wep_networks = self.user_params["wep_networks"]
408
409                    network_list_2g.append(networks_dict["2g"])
410                    network_list_5g.append(networks_dict["5g"])
411
412                if ent_network:
413                    networks_dict = self.get_open_network(
414                                        mirror_ap,
415                                        self.user_params["ent_networks"],
416                                        hidden=hidden,
417                                        same_ssid=same_ssid)
418                    networks_dict["2g"]["security"] = hostapd_constants.ENT_STRING
419                    networks_dict["2g"].update(radius_conf_2g)
420                    networks_dict["5g"]["security"] = hostapd_constants.ENT_STRING
421                    networks_dict["5g"].update(radius_conf_5g)
422                    self.ent_networks = self.user_params["ent_networks"]
423
424                    network_list_2g.append(networks_dict["2g"])
425                    network_list_5g.append(networks_dict["5g"])
426
427                if ent_network_pwd:
428                    networks_dict = self.get_open_network(
429                                        mirror_ap,
430                                        self.user_params["ent_networks_pwd"],
431                                        hidden=hidden,
432                                        same_ssid=same_ssid)
433                    networks_dict["2g"]["security"] = hostapd_constants.ENT_STRING
434                    networks_dict["2g"].update(radius_conf_pwd)
435                    networks_dict["5g"]["security"] = hostapd_constants.ENT_STRING
436                    networks_dict["5g"].update(radius_conf_pwd)
437                    self.ent_networks_pwd = self.user_params["ent_networks_pwd"]
438
439                    network_list_2g.append(networks_dict["2g"])
440                    network_list_5g.append(networks_dict["5g"])
441
442            orig_network_list_5g = copy.copy(network_list_5g)
443            orig_network_list_2g = copy.copy(network_list_2g)
444
445            if len(network_list_5g) > 1:
446                self.config_5g = self._generate_legacy_ap_config(network_list_5g)
447            if len(network_list_2g) > 1:
448                self.config_2g = self._generate_legacy_ap_config(network_list_2g)
449
450            self.access_points[count].start_ap(self.config_2g)
451            self.access_points[count].start_ap(self.config_5g)
452            self.populate_bssid(count, self.access_points[count], orig_network_list_5g,
453                                orig_network_list_2g)
454
455        # Repeat configuration on the second router.
456        if mirror_ap and ap_count == 2:
457            self.access_points[AP_2].start_ap(self.config_2g)
458            self.access_points[AP_2].start_ap(self.config_5g)
459            self.populate_bssid(AP_2, self.access_points[AP_2],
460                orig_network_list_5g, orig_network_list_2g)
461
462    def _kill_processes(self, ap, daemon):
463        """ Kill hostapd and dhcpd daemons
464
465        Args:
466            ap: AP to cleanup
467            daemon: process to kill
468
469        Returns: True/False if killing process is successful
470        """
471        self.log.info("Killing %s" % daemon)
472        pids = ap.ssh.run('pidof %s' % daemon, ignore_status=True)
473        if pids.stdout:
474            ap.ssh.run('kill %s' % pids.stdout, ignore_status=True)
475        time.sleep(3)
476        pids = ap.ssh.run('pidof %s' % daemon, ignore_status=True)
477        if pids.stdout:
478            return False
479        return True
480
481    def _cleanup_hostapd_and_dhcpd(self, count):
482        """ Check if AP was cleaned up properly
483
484        Kill hostapd and dhcpd processes if cleanup was not successful in the
485        last run
486
487        Args:
488            count: AP to check
489
490        Returns:
491            New AccessPoint object if AP required cleanup
492
493        Raises:
494            Error: if the AccessPoint timed out to setup
495        """
496        ap = self.access_points[count]
497        phy_ifaces = ap.interfaces.get_physical_interface()
498        kill_hostapd = False
499        for iface in phy_ifaces:
500            if '2g_' in iface or '5g_' in iface or 'xg_' in iface:
501                kill_hostapd = True
502                break
503
504        if not kill_hostapd:
505            return
506
507        self.log.debug("Cleanup AP")
508        if not self._kill_processes(ap, 'hostapd') or \
509            not self._kill_processes(ap, 'dhcpd'):
510              raise("Failed to cleanup AP")
511
512        ap.__init__(self.user_params['AccessPoint'][count])
513
514    def _generate_legacy_ap_config(self, network_list):
515        bss_settings = []
516        wlan_2g = self.access_points[AP_1].wlan_2g
517        wlan_5g = self.access_points[AP_1].wlan_5g
518        ap_settings = network_list.pop(0)
519        # TODO:(bmahadev) This is a bug. We should not have to pop the first
520        # network in the list and treat it as a separate case. Instead,
521        # create_ap_preset() should be able to take NULL ssid and security and
522        # build config based on the bss_Settings alone.
523        hostapd_config_settings = network_list.pop(0)
524        for network in network_list:
525            if "password" in network:
526                bss_settings.append(
527                    hostapd_bss_settings.BssSettings(
528                        name=network["SSID"],
529                        ssid=network["SSID"],
530                        hidden=network["hiddenSSID"],
531                        security=hostapd_security.Security(
532                            security_mode=network["security"],
533                            password=network["password"])))
534            elif "wepKeys" in network:
535                bss_settings.append(
536                    hostapd_bss_settings.BssSettings(
537                        name=network["SSID"],
538                        ssid=network["SSID"],
539                        hidden=network["hiddenSSID"],
540                        security=hostapd_security.Security(
541                            security_mode=network["security"],
542                            password=network["wepKeys"][0])))
543            elif network["security"] == hostapd_constants.ENT_STRING:
544                bss_settings.append(
545                    hostapd_bss_settings.BssSettings(
546                        name=network["SSID"],
547                        ssid=network["SSID"],
548                        hidden=network["hiddenSSID"],
549                        security=hostapd_security.Security(
550                            security_mode=network["security"],
551                            radius_server_ip=network["radius_server_ip"],
552                            radius_server_port=network["radius_server_port"],
553                            radius_server_secret=network["radius_server_secret"])))
554            else:
555                bss_settings.append(
556                    hostapd_bss_settings.BssSettings(
557                        name=network["SSID"],
558                        ssid=network["SSID"],
559                        hidden=network["hiddenSSID"]))
560        if "password" in hostapd_config_settings:
561            config = hostapd_ap_preset.create_ap_preset(
562                iface_wlan_2g=wlan_2g,
563                iface_wlan_5g=wlan_5g,
564                channel=ap_settings["channel"],
565                ssid=hostapd_config_settings["SSID"],
566                hidden=hostapd_config_settings["hiddenSSID"],
567                security=hostapd_security.Security(
568                    security_mode=hostapd_config_settings["security"],
569                    password=hostapd_config_settings["password"]),
570                bss_settings=bss_settings)
571        elif "wepKeys" in hostapd_config_settings:
572            config = hostapd_ap_preset.create_ap_preset(
573                iface_wlan_2g=wlan_2g,
574                iface_wlan_5g=wlan_5g,
575                channel=ap_settings["channel"],
576                ssid=hostapd_config_settings["SSID"],
577                hidden=hostapd_config_settings["hiddenSSID"],
578                security=hostapd_security.Security(
579                    security_mode=hostapd_config_settings["security"],
580                    password=hostapd_config_settings["wepKeys"][0]),
581                bss_settings=bss_settings)
582        else:
583            config = hostapd_ap_preset.create_ap_preset(
584                iface_wlan_2g=wlan_2g,
585                iface_wlan_5g=wlan_5g,
586                channel=ap_settings["channel"],
587                ssid=hostapd_config_settings["SSID"],
588                hidden=hostapd_config_settings["hiddenSSID"],
589                bss_settings=bss_settings)
590        return config
591
592    def configure_packet_capture(
593            self,
594            channel_5g=hostapd_constants.AP_DEFAULT_CHANNEL_5G,
595            channel_2g=hostapd_constants.AP_DEFAULT_CHANNEL_2G):
596        """Configure packet capture for 2G and 5G bands.
597
598        Args:
599            channel_5g: Channel to set the monitor mode to for 5G band.
600            channel_2g: Channel to set the monitor mode to for 2G band.
601        """
602        self.packet_capture = self.packet_capture[0]
603        result = self.packet_capture.configure_monitor_mode(
604            hostapd_constants.BAND_2G, channel_2g)
605        if not result:
606            raise ValueError("Failed to configure channel for 2G band")
607
608        result = self.packet_capture.configure_monitor_mode(
609            hostapd_constants.BAND_5G, channel_5g)
610        if not result:
611            raise ValueError("Failed to configure channel for 5G band.")
612
613    @staticmethod
614    def wifi_test_wrap(fn):
615        def _safe_wrap_test_case(self, *args, **kwargs):
616            test_id = "%s:%s:%s" % (self.__class__.__name__, self.test_name,
617                                    self.log_begin_time.replace(' ', '-'))
618            self.test_id = test_id
619            self.result_detail = ""
620            tries = int(self.user_params.get("wifi_auto_rerun", 3))
621            for ad in self.android_devices:
622                ad.log_path = self.log_path
623            for i in range(tries + 1):
624                result = True
625                if i > 0:
626                    log_string = "[Test Case] RETRY:%s %s" % (i, self.test_name)
627                    self.log.info(log_string)
628                    self._teardown_test(self.test_name)
629                    self._setup_test(self.test_name)
630                try:
631                    result = fn(self, *args, **kwargs)
632                except signals.TestFailure as e:
633                    self.log.warn("Error msg: %s" % e)
634                    if self.result_detail:
635                        signal.details = self.result_detail
636                    result = False
637                except signals.TestSignal:
638                    if self.result_detail:
639                        signal.details = self.result_detail
640                    raise
641                except Exception as e:
642                    self.log.exception(e)
643                    asserts.fail(self.result_detail)
644                if result is False:
645                    if i < tries:
646                        continue
647                else:
648                    break
649            if result is not False:
650                asserts.explicit_pass(self.result_detail)
651            else:
652                asserts.fail(self.result_detail)
653
654        return _safe_wrap_test_case
655