1# Copyright (C) 2024 The Android Open Source Project 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15"""Nearby Connection E2E stress tests for D2D wifi performance.""" 16 17import abc 18import datetime 19import logging 20import time 21from typing import Any 22 23from mobly import asserts 24from mobly.controllers import android_device 25 26from betocq import iperf_utils 27from betocq import nc_base_test 28from betocq import nc_constants 29from betocq import nearby_connection_wrapper 30from betocq import setup_utils 31 32 33_DELAY_BETWEEN_EACH_TEST_CYCLE = datetime.timedelta(seconds=0) 34_BITS_PER_BYTE = 8 35_MAX_FREQ_2G_MHZ = 2500 36_MIN_FREQ_5G_DFS_MHZ = 5260 37_MAX_FREQ_5G_DFS_MHZ = 5720 38 39 40class D2dPerformanceTestBase(nc_base_test.NCBaseTestClass, abc.ABC): 41 """Abstract class for D2D performance test for different connection meidums.""" 42 43 performance_test_iterations: int 44 45 # @typing.override 46 def __init__(self, configs): 47 super().__init__(configs) 48 self._is_mcc: bool = False 49 self._is_2g_d2d_wifi_medium: bool = False 50 self._is_dbs_mode: bool = False 51 self._throughput_low_string: str = '' 52 self._upgrade_medium_under_test: nc_constants.NearbyMedium = None 53 self._connection_medium: nc_constants.NearbyMedium = None 54 self._advertising_discovery_medium: nc_constants.NearbyMedium = None 55 self._current_test_result: nc_constants.SingleTestResult = ( 56 nc_constants.SingleTestResult() 57 ) 58 self._performance_test_metrics: nc_constants.NcPerformanceTestMetrics = ( 59 nc_constants.NcPerformanceTestMetrics() 60 ) 61 self._prior_bt_nc_fail_reason: nc_constants.SingleTestFailureReason = ( 62 nc_constants.SingleTestFailureReason.UNINITIALIZED 63 ) 64 self._active_nc_fail_reason: nc_constants.SingleTestFailureReason = ( 65 nc_constants.SingleTestFailureReason.UNINITIALIZED 66 ) 67 self._finished_test_iteration: int = 0 68 self._use_prior_bt: bool = False 69 self._start_time: datetime.datetime = datetime.datetime.now() 70 self._wifi_ssid: str = '' 71 self._test_results: list[nc_constants.SingleTestResult] = [] 72 73 # @typing.override 74 def setup_test(self): 75 self._current_test_result: nc_constants.SingleTestResult = ( 76 nc_constants.SingleTestResult() 77 ) 78 self._prior_bt_nc_fail_reason = ( 79 nc_constants.SingleTestFailureReason.UNINITIALIZED 80 ) 81 self._active_nc_fail_reason = ( 82 nc_constants.SingleTestFailureReason.UNINITIALIZED 83 ) 84 super().setup_test() 85 86 def teardown_test(self): 87 self._write_current_test_report() 88 self._collect_current_test_metrics() 89 super().teardown_test() 90 time.sleep(_DELAY_BETWEEN_EACH_TEST_CYCLE.total_seconds()) 91 92 @property 93 def _devices_capabilities_definition(self) -> dict[str, dict[str, bool]]: 94 """Returns the definition of devices capabilities.""" 95 return {} 96 97 # @typing.override 98 def _get_skipped_test_class_reason(self) -> str | None: 99 if not self._is_wifi_ap_ready(): 100 return 'Wifi AP is not ready for this test.' 101 skip_reason = self._check_devices_capabilities() 102 if skip_reason is not None: 103 return ( 104 f'The test is not required per the device capabilities. {skip_reason}' 105 ) 106 return None 107 108 @abc.abstractmethod 109 def _is_wifi_ap_ready(self) -> bool: 110 pass 111 112 def _check_devices_capabilities(self) -> str | None: 113 """Checks if all devices capabilities meet requirements.""" 114 for ad_role, capabilities in self._devices_capabilities_definition.items(): 115 for key, value in capabilities.items(): 116 ad = getattr(self, ad_role) 117 capability = getattr(ad, key) 118 if capability != value: 119 return ( 120 f'{ad} {ad_role}.{key} is' 121 f' {"enabled" if capability else "disabled"}' 122 ) 123 return None 124 125 def _get_target_sta_frequency_and_max_link_speed(self) -> tuple[int, int]: 126 """Gets the STA frequency and max link speed.""" 127 connection_info = self.advertiser.nearby.wifiGetConnectionInfo() 128 sta_frequency = int( 129 connection_info.get('mFrequency', nc_constants.INVALID_INT) 130 ) 131 132 sta_max_link_speed_mbps = int( 133 connection_info.get( 134 'mMaxSupportedTxLinkSpeed', nc_constants.INVALID_INT 135 ) 136 ) 137 if sta_frequency == nc_constants.INVALID_INT: 138 sta_frequency = setup_utils.get_wifi_sta_frequency(self.advertiser) 139 sta_max_link_speed_mbps = setup_utils.get_wifi_sta_max_link_speed( 140 self.advertiser 141 ) 142 return (sta_frequency, sta_max_link_speed_mbps) 143 144 def _get_throughput_benchmark( 145 self, sta_frequency: int, sta_max_link_speed_mbps: int 146 ) -> tuple[float, float]: 147 """Gets the throughput benchmark as MBps.""" 148 max_num_streams = min( 149 self.discoverer.max_num_streams, self.advertiser.max_num_streams 150 ) 151 152 if self._is_2g_d2d_wifi_medium: 153 max_phy_rate_mbps = min( 154 self.discoverer.max_phy_rate_2g_mbps, 155 self.advertiser.max_phy_rate_2g_mbps, 156 ) 157 max_phy_rate_mbps = min( 158 max_phy_rate_mbps, 159 max_num_streams * nc_constants.MAX_PHY_RATE_PER_STREAM_N_20_MBPS, 160 ) 161 min_throughput_mbyte_per_sec = int( 162 max_phy_rate_mbps 163 * nc_constants.MAX_PHY_RATE_TO_MIN_THROUGHPUT_RATIO_2G 164 / _BITS_PER_BYTE 165 ) 166 nc_min_throughput_mbyte_per_sec = min_throughput_mbyte_per_sec 167 else: # 5G wifi medium 168 max_phy_rate_mbps = min( 169 self.discoverer.max_phy_rate_5g_mbps, 170 self.advertiser.max_phy_rate_5g_mbps, 171 ) 172 # max_num_streams could be smaller in DBS mode 173 if self._is_dbs_mode: 174 max_num_streams = self.advertiser.max_num_streams_dbs 175 176 max_phy_rate_ac80 = ( 177 max_num_streams * nc_constants.MAX_PHY_RATE_PER_STREAM_AC_80_MBPS 178 ) 179 max_phy_rate_mbps = min(max_phy_rate_mbps, max_phy_rate_ac80) 180 181 # if STA is connected to 5G AP with channel BW < 80, 182 # limit the max phy rate to AC 40. 183 if ( 184 sta_frequency > 5000 185 and sta_max_link_speed_mbps > 0 186 and sta_max_link_speed_mbps < max_phy_rate_ac80 187 ): 188 max_phy_rate_mbps = min( 189 max_phy_rate_mbps, 190 max_num_streams * nc_constants.MAX_PHY_RATE_PER_STREAM_AC_40_MBPS, 191 ) 192 193 min_throughput_mbyte_per_sec = int( 194 max_phy_rate_mbps 195 * nc_constants.MAX_PHY_RATE_TO_MIN_THROUGHPUT_RATIO_5G 196 / _BITS_PER_BYTE 197 ) 198 if self._is_mcc: 199 min_throughput_mbyte_per_sec = int( 200 min_throughput_mbyte_per_sec 201 * nc_constants.MCC_THROUGHPUT_MULTIPLIER 202 ) 203 204 nc_min_throughput_mbyte_per_sec = min_throughput_mbyte_per_sec 205 if ( 206 self._current_test_result.quality_info.upgrade_medium 207 == nc_constants.NearbyConnectionMedium.WIFI_LAN 208 ): 209 nc_min_throughput_mbyte_per_sec = min( 210 nc_min_throughput_mbyte_per_sec, 211 nc_constants.WLAN_THROUGHPUT_CAP_MBPS, 212 ) 213 214 215 self.advertiser.log.info( 216 f'target STA freq = {sta_frequency}, ' 217 f'max STA speed (Mb/s): {sta_max_link_speed_mbps}, ' 218 f'max D2D speed (MB/s): {max_phy_rate_mbps / _BITS_PER_BYTE}, ' 219 f'min D2D speed (MB/s), iperf: {min_throughput_mbyte_per_sec}, ' 220 f'nc: {nc_min_throughput_mbyte_per_sec}' 221 ) 222 return (min_throughput_mbyte_per_sec, nc_min_throughput_mbyte_per_sec) 223 224 def _test_connection_medium_performance( 225 self, 226 upgrade_medium_under_test: nc_constants.NearbyMedium, 227 wifi_ssid: str = '', # used by discoverer and possibly advertiser 228 wifi_password: str = '', # used by discoverer and advertiser 229 force_disable_bt_multiplex: bool = False, 230 connection_medium: nc_constants.NearbyMedium = nc_constants.NearbyMedium.BT_ONLY, 231 wifi_ssid2: str = '', # used by advertiser if not empty 232 ) -> None: 233 """Test the D2D performance with the specified upgrade medium.""" 234 self._upgrade_medium_under_test = upgrade_medium_under_test 235 self._connection_medium = connection_medium 236 self._wifi_ssid = wifi_ssid 237 238 if self.test_parameters.toggle_airplane_mode_target_side: 239 setup_utils.toggle_airplane_mode(self.advertiser) 240 advertising_discovery_medium = nc_constants.NearbyMedium( 241 self.test_parameters.advertising_discovery_medium 242 ) 243 self._advertising_discovery_medium = advertising_discovery_medium 244 if self.test_parameters.reset_wifi_connection: 245 self._reset_wifi_connection() 246 # 1. discoverer connect to wifi STA/AP 247 self._current_test_result = nc_constants.SingleTestResult() 248 if wifi_ssid: 249 self._active_nc_fail_reason = ( 250 nc_constants.SingleTestFailureReason.SOURCE_WIFI_CONNECTION 251 ) 252 discoverer_wifi_sta_latency = ( 253 setup_utils.connect_to_wifi_sta_till_success( 254 self.discoverer, wifi_ssid, wifi_password 255 ) 256 ) 257 self._active_nc_fail_reason = nc_constants.SingleTestFailureReason.SUCCESS 258 self.discoverer.log.info( 259 'connecting to wifi in ' 260 f'{round(discoverer_wifi_sta_latency.total_seconds())} s' 261 ) 262 self._current_test_result.discoverer_sta_expected = True 263 self._current_test_result.discoverer_sta_latency = ( 264 discoverer_wifi_sta_latency 265 ) 266 267 # 2. set up BT connection if required 268 # Because target STA is not yet connected, discovery is done over BLE only. 269 advertising_discovery_medium = nc_constants.NearbyMedium.BLE_ONLY 270 271 connection_setup_timeouts = nc_constants.ConnectionSetupTimeouts( 272 nc_constants.FIRST_DISCOVERY_TIMEOUT, 273 nc_constants.FIRST_CONNECTION_INIT_TIMEOUT, 274 nc_constants.FIRST_CONNECTION_RESULT_TIMEOUT, 275 ) 276 prior_bt_snippet = None 277 if ( 278 not force_disable_bt_multiplex 279 and self.test_parameters.requires_bt_multiplex 280 ): 281 logging.info('set up a prior BT connection.') 282 self._use_prior_bt = True 283 prior_bt_snippet = nearby_connection_wrapper.NearbyConnectionWrapper( 284 self.advertiser, 285 self.discoverer, 286 self.advertiser.nearby2, 287 self.discoverer.nearby2, 288 advertising_discovery_medium=nc_constants.NearbyMedium.BLE_ONLY, 289 connection_medium=nc_constants.NearbyMedium.BT_ONLY, 290 upgrade_medium=nc_constants.NearbyMedium.BT_ONLY, 291 ) 292 293 try: 294 prior_bt_snippet.start_nearby_connection( 295 timeouts=connection_setup_timeouts, 296 medium_upgrade_type=nc_constants.MediumUpgradeType.NON_DISRUPTIVE, 297 ) 298 finally: 299 self._prior_bt_nc_fail_reason = prior_bt_snippet.test_failure_reason 300 self._current_test_result.prior_nc_quality_info = ( 301 prior_bt_snippet.connection_quality_info 302 ) 303 304 # set up Wifi connection and transfer 305 # 3. advertiser connect to wifi STA/AP 306 wifi_ssid_advertiser = wifi_ssid 307 if wifi_ssid2: 308 wifi_ssid_advertiser = wifi_ssid2 309 if wifi_ssid_advertiser: 310 self._active_nc_fail_reason = ( 311 nc_constants.SingleTestFailureReason.TARGET_WIFI_CONNECTION 312 ) 313 advertiser_wifi_sta_latency = ( 314 setup_utils.connect_to_wifi_sta_till_success( 315 self.advertiser, wifi_ssid_advertiser, wifi_password 316 ) 317 ) 318 self.advertiser.log.info( 319 'connecting to wifi in ' 320 f'{round(advertiser_wifi_sta_latency.total_seconds())} s' 321 ) 322 self.advertiser.log.info( 323 self.advertiser.nearby.wifiGetConnectionInfo().get('mFrequency') 324 ) 325 self._current_test_result.advertiser_wifi_expected = True 326 self._current_test_result.advertiser_sta_latency = ( 327 advertiser_wifi_sta_latency 328 ) 329 # Let scan, DHCP and internet validation complete before NC. 330 # This is important especially for the transfer speed test. 331 time.sleep(self.test_parameters.target_post_wifi_connection_idle_time_sec) 332 333 # 4. set up the D2D nearby connection 334 logging.info('set up a nearby connection for file transfer.') 335 active_snippet = nearby_connection_wrapper.NearbyConnectionWrapper( 336 self.advertiser, 337 self.discoverer, 338 self.advertiser.nearby, 339 self.discoverer.nearby, 340 advertising_discovery_medium=advertising_discovery_medium, 341 connection_medium=connection_medium, 342 upgrade_medium=upgrade_medium_under_test, 343 ) 344 if prior_bt_snippet: 345 connection_setup_timeouts = nc_constants.ConnectionSetupTimeouts( 346 nc_constants.SECOND_DISCOVERY_TIMEOUT, 347 nc_constants.SECOND_CONNECTION_INIT_TIMEOUT, 348 nc_constants.SECOND_CONNECTION_RESULT_TIMEOUT, 349 ) 350 try: 351 active_snippet.start_nearby_connection( 352 timeouts=connection_setup_timeouts, 353 medium_upgrade_type=nc_constants.MediumUpgradeType.DISRUPTIVE, 354 keep_alive_timeout_ms=self.test_parameters.keep_alive_timeout_ms, 355 keep_alive_interval_ms=self.test_parameters.keep_alive_interval_ms, 356 ) 357 finally: 358 self._active_nc_fail_reason = active_snippet.test_failure_reason 359 self._current_test_result.quality_info = ( 360 active_snippet.connection_quality_info 361 ) 362 363 # 5. transfer file through the nearby connection and optionally run iperf 364 try: 365 self._current_test_result.file_transfer_throughput_kbps = ( 366 active_snippet.transfer_file( 367 self._get_transfer_file_size(), 368 self._get_file_transfer_timeout(), 369 self.test_parameters.payload_type, 370 ) 371 ) 372 if ( 373 self.test_parameters.run_iperf_test 374 and not self._is_mcc 375 and upgrade_medium_under_test 376 in [ 377 nc_constants.NearbyMedium.UPGRADE_TO_WIFIDIRECT, 378 nc_constants.NearbyMedium.UPGRADE_TO_WIFIHOTSPOT, 379 nc_constants.NearbyMedium.WIFILAN_ONLY, 380 nc_constants.NearbyMedium.WIFIAWARE_ONLY, 381 ] 382 ): 383 # TODO: b/338094399 - update this part for the connection over WFD. 384 self._current_test_result.iperf_throughput_kbps = ( 385 iperf_utils.run_iperf_test( 386 self.discoverer, 387 self.advertiser, 388 self._current_test_result.quality_info.upgrade_medium, 389 ) 390 ) 391 392 finally: 393 self._active_nc_fail_reason = active_snippet.test_failure_reason 394 self._check_ap_connection_and_speed(wifi_ssid_advertiser) 395 396 # 6. disconnect prior BT connection if required 397 if prior_bt_snippet: 398 prior_bt_snippet.disconnect_endpoint() 399 # 7. disconnect D2D active connection 400 active_snippet.disconnect_endpoint() 401 402 def _check_ap_connection_and_speed(self, wifi_ssid: str) -> None: 403 (sta_frequency, max_link_speed_mbps) = ( 404 self._get_target_sta_frequency_and_max_link_speed() 405 ) 406 self._current_test_result.sta_frequency = sta_frequency 407 self._current_test_result.max_sta_link_speed_mbps = max_link_speed_mbps 408 if wifi_ssid and( 409 sta_frequency == nc_constants.INVALID_INT 410 or max_link_speed_mbps == nc_constants.INVALID_INT 411 ): 412 self._active_nc_fail_reason = ( 413 nc_constants.SingleTestFailureReason.DISCONNECTED_FROM_AP 414 ) 415 asserts.fail( 416 'Target device is disconnected from AP. Check AP DHCP config.' 417 ) 418 419 iperf_speed_min_mbps, nc_speed_min_mbps = self._get_throughput_benchmark( 420 sta_frequency, max_link_speed_mbps 421 ) 422 423 if wifi_ssid and not self._is_valid_sta_frequency(wifi_ssid, sta_frequency): 424 self._active_nc_fail_reason = ( 425 nc_constants.SingleTestFailureReason.WRONG_AP_FREQUENCY 426 ) 427 asserts.fail(f'AP is set to a wrong frequency {sta_frequency}') 428 429 if ( 430 self._current_test_result.quality_info.upgrade_medium 431 in [ 432 nc_constants.NearbyConnectionMedium.WIFI_DIRECT, 433 nc_constants.NearbyConnectionMedium.WIFI_HOTSPOT, 434 ] 435 ): 436 p2p_frequency = setup_utils.get_wifi_p2p_frequency(self.advertiser) 437 self._current_test_result.quality_info.medium_frequency = p2p_frequency 438 if all([ 439 p2p_frequency != nc_constants.INVALID_INT, 440 p2p_frequency != sta_frequency, 441 not self._is_mcc, 442 not self._is_dbs_mode, 443 nc_speed_min_mbps > 0, 444 ]): 445 self._active_nc_fail_reason = ( 446 nc_constants.SingleTestFailureReason.WRONG_P2P_FREQUENCY 447 ) 448 asserts.fail( 449 f'P2P frequeny ({p2p_frequency}) is different from STA frequency' 450 f' ({sta_frequency}) in SCC test case. Check the device capability' 451 ' configuration especially for DBS, DFS, indoor capabilities.' 452 ) 453 if all([ 454 p2p_frequency != nc_constants.INVALID_INT, 455 p2p_frequency == sta_frequency, 456 self._is_mcc, 457 nc_speed_min_mbps > 0, 458 ]): 459 self._active_nc_fail_reason = ( 460 nc_constants.SingleTestFailureReason.WRONG_P2P_FREQUENCY 461 ) 462 asserts.fail( 463 f'P2P frequeny ({p2p_frequency}) is same as STA frequency' 464 f' ({sta_frequency}) in MCC test case. Check the device capability' 465 ' configuration especially for DBS, DFS, indoor capabilities.' 466 ) 467 468 if ( 469 self._active_nc_fail_reason 470 is nc_constants.SingleTestFailureReason.SUCCESS 471 ): 472 iperf_speed_mbps = int( 473 self._current_test_result.iperf_throughput_kbps / 1024 474 ) 475 nc_speed_mbps = round( 476 self._current_test_result.file_transfer_throughput_kbps / 1024, 2 477 ) 478 479 if (nc_speed_mbps < nc_speed_min_mbps) or ( 480 iperf_speed_mbps > 0 and iperf_speed_mbps < iperf_speed_min_mbps 481 ): 482 self._active_nc_fail_reason = ( 483 nc_constants.SingleTestFailureReason.FILE_TRANSFER_THROUGHPUT_LOW 484 ) 485 result_str = '' 486 if iperf_speed_mbps > 0 and iperf_speed_mbps < iperf_speed_min_mbps: 487 result_str = result_str + ( 488 f'iperf speed {iperf_speed_mbps} < target {iperf_speed_min_mbps}' 489 ) 490 if nc_speed_mbps < nc_speed_min_mbps: 491 result_str = result_str + ( 492 f' file speed {nc_speed_mbps} < target {nc_speed_min_mbps}' 493 ) 494 self._throughput_low_string = result_str + ' MB/s' 495 asserts.fail(self._throughput_low_string) 496 497 def _is_valid_sta_frequency(self, wifi_ssid: str, sta_frequency: int) -> bool: 498 if wifi_ssid == self.test_parameters.wifi_2g_ssid: 499 return sta_frequency <= _MAX_FREQ_2G_MHZ 500 elif wifi_ssid == self.test_parameters.wifi_5g_ssid: 501 return sta_frequency > _MAX_FREQ_2G_MHZ and ( 502 sta_frequency < _MIN_FREQ_5G_DFS_MHZ 503 or sta_frequency > _MAX_FREQ_5G_DFS_MHZ 504 ) 505 else: # 5G DFS band 506 return ( 507 sta_frequency >= _MIN_FREQ_5G_DFS_MHZ 508 and sta_frequency <= _MAX_FREQ_5G_DFS_MHZ 509 ) 510 511 def _get_transfer_file_size(self) -> int: 512 return nc_constants.TRANSFER_FILE_SIZE_500MB 513 514 def _get_file_transfer_timeout(self) -> datetime.timedelta: 515 return nc_constants.WIFI_500M_PAYLOAD_TRANSFER_TIMEOUT 516 517 def _write_current_test_report(self) -> None: 518 """Writes test report for each iteration.""" 519 self._current_test_result.test_iteration = self._finished_test_iteration 520 self._finished_test_iteration += 1 521 if ( 522 self._use_prior_bt 523 and self._prior_bt_nc_fail_reason 524 is not nc_constants.SingleTestFailureReason.SUCCESS 525 ): 526 self._current_test_result.is_failed_with_prior_bt = True 527 self._current_test_result.failure_reason = self._prior_bt_nc_fail_reason 528 else: 529 self._current_test_result.failure_reason = self._active_nc_fail_reason 530 result_message = self._get_current_test_result_message() 531 self._current_test_result.result_message = result_message 532 self._test_results.append(self._current_test_result) 533 534 quality_info: list[Any] = [] 535 if self._use_prior_bt: 536 quality_info.append( 537 'prior_bt:' 538 f'{self._current_test_result.prior_nc_quality_info.get_dict()}' 539 ) 540 quality_info.append( 541 'file_transfer:' 542 f'{self._current_test_result.quality_info.get_dict()}' 543 ) 544 quality_info.append( 545 'speed: ' 546 f'{round(self._current_test_result.file_transfer_throughput_kbps/1024, 1)}' 547 'MBps' 548 ) 549 if self._current_test_result.iperf_throughput_kbps > 0: 550 quality_info.append( 551 'iperf: ' 552 f'{round(self._current_test_result.iperf_throughput_kbps/1024, 1)}' 553 'MBps' 554 ) 555 556 if self._current_test_result.discoverer_sta_expected: 557 src_connection_latency = round( 558 self._current_test_result.discoverer_sta_latency.total_seconds() 559 ) 560 quality_info.append(f'src_sta: {src_connection_latency}s') 561 if self._current_test_result.advertiser_wifi_expected: 562 tgt_connection_latency = round( 563 self._current_test_result.advertiser_sta_latency.total_seconds() 564 ) 565 quality_info.append(f'tgt_sta: {tgt_connection_latency}s') 566 567 test_report = { 568 'result': result_message, 569 'quality_info': quality_info, 570 } 571 572 self.discoverer.log.info(test_report) 573 self.record_data({ 574 'Test Class': self.TAG, 575 'Test Name': self.current_test_info.name, 576 'properties': test_report, 577 }) 578 579 def _get_current_test_result_message(self) -> str: 580 if ( 581 self._use_prior_bt 582 and self._prior_bt_nc_fail_reason 583 is not nc_constants.SingleTestFailureReason.SUCCESS 584 ): 585 return ''.join([ 586 'FAIL (The prior BT connection): ', 587 f'{self._prior_bt_nc_fail_reason.name} - ', 588 nc_constants.COMMON_TRIAGE_TIP.get(self._prior_bt_nc_fail_reason), 589 ]) 590 591 if ( 592 self._active_nc_fail_reason 593 == nc_constants.SingleTestFailureReason.SUCCESS 594 ): 595 return 'PASS' 596 if ( 597 self._active_nc_fail_reason 598 == nc_constants.SingleTestFailureReason.SOURCE_WIFI_CONNECTION 599 ): 600 return ''.join([ 601 f'FAIL: {self._active_nc_fail_reason.name} - ', 602 nc_constants.COMMON_TRIAGE_TIP.get(self._active_nc_fail_reason), 603 ]) 604 605 if ( 606 self._active_nc_fail_reason 607 is nc_constants.SingleTestFailureReason.WIFI_MEDIUM_UPGRADE 608 ): 609 return ''.join([ 610 f'FAIL: {self._active_nc_fail_reason.name} - ', 611 self._get_medium_upgrade_failure_tip(), 612 ]) 613 if ( 614 self._active_nc_fail_reason 615 is nc_constants.SingleTestFailureReason.FILE_TRANSFER_FAIL 616 ): 617 return ''.join([ 618 f'{self._active_nc_fail_reason.name} - ', 619 self._get_file_transfer_failure_tip(), 620 ]) 621 if ( 622 self._active_nc_fail_reason 623 is nc_constants.SingleTestFailureReason.FILE_TRANSFER_THROUGHPUT_LOW 624 ): 625 return ''.join([ 626 f'{self._active_nc_fail_reason.name} - ', 627 self._get_throughput_low_tip(), 628 ]) 629 630 return ''.join([ 631 f'{self._active_nc_fail_reason.name} - ', 632 nc_constants.COMMON_TRIAGE_TIP.get( 633 self._active_nc_fail_reason, 'UNKNOWN' 634 ), 635 ]) 636 637 def _get_medium_upgrade_failure_tip(self) -> str: 638 return nc_constants.MEDIUM_UPGRADE_FAIL_TRIAGE_TIPS.get( 639 self._upgrade_medium_under_test, 640 f'unexpected upgrade medium - {self._upgrade_medium_under_test}', 641 ) 642 643 @abc.abstractmethod 644 def _get_file_transfer_failure_tip(self) -> str: 645 pass 646 647 @abc.abstractmethod 648 def _get_throughput_low_tip(self) -> str: 649 pass 650 651 def _collect_current_test_metrics(self) -> None: 652 """Collects test result metrics for each iteration.""" 653 if self._use_prior_bt: 654 self._performance_test_metrics.prior_bt_discovery_latencies.append( 655 self._current_test_result.prior_nc_quality_info.discovery_latency 656 ) 657 self._performance_test_metrics.prior_bt_connection_latencies.append( 658 self._current_test_result.prior_nc_quality_info.connection_latency 659 ) 660 661 self._performance_test_metrics.file_transfer_discovery_latencies.append( 662 self._current_test_result.quality_info.discovery_latency 663 ) 664 self._performance_test_metrics.file_transfer_connection_latencies.append( 665 self._current_test_result.quality_info.connection_latency 666 ) 667 self._performance_test_metrics.upgraded_wifi_transfer_mediums.append( 668 self._current_test_result.quality_info.upgrade_medium 669 ) 670 self._performance_test_metrics.file_transfer_throughputs_kbps.append( 671 self._current_test_result.file_transfer_throughput_kbps 672 ) 673 self._performance_test_metrics.iperf_throughputs_kbps.append( 674 self._current_test_result.iperf_throughput_kbps 675 ) 676 self._performance_test_metrics.discoverer_wifi_sta_latencies.append( 677 self._current_test_result.discoverer_sta_latency 678 ) 679 self._performance_test_metrics.advertiser_wifi_sta_latencies.append( 680 self._current_test_result.advertiser_sta_latency 681 ) 682 if ( 683 self._current_test_result.quality_info.medium_upgrade_expected 684 ): 685 self._performance_test_metrics.medium_upgrade_latencies.append( 686 self._current_test_result.quality_info.medium_upgrade_latency 687 ) 688 689 def __convert_kbps_to_mbps(self, throughput_kbps: float) -> float: 690 """Convert throughput from kbyte/s to mbyte/s.""" 691 return round(throughput_kbps / 1024, 1) 692 693 def __get_transfer_stats( 694 self, 695 throughput_indicators: list[float], 696 ) -> nc_constants.TestResultStats: 697 """get the min, median and max throughput from iterations which finished file transfer.""" 698 filtered = [ 699 x 700 for x in throughput_indicators 701 if x != nc_constants.UNSET_THROUGHPUT_KBPS 702 ] 703 if not filtered: 704 # all test cases are failed 705 return nc_constants.TestResultStats(0, 0, 0, 0) 706 # use the descenting order of the throughput 707 filtered.sort(reverse=True) 708 return nc_constants.TestResultStats( 709 len(filtered), 710 self.__convert_kbps_to_mbps(filtered[len(filtered) - 1]), 711 self.__convert_kbps_to_mbps( 712 filtered[int(len(filtered) * nc_constants.PERCENTILE_50_FACTOR)] 713 ), 714 self.__convert_kbps_to_mbps(filtered[0]), 715 ) 716 717 def __get_latency_stats( 718 self, latency_indicators: list[datetime.timedelta] 719 ) -> nc_constants.TestResultStats: 720 filtered = [ 721 latency.total_seconds() 722 for latency in latency_indicators 723 if latency != nc_constants.UNSET_LATENCY 724 ] 725 if not filtered: 726 # All test cases are failed. 727 return nc_constants.TestResultStats(0, 0, 0, 0) 728 729 filtered.sort() 730 731 percentile_50 = round( 732 filtered[int(len(filtered) * nc_constants.PERCENTILE_50_FACTOR)], 733 nc_constants.LATENCY_PRECISION_DIGITS, 734 ) 735 return nc_constants.TestResultStats( 736 len(filtered), 737 round(filtered[0], nc_constants.LATENCY_PRECISION_DIGITS), 738 percentile_50, 739 round( 740 filtered[len(filtered) - 1], nc_constants.LATENCY_PRECISION_DIGITS 741 ), 742 ) 743 744 # @typing.override 745 def _summary_test_results(self) -> None: 746 """Summarizes test results of all iterations.""" 747 success_count = sum( 748 test_result.failure_reason 749 == nc_constants.SingleTestFailureReason.SUCCESS 750 for test_result in self._test_results 751 ) 752 passed = success_count >= round( 753 self.performance_test_iterations * nc_constants.SUCCESS_RATE_TARGET 754 ) 755 final_result_message = ( 756 'PASS' 757 if passed 758 else ( 759 'FAIL: low successe rate: ' 760 f' {success_count / self.performance_test_iterations:.2%} is lower' 761 f' than the target {nc_constants.SUCCESS_RATE_TARGET:.2%}' 762 ) 763 ) 764 detailed_stats = [ 765 f'Required Iterations: {self.performance_test_iterations}', 766 f'Finished Iterations: {len(self._test_results)}', 767 ] 768 detailed_stats.append('Failed Iterations:') 769 detailed_stats.extend(self.__get_failed_iteration_messages()) 770 detailed_stats.append('File Transfer Connection Stats:') 771 detailed_stats.extend(self.__get_file_transfer_connection_stats()) 772 773 if self._use_prior_bt: 774 detailed_stats.append('Prior BT Connection Stats:') 775 detailed_stats.extend(self.__get_prior_bt_connection_stats()) 776 777 self.record_data({ 778 'Test Class': self.TAG, 779 'properties': { 780 '01_test_result': final_result_message, 781 '02_source_device': '\n'.join( 782 self.__get_device_attributes(self.discoverer) 783 ), 784 '03_target_device': '\n'.join( 785 self.__get_device_attributes(self.advertiser) 786 ), 787 '04_test_config': '\n'.join([ 788 f'Country Code: {self._get_country_code()}', 789 f'MCC mode: {self._is_mcc}', 790 f'2G medium {self._is_2g_d2d_wifi_medium}', 791 f'DBS mode: {self._is_dbs_mode}', 792 ( 793 'advertising_discovery_medium:' 794 f' {self._advertising_discovery_medium.name}' 795 ), 796 f'connection_medium: {self._connection_medium.name}', 797 f'upgrade_medium: {self._upgrade_medium_under_test.name}', 798 f'wifi_ssid: {self._wifi_ssid}', 799 f'Start time: {self._start_time}', 800 f'End time: {datetime.datetime.now()}', 801 ]), 802 '05_detailed_stats': '\n'.join(detailed_stats), 803 }, 804 }) 805 806 asserts.assert_true(passed, final_result_message) 807 808 def __get_failed_iteration_messages(self) -> list[str]: 809 stats = [] 810 for test_result in self._test_results: 811 if ( 812 test_result.failure_reason 813 is not nc_constants.SingleTestFailureReason.SUCCESS 814 ): 815 stats.append( 816 f'- Iter: {test_result.test_iteration}: {test_result.start_time}' 817 f' {test_result.result_message}\n' 818 f' sta freq: {test_result.sta_frequency},' 819 f' sta max link speed: {test_result.max_sta_link_speed_mbps},' 820 f' used medium: {test_result.quality_info.get_medium_name()},' 821 f' medium freq: {test_result.quality_info.medium_frequency}.' 822 ) 823 824 if stats: 825 return stats 826 else: 827 return [' - NA'] 828 829 def __get_prior_bt_connection_stats(self) -> list[str]: 830 if not self._use_prior_bt: 831 return [] 832 discovery_latency_stats = self.__get_latency_stats( 833 self._performance_test_metrics.prior_bt_discovery_latencies 834 ) 835 connection_latency_stats = self.__get_latency_stats( 836 self._performance_test_metrics.prior_bt_connection_latencies 837 ) 838 return [ 839 ( 840 ' - Min / Median / Max Discovery Latency' 841 f' ({discovery_latency_stats.success_count} discovery):' 842 f' {discovery_latency_stats.min_val} /' 843 f' {discovery_latency_stats.median_val} /' 844 f' {discovery_latency_stats.max_val}s ' 845 ), 846 ( 847 ' - Min / Median / Max Connection Latency' 848 f' ({connection_latency_stats.success_count} connections):' 849 f' {connection_latency_stats.min_val} /' 850 f' {connection_latency_stats.median_val} /' 851 f' {connection_latency_stats.max_val}s ' 852 ), 853 ] 854 855 def __get_file_transfer_connection_stats(self) -> list[str]: 856 discovery_latency_stats = self.__get_latency_stats( 857 self._performance_test_metrics.file_transfer_discovery_latencies 858 ) 859 connection_latency_stats = self.__get_latency_stats( 860 self._performance_test_metrics.file_transfer_connection_latencies 861 ) 862 transfer_stats = self.__get_transfer_stats( 863 self._performance_test_metrics.file_transfer_throughputs_kbps 864 ) 865 iperf_stats = self.__get_transfer_stats( 866 self._performance_test_metrics.iperf_throughputs_kbps 867 ) 868 stats = [ 869 ( 870 ' - Min / Median / Max Discovery Latency' 871 f' ({discovery_latency_stats.success_count} discovery):' 872 f' {discovery_latency_stats.min_val} /' 873 f' {discovery_latency_stats.median_val} /' 874 f' {discovery_latency_stats.max_val}s ' 875 ), 876 ( 877 ' - Min / Median / Max Connection Latency' 878 f' ({connection_latency_stats.success_count} connections):' 879 f' {connection_latency_stats.min_val} /' 880 f' {connection_latency_stats.median_val} /' 881 f' {connection_latency_stats.max_val}s ' 882 ), 883 ( 884 ' - Min / Median / Max Speed' 885 f' ({transfer_stats.success_count} transfer):' 886 f' {transfer_stats.min_val} / {transfer_stats.median_val} /' 887 f' {transfer_stats.max_val} MBps' 888 ), 889 ( 890 ' - Min / Median / Max iperf Speed' 891 f' ({iperf_stats.success_count} transfer):' 892 f' {iperf_stats.min_val} / {iperf_stats.median_val} /' 893 f' {iperf_stats.max_val} MBps' 894 ), 895 ] 896 if nc_constants.is_high_quality_medium(self._upgrade_medium_under_test): 897 medium_upgrade_latency_stats = self.__get_latency_stats( 898 self._performance_test_metrics.medium_upgrade_latencies 899 ) 900 stats.extend([ 901 ( 902 ' - Min / Median / Max Upgrade Latency ' 903 f' ({medium_upgrade_latency_stats. success_count} upgrade):' 904 f' {medium_upgrade_latency_stats.min_val} /' 905 f' {medium_upgrade_latency_stats.median_val} /' 906 f' {medium_upgrade_latency_stats.max_val}s ' 907 ), 908 ' - Upgrade Medium Stats:', 909 ]) 910 stats.extend(self._summary_upgraded_wifi_transfer_mediums()) 911 912 return stats 913 914 def _summary_upgraded_wifi_transfer_mediums(self) -> list[str]: 915 medium_counts = {} 916 for ( 917 upgraded_medium 918 ) in self._performance_test_metrics.upgraded_wifi_transfer_mediums: 919 if upgraded_medium: 920 medium_counts[upgraded_medium.name] = ( 921 medium_counts.get(upgraded_medium.name, 0) + 1 922 ) 923 return [f' - {name}: {count}' for name, count in medium_counts.items()] 924 925 def __get_device_attributes( 926 self, ad: android_device.AndroidDevice 927 ) -> list[str]: 928 return [ 929 f'Device Serial: {ad.serial}', 930 f'Device Model: {ad.model}', 931 f'Build: {ad.build_info}', 932 f'Wifi chipset: {ad.wifi_chipset}', 933 f'Wifi FW: {ad.adb.getprop("vendor.wlan.firmware.version")}', 934 f'Supports 5G Wifi: {ad.supports_5g}', 935 f'Supports DBS: {ad.supports_dbs_sta_wfd}', 936 ( 937 'Enable STA DFS channel for peer network:' 938 f' {ad.enable_sta_dfs_channel_for_peer_network}' 939 ), 940 ( 941 'Enable STA Indoor channel for peer network:' 942 f' {ad.enable_sta_indoor_channel_for_peer_network}' 943 ), 944 f'Max num of streams: {ad.max_num_streams}', 945 f'Max num of streams (DBS): {ad.max_num_streams_dbs}', 946 f'Android Version: {ad.android_version}', 947 f'GMS_version: {setup_utils.dump_gms_version(ad)}', 948 ] 949