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