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
320        config_count = 1
321        count = 0
322
323        # For example, the NetworkSelector tests use 2 APs and require that
324        # both APs are not mirrored.
325        if not mirror_ap and ap_count == 1:
326             raise ValueError("ap_count cannot be 1 if mirror_ap is False.")
327
328        if not mirror_ap:
329            config_count = ap_count
330
331        self.user_params["reference_networks"] = []
332        self.user_params["open_network"] = []
333        if wpa_network:
334            self.user_params["wpa_networks"] = []
335        if wep_network:
336            self.user_params["wep_networks"] = []
337        if ent_network:
338            self.user_params["ent_networks"] = []
339        if ent_network_pwd:
340            self.user_params["ent_networks_pwd"] = []
341
342        # kill hostapd & dhcpd if the cleanup was not successful
343        for i in range(len(self.access_points)):
344            self.log.debug("Check ap state and cleanup")
345            self._cleanup_hostapd_and_dhcpd(i)
346
347        for count in range(config_count):
348
349            network_list_2g = []
350            network_list_5g = []
351
352            orig_network_list_2g = []
353            orig_network_list_5g = []
354
355            network_list_2g.append({"channel": channel_2g})
356            network_list_5g.append({"channel": channel_5g})
357
358            networks_dict = self.get_psk_network(
359                                mirror_ap,
360                                self.user_params["reference_networks"],
361                                hidden=hidden,
362                                same_ssid=same_ssid)
363            self.reference_networks = self.user_params["reference_networks"]
364
365            network_list_2g.append(networks_dict["2g"])
366            network_list_5g.append(networks_dict["5g"])
367
368            # When same_ssid is set, only configure one set of WPA networks.
369            # We cannot have more than one set because duplicate interface names
370            # are not allowed.
371            # TODO(bmahadev): Provide option to select the type of network,
372            # instead of defaulting to WPA.
373            if not same_ssid:
374                networks_dict = self.get_open_network(
375                                    mirror_ap,
376                                    self.user_params["open_network"],
377                                    hidden=hidden,
378                                    same_ssid=same_ssid)
379                self.open_network = self.user_params["open_network"]
380
381                network_list_2g.append(networks_dict["2g"])
382                network_list_5g.append(networks_dict["5g"])
383
384                if wpa_network:
385                    networks_dict = self.get_psk_network(
386                                        mirror_ap,
387                                        self.user_params["wpa_networks"],
388                                        hidden=hidden,
389                                        same_ssid=same_ssid,
390                                        security_mode=hostapd_constants.WPA_STRING)
391                    self.wpa_networks = self.user_params["wpa_networks"]
392
393                    network_list_2g.append(networks_dict["2g"])
394                    network_list_5g.append(networks_dict["5g"])
395
396                if wep_network:
397                    networks_dict = self.get_wep_network(
398                                        mirror_ap,
399                                        self.user_params["wep_networks"],
400                                        hidden=hidden,
401                                        same_ssid=same_ssid)
402                    self.wep_networks = self.user_params["wep_networks"]
403
404                    network_list_2g.append(networks_dict["2g"])
405                    network_list_5g.append(networks_dict["5g"])
406
407                if ent_network:
408                    networks_dict = self.get_open_network(
409                                        mirror_ap,
410                                        self.user_params["ent_networks"],
411                                        hidden=hidden,
412                                        same_ssid=same_ssid)
413                    networks_dict["2g"]["security"] = hostapd_constants.ENT_STRING
414                    networks_dict["2g"].update(radius_conf_2g)
415                    networks_dict["5g"]["security"] = hostapd_constants.ENT_STRING
416                    networks_dict["5g"].update(radius_conf_5g)
417                    self.ent_networks = self.user_params["ent_networks"]
418
419                    network_list_2g.append(networks_dict["2g"])
420                    network_list_5g.append(networks_dict["5g"])
421
422                if ent_network_pwd:
423                    networks_dict = self.get_open_network(
424                                        mirror_ap,
425                                        self.user_params["ent_networks_pwd"],
426                                        hidden=hidden,
427                                        same_ssid=same_ssid)
428                    networks_dict["2g"]["security"] = hostapd_constants.ENT_STRING
429                    networks_dict["2g"].update(radius_conf_pwd)
430                    networks_dict["5g"]["security"] = hostapd_constants.ENT_STRING
431                    networks_dict["5g"].update(radius_conf_pwd)
432                    self.ent_networks_pwd = self.user_params["ent_networks_pwd"]
433
434                    network_list_2g.append(networks_dict["2g"])
435                    network_list_5g.append(networks_dict["5g"])
436
437            orig_network_list_5g = copy.copy(network_list_5g)
438            orig_network_list_2g = copy.copy(network_list_2g)
439
440            if len(network_list_5g) > 1:
441                self.config_5g = self._generate_legacy_ap_config(network_list_5g)
442            if len(network_list_2g) > 1:
443                self.config_2g = self._generate_legacy_ap_config(network_list_2g)
444
445            self.access_points[count].start_ap(self.config_2g)
446            self.access_points[count].start_ap(self.config_5g)
447            self.populate_bssid(count, self.access_points[count], orig_network_list_5g,
448                                orig_network_list_2g)
449
450        # Repeat configuration on the second router.
451        if mirror_ap and ap_count == 2:
452            self.access_points[AP_2].start_ap(self.config_2g)
453            self.access_points[AP_2].start_ap(self.config_5g)
454            self.populate_bssid(AP_2, self.access_points[AP_2],
455                orig_network_list_5g, orig_network_list_2g)
456
457    def _kill_processes(self, ap, daemon):
458        """ Kill hostapd and dhcpd daemons
459
460        Args:
461            ap: AP to cleanup
462            daemon: process to kill
463
464        Returns: True/False if killing process is successful
465        """
466        self.log.info("Killing %s" % daemon)
467        pids = ap.ssh.run('pidof %s' % daemon, ignore_status=True)
468        if pids.stdout:
469            ap.ssh.run('kill %s' % pids.stdout, ignore_status=True)
470        time.sleep(3)
471        pids = ap.ssh.run('pidof %s' % daemon, ignore_status=True)
472        if pids.stdout:
473            return False
474        return True
475
476    def _cleanup_hostapd_and_dhcpd(self, count):
477        """ Check if AP was cleaned up properly
478
479        Kill hostapd and dhcpd processes if cleanup was not successful in the
480        last run
481
482        Args:
483            count: AP to check
484
485        Returns:
486            New AccessPoint object if AP required cleanup
487
488        Raises:
489            Error: if the AccessPoint timed out to setup
490        """
491        ap = self.access_points[count]
492        phy_ifaces = ap.interfaces.get_physical_interface()
493        kill_hostapd = False
494        for iface in phy_ifaces:
495            if '2g_' in iface or '5g_' in iface or 'xg_' in iface:
496                kill_hostapd = True
497                break
498
499        if not kill_hostapd:
500            return
501
502        self.log.debug("Cleanup AP")
503        if not self._kill_processes(ap, 'hostapd') or \
504            not self._kill_processes(ap, 'dhcpd'):
505              raise("Failed to cleanup AP")
506
507        ap.__init__(self.user_params['AccessPoint'][count])
508
509    def _generate_legacy_ap_config(self, network_list):
510        bss_settings = []
511        wlan_2g = self.access_points[AP_1].wlan_2g
512        wlan_5g = self.access_points[AP_1].wlan_5g
513        ap_settings = network_list.pop(0)
514        # TODO:(bmahadev) This is a bug. We should not have to pop the first
515        # network in the list and treat it as a separate case. Instead,
516        # create_ap_preset() should be able to take NULL ssid and security and
517        # build config based on the bss_Settings alone.
518        hostapd_config_settings = network_list.pop(0)
519        for network in network_list:
520            if "password" in network:
521                bss_settings.append(
522                    hostapd_bss_settings.BssSettings(
523                        name=network["SSID"],
524                        ssid=network["SSID"],
525                        hidden=network["hiddenSSID"],
526                        security=hostapd_security.Security(
527                            security_mode=network["security"],
528                            password=network["password"])))
529            elif "wepKeys" in network:
530                bss_settings.append(
531                    hostapd_bss_settings.BssSettings(
532                        name=network["SSID"],
533                        ssid=network["SSID"],
534                        hidden=network["hiddenSSID"],
535                        security=hostapd_security.Security(
536                            security_mode=network["security"],
537                            password=network["wepKeys"][0])))
538            elif network["security"] == hostapd_constants.ENT_STRING:
539                bss_settings.append(
540                    hostapd_bss_settings.BssSettings(
541                        name=network["SSID"],
542                        ssid=network["SSID"],
543                        hidden=network["hiddenSSID"],
544                        security=hostapd_security.Security(
545                            security_mode=network["security"],
546                            radius_server_ip=network["radius_server_ip"],
547                            radius_server_port=network["radius_server_port"],
548                            radius_server_secret=network["radius_server_secret"])))
549            else:
550                bss_settings.append(
551                    hostapd_bss_settings.BssSettings(
552                        name=network["SSID"],
553                        ssid=network["SSID"],
554                        hidden=network["hiddenSSID"]))
555        if "password" in hostapd_config_settings:
556            config = hostapd_ap_preset.create_ap_preset(
557                iface_wlan_2g=wlan_2g,
558                iface_wlan_5g=wlan_5g,
559                channel=ap_settings["channel"],
560                ssid=hostapd_config_settings["SSID"],
561                hidden=hostapd_config_settings["hiddenSSID"],
562                security=hostapd_security.Security(
563                    security_mode=hostapd_config_settings["security"],
564                    password=hostapd_config_settings["password"]),
565                bss_settings=bss_settings)
566        elif "wepKeys" in hostapd_config_settings:
567            config = hostapd_ap_preset.create_ap_preset(
568                iface_wlan_2g=wlan_2g,
569                iface_wlan_5g=wlan_5g,
570                channel=ap_settings["channel"],
571                ssid=hostapd_config_settings["SSID"],
572                hidden=hostapd_config_settings["hiddenSSID"],
573                security=hostapd_security.Security(
574                    security_mode=hostapd_config_settings["security"],
575                    password=hostapd_config_settings["wepKeys"][0]),
576                bss_settings=bss_settings)
577        else:
578            config = hostapd_ap_preset.create_ap_preset(
579                iface_wlan_2g=wlan_2g,
580                iface_wlan_5g=wlan_5g,
581                channel=ap_settings["channel"],
582                ssid=hostapd_config_settings["SSID"],
583                hidden=hostapd_config_settings["hiddenSSID"],
584                bss_settings=bss_settings)
585        return config
586
587    def configure_packet_capture(
588            self,
589            channel_5g=hostapd_constants.AP_DEFAULT_CHANNEL_5G,
590            channel_2g=hostapd_constants.AP_DEFAULT_CHANNEL_2G):
591        """Configure packet capture for 2G and 5G bands.
592
593        Args:
594            channel_5g: Channel to set the monitor mode to for 5G band.
595            channel_2g: Channel to set the monitor mode to for 2G band.
596        """
597        self.packet_capture = self.packet_capture[0]
598        result = self.packet_capture.configure_monitor_mode(
599            hostapd_constants.BAND_2G, channel_2g)
600        if not result:
601            raise ValueError("Failed to configure channel for 2G band")
602
603        result = self.packet_capture.configure_monitor_mode(
604            hostapd_constants.BAND_5G, channel_5g)
605        if not result:
606            raise ValueError("Failed to configure channel for 5G band.")
607
608    @staticmethod
609    def wifi_test_wrap(fn):
610        def _safe_wrap_test_case(self, *args, **kwargs):
611            test_id = "%s:%s:%s" % (self.__class__.__name__, self.test_name,
612                                    self.log_begin_time.replace(' ', '-'))
613            self.test_id = test_id
614            self.result_detail = ""
615            tries = int(self.user_params.get("wifi_auto_rerun", 3))
616            for ad in self.android_devices:
617                ad.log_path = self.log_path
618            for i in range(tries + 1):
619                result = True
620                if i > 0:
621                    log_string = "[Test Case] RETRY:%s %s" % (i, self.test_name)
622                    self.log.info(log_string)
623                    self._teardown_test(self.test_name)
624                    self._setup_test(self.test_name)
625                try:
626                    result = fn(self, *args, **kwargs)
627                except signals.TestFailure as e:
628                    self.log.warn("Error msg: %s" % e)
629                    if self.result_detail:
630                        signal.details = self.result_detail
631                    result = False
632                except signals.TestSignal:
633                    if self.result_detail:
634                        signal.details = self.result_detail
635                    raise
636                except Exception as e:
637                    self.log.exception(e)
638                    asserts.fail(self.result_detail)
639                if result is False:
640                    if i < tries:
641                        continue
642                else:
643                    break
644            if result is not False:
645                asserts.explicit_pass(self.result_detail)
646            else:
647                asserts.fail(self.result_detail)
648
649        return _safe_wrap_test_case
650