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