1#!/usr/bin/env python3 2# 3# Copyright 2018 - The Android Open Source Project 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 17import time 18 19import scapy.all as scapy 20 21from acts import asserts 22from acts import utils 23from acts.metrics.loggers.blackbox import BlackboxMetricLogger 24from acts_contrib.test_utils.power import IperfHelper as IPH 25from acts_contrib.test_utils.power import plot_utils 26import acts_contrib.test_utils.power.cellular.cellular_power_base_test as PWCEL 27from acts_contrib.test_utils.tel.tel_logging_utils import start_adb_tcpdump 28from acts_contrib.test_utils.tel.tel_logging_utils import stop_adb_tcpdump 29from acts_contrib.test_utils.tel.tel_logging_utils import get_tcpdump_log 30 31class PowerTelTrafficTest(PWCEL.PowerCellularLabBaseTest): 32 """ Cellular traffic power test. 33 34 Inherits from PowerCellularLabBaseTest. Parses config specific 35 to this kind of test. Contains methods to start data traffic 36 between a local instance of iPerf and one running in the dut. 37 38 """ 39 40 # Keywords for test name parameters 41 PARAM_DIRECTION = 'direction' 42 PARAM_DIRECTION_UL = 'ul' 43 PARAM_DIRECTION_DL = 'dl' 44 PARAM_DIRECTION_DL_UL = 'dlul' 45 PARAM_BANDWIDTH_LIMIT = 'blimit' 46 47 # Iperf waiting time 48 IPERF_MARGIN = 10 49 50 def __init__(self, controllers): 51 """ Class initialization. 52 53 Sets test parameters to initial values. 54 """ 55 56 super().__init__(controllers) 57 58 # These variables are passed to iPerf when starting data 59 # traffic with the -b parameter to limit throughput on 60 # the application layer. 61 self.bandwidth_limit_dl = None 62 self.bandwidth_limit_ul = None 63 64 # Throughput obtained from iPerf 65 self.iperf_results = {} 66 67 # Blackbox metrics loggers 68 69 self.dl_tput_logger = BlackboxMetricLogger.for_test_case( 70 metric_name='avg_dl_tput') 71 self.ul_tput_logger = BlackboxMetricLogger.for_test_case( 72 metric_name='avg_ul_tput') 73 74 def setup_class(self): 75 super().setup_class() 76 77 # Unpack test parameters used in this class 78 self.unpack_userparams(tcp_window_fraction=0, tcp_dumps=False) 79 80 # Verify that at least one PacketSender controller has been initialized 81 if not hasattr(self, 'packet_senders'): 82 raise RuntimeError('At least one packet sender controller needs ' 83 'to be defined in the test config files.') 84 85 def setup_test(self): 86 """ Executed before every test case. 87 88 Parses test configuration from the test name and prepares 89 the simulation for measurement. 90 """ 91 92 # Reset results at the start of the test 93 self.iperf_results = {} 94 95 # Call parent method first to setup simulation 96 if not super().setup_test(): 97 return False 98 99 # Traffic direction 100 101 values = self.consume_parameter(self.PARAM_DIRECTION, 1) 102 103 if not values: 104 self.log.warning("The keyword {} was not included in the testname " 105 "parameters. Setting to {} by default.".format( 106 self.PARAM_DIRECTION, 107 self.PARAM_DIRECTION_DL_UL)) 108 self.traffic_direction = self.PARAM_DIRECTION_DL_UL 109 elif values[1] in [ 110 self.PARAM_DIRECTION_DL, self.PARAM_DIRECTION_UL, 111 self.PARAM_DIRECTION_DL_UL 112 ]: 113 self.traffic_direction = values[1] 114 else: 115 self.log.error("The test name has to include parameter {} " 116 "followed by {}/{}/{}.".format( 117 self.PARAM_DIRECTION, self.PARAM_DIRECTION_UL, 118 self.PARAM_DIRECTION_DL, 119 self.PARAM_DIRECTION_DL_UL)) 120 return False 121 122 # Bandwidth limit 123 124 values = self.consume_parameter(self.PARAM_BANDWIDTH_LIMIT, 2) 125 126 if values: 127 self.bandwidth_limit_dl = values[1] 128 self.bandwidth_limit_ul = values[2] 129 else: 130 self.bandwidth_limit_dl = 0 131 self.bandwidth_limit_ul = 0 132 self.log.error( 133 "No bandwidth limit was indicated in the test parameters. " 134 "Setting to default value of 0 (no limit to bandwidth). To set " 135 "a different value include parameter '{}' followed by two " 136 "strings indicating downlink and uplink bandwidth limits for " 137 "iPerf.".format(self.PARAM_BANDWIDTH_LIMIT)) 138 139 # No errors when parsing parameters 140 return True 141 142 def teardown_test(self): 143 """Tear down necessary objects after test case is finished. 144 145 """ 146 147 super().teardown_test() 148 149 # Log the throughput values to Blackbox 150 self.dl_tput_logger.metric_value = self.iperf_results.get('DL', 0) 151 self.ul_tput_logger.metric_value = self.iperf_results.get('UL', 0) 152 153 # Log the throughput values to Spanner 154 self.power_logger.set_dl_tput(self.iperf_results.get('DL', 0)) 155 self.power_logger.set_ul_tput(self.iperf_results.get('UL', 0)) 156 157 try: 158 dl_max_throughput = self.simulation.maximum_downlink_throughput() 159 ul_max_throughput = self.simulation.maximum_uplink_throughput() 160 self.power_logger.set_dl_tput_threshold(dl_max_throughput) 161 self.power_logger.set_ul_tput_threshold(ul_max_throughput) 162 except NotImplementedError as e: 163 self.log.error("%s Downlink/uplink thresholds will not be " 164 "logged in the power proto" % e) 165 166 for ips in self.iperf_servers: 167 ips.stop() 168 169 def power_tel_traffic_test(self): 170 """ Measures power and throughput during data transmission. 171 172 Measurement step in this test. Starts iPerf client in the DUT and then 173 initiates power measurement. After that, DUT is connected again and 174 the result from iPerf is collected. Pass or fail is decided with a 175 threshold value. 176 """ 177 178 # Start data traffic 179 iperf_helpers = self.start_tel_traffic(self.dut) 180 181 # Measure power 182 self.collect_power_data() 183 184 # Wait for iPerf to finish 185 time.sleep(self.IPERF_MARGIN + 2) 186 187 # Collect throughput measurement 188 self.iperf_results = self.get_iperf_results(self.dut, iperf_helpers) 189 190 # Check if power measurement is below the required value 191 self.pass_fail_check(self.avg_current) 192 193 return self.avg_current, self.iperf_results 194 195 def get_iperf_results(self, device, iperf_helpers): 196 """ Pulls iperf results from the device. 197 198 Args: 199 device: the device from which iperf results need to be pulled. 200 201 Returns: 202 a dictionary containing DL/UL throughput in Mbit/s. 203 """ 204 205 # Pull TCP logs if enabled 206 if self.tcp_dumps: 207 self.log.info('Pulling TCP dumps.') 208 stop_adb_tcpdump(self.dut) 209 get_tcpdump_log(self.dut) 210 211 throughput = {} 212 213 for iph in iperf_helpers: 214 215 self.log.info("Getting {} throughput results.".format( 216 iph.traffic_direction)) 217 218 iperf_result = iph.process_iperf_results(device, self.log, 219 self.iperf_servers, 220 self.test_name) 221 222 throughput[iph.traffic_direction] = iperf_result 223 224 return throughput 225 226 def check_throughput_results(self, iperf_results): 227 """ Checks throughput results. 228 229 Compares the obtained throughput with the expected value 230 provided by the simulation class. 231 232 """ 233 234 for direction, throughput in iperf_results.items(): 235 try: 236 if direction == "UL": 237 expected_t = self.simulation.maximum_uplink_throughput() 238 elif direction == "DL": 239 expected_t = self.simulation.maximum_downlink_throughput() 240 else: 241 raise RuntimeError("Unexpected traffic direction value.") 242 except NotImplementedError: 243 # Some simulation classes might not have implemented the max 244 # throughput calculation yet. 245 self.log.debug("Expected throughput is not available for the " 246 "current simulation class.") 247 else: 248 249 self.log.info( 250 "The expected {} throughput is {} Mbit/s.".format( 251 direction, expected_t)) 252 asserts.assert_true( 253 0.90 < throughput / expected_t < 1.10, 254 "{} throughput differed more than 10% from the expected " 255 "value! ({}/{} = {})".format( 256 direction, round(throughput, 3), round(expected_t, 3), 257 round(throughput / expected_t, 3))) 258 259 def pass_fail_check(self, average_current=None): 260 """ Checks power consumption and throughput. 261 262 Uses the base class method to check power consumption. Also, compares 263 the obtained throughput with the expected value provided by the 264 simulation class. 265 266 """ 267 self.check_throughput_results(self.iperf_results) 268 super().pass_fail_check(average_current) 269 270 def start_tel_traffic(self, client_host): 271 """ Starts iPerf in the indicated device and initiates traffic. 272 273 Starts the required iperf clients and servers according to the traffic 274 pattern config in the current test. 275 276 Args: 277 client_host: device handler in which to start the iperf client. 278 279 Returns: 280 A list of iperf helpers. 281 """ 282 # The iPerf server is hosted in this computer 283 self.iperf_server_address = scapy.get_if_addr( 284 self.packet_senders[0].interface) 285 286 self.log.info('Testing IP connectivity with ping.') 287 if not utils.adb_shell_ping( 288 client_host, count=10, dest_ip=self.iperf_server_address): 289 raise RuntimeError('Ping between DUT and host failed.') 290 291 # Start iPerf traffic 292 iperf_helpers = [] 293 294 # If the tcp_window_fraction parameter was set, calculate the TCP 295 # window size as a fraction of the peak throughput. 296 ul_tcp_window = None 297 dl_tcp_window = None 298 if self.tcp_window_fraction == 0: 299 self.log.info("tcp_window_fraction was not indicated. " 300 "Disabling fixed TCP window.") 301 else: 302 try: 303 max_dl_tput = self.simulation.maximum_downlink_throughput() 304 max_ul_tput = self.simulation.maximum_uplink_throughput() 305 dl_tcp_window = max_dl_tput / self.tcp_window_fraction 306 ul_tcp_window = max_ul_tput / self.tcp_window_fraction 307 except NotImplementedError: 308 self.log.error("Maximum downlink/uplink throughput method not " 309 "implemented for %s." % 310 type(self.simulation).__name__) 311 312 if self.traffic_direction in [ 313 self.PARAM_DIRECTION_DL, self.PARAM_DIRECTION_DL_UL 314 ]: 315 # Downlink traffic 316 iperf_helpers.append( 317 self.start_iperf_traffic(client_host, 318 server_idx=len(iperf_helpers), 319 traffic_direction='DL', 320 window=dl_tcp_window, 321 bandwidth=self.bandwidth_limit_dl)) 322 323 if self.traffic_direction in [ 324 self.PARAM_DIRECTION_UL, self.PARAM_DIRECTION_DL_UL 325 ]: 326 # Uplink traffic 327 iperf_helpers.append( 328 self.start_iperf_traffic(client_host, 329 server_idx=len(iperf_helpers), 330 traffic_direction='UL', 331 window=ul_tcp_window, 332 bandwidth=self.bandwidth_limit_ul)) 333 334 # Enable TCP logger. 335 if self.tcp_dumps: 336 self.log.info('Enabling TCP logger.') 337 start_adb_tcpdump(self.dut) 338 339 return iperf_helpers 340 341 def start_iperf_traffic(self, 342 client_host, 343 server_idx, 344 traffic_direction, 345 bandwidth=0, 346 window=None): 347 """Starts iPerf data traffic. 348 349 Starts an iperf client in an android device and a server locally. 350 351 Args: 352 client_host: device handler in which to start the iperf client 353 server_idx: id of the iperf server to connect to 354 traffic_direction: has to be either 'UL' or 'DL' 355 bandwidth: bandwidth limit for data traffic 356 window: the tcp window. if None, no window will be passed to iperf 357 358 Returns: 359 An IperfHelper object for the started client/server pair. 360 """ 361 362 # Start the server locally 363 self.iperf_servers[server_idx].start() 364 365 config = { 366 'traffic_type': 'TCP', 367 'duration': 368 self.mon_duration + self.mon_offset + self.IPERF_MARGIN, 369 'start_meas_time': 4, 370 'server_idx': server_idx, 371 'port': self.iperf_servers[server_idx].port, 372 'traffic_direction': traffic_direction, 373 'window': window 374 } 375 376 # If bandwidth is equal to zero then no bandwidth requirements are set 377 if bandwidth > 0: 378 config['bandwidth'] = bandwidth 379 380 iph = IPH.IperfHelper(config) 381 382 # Start the client in the android device 383 client_host.adb.shell_nb( 384 "nohup >/dev/null 2>&1 sh -c 'iperf3 -c {} {} " 385 "&'".format(self.iperf_server_address, iph.iperf_args)) 386 387 self.log.info('{} iPerf started on port {}.'.format( 388 traffic_direction, iph.port)) 389 390 return iph 391 392 393class PowerTelRvRTest(PowerTelTrafficTest): 394 """ Gets Range vs Rate curves while measuring power consumption. 395 396 Uses PowerTelTrafficTest as a base class. 397 """ 398 399 # Test name configuration keywords 400 PARAM_SWEEP = "sweep" 401 PARAM_SWEEP_UPLINK = "uplink" 402 PARAM_SWEEP_DOWNLINK = "downlink" 403 404 # Sweep values. Need to be set before starting test by test 405 # function or child class. 406 downlink_power_sweep = None 407 uplink_power_sweep = None 408 409 def setup_test(self): 410 """ Executed before every test case. 411 412 Parses test configuration from the test name and prepares 413 the simulation for measurement. 414 """ 415 416 # Call parent method first to setup simulation 417 if not super().setup_test(): 418 return False 419 420 # Get which power value to sweep from config 421 422 try: 423 values = self.consume_parameter(self.PARAM_SWEEP, 1) 424 425 if values[1] == self.PARAM_SWEEP_UPLINK: 426 self.sweep = self.PARAM_SWEEP_UPLINK 427 elif values[1] == self.PARAM_SWEEP_DOWNLINK: 428 self.sweep = self.PARAM_SWEEP_DOWNLINK 429 else: 430 raise ValueError() 431 except: 432 self.log.error( 433 "The test name has to include parameter {} followed by " 434 "either {} or {}.".format(self.PARAM_SWEEP, 435 self.PARAM_SWEEP_DOWNLINK, 436 self.PARAM_SWEEP_UPLINK)) 437 return False 438 439 return True 440 441 def power_tel_rvr_test(self): 442 """ Main function for the RvR test. 443 444 Produces the RvR curve according to the indicated sweep values. 445 """ 446 447 if self.sweep == self.PARAM_SWEEP_DOWNLINK: 448 sweep_range = self.downlink_power_sweep 449 elif self.sweep == self.PARAM_SWEEP_UPLINK: 450 sweep_range = self.uplink_power_sweep 451 452 current = [] 453 throughput = [] 454 455 for pw in sweep_range: 456 457 if self.sweep == self.PARAM_SWEEP_DOWNLINK: 458 self.simulation.set_downlink_rx_power(self.simulation.bts1, pw) 459 elif self.sweep == self.PARAM_SWEEP_UPLINK: 460 self.simulation.set_uplink_tx_power(self.simulation.bts1, pw) 461 462 i, t = self.power_tel_traffic_test() 463 self.log.info("---------------------") 464 self.log.info("{} -- {} --".format(self.sweep, pw)) 465 self.log.info("{} ----- {}".format(i, t[0])) 466 self.log.info("---------------------") 467 468 current.append(i) 469 throughput.append(t[0]) 470 471 print(sweep_range) 472 print(current) 473 print(throughput) 474 475 476class PowerTelTxPowerSweepTest(PowerTelTrafficTest): 477 """ Gets Average Current vs Tx Power plot. 478 479 Uses PowerTelTrafficTest as a base class. 480 """ 481 482 # Test config keywords 483 KEY_TX_STEP = 'step' 484 KEY_UP_TOLERANCE = 'up_tolerance' 485 KEY_DOWN_TOLERANCE = 'down_tolerance' 486 487 # Test name parameters 488 PARAM_TX_POWER_SWEEP = 'sweep' 489 490 def setup_class(self): 491 super().setup_class() 492 self.unpack_userparams( 493 [self.KEY_TX_STEP, self.KEY_UP_TOLERANCE, self.KEY_DOWN_TOLERANCE]) 494 495 def setup_test(self): 496 """ Executed before every test case. 497 498 Parses test configuration from the test name and prepares 499 the simulation for measurement. 500 """ 501 # Call parent method first to setup simulation 502 if not super().setup_test(): 503 return False 504 505 # Determine power range to sweep from test case params 506 try: 507 values = self.consume_parameter(self.PARAM_TX_POWER_SWEEP, 2) 508 509 if len(values) == 3: 510 self.start_dbm = int(values[1].replace('n', '-')) 511 self.end_dbm = int(values[2].replace('n', '-')) 512 else: 513 raise ValueError('Not enough params specified for sweep.') 514 except ValueError as e: 515 self.log.error("Unable to parse test param sweep: {}".format(e)) 516 return False 517 518 return True 519 520 def pass_fail_check(self, currents, txs, iperf_results): 521 """ Compares the obtained throughput with the expected 522 value provided by the simulation class. Also, ensures 523 consecutive currents do not increase or decrease beyond 524 specified tolerance 525 """ 526 for iperf_result in iperf_results: 527 self.check_throughput_results(iperf_result) 528 529 # x = reference current value, y = next current value, i = index of x 530 for i, (x, y) in enumerate(zip(currents[::], currents[1::])): 531 measured_change = (y - x) / x * 100 532 asserts.assert_true( 533 -self.down_tolerance < measured_change < self.up_tolerance, 534 "Current went from {} to {} ({}%) between {} dBm and {} dBm. " 535 "Tolerance range: -{}% to {}%".format(x, y, measured_change, 536 txs[i], txs[i + 1], 537 self.down_tolerance, 538 self.up_tolerance)) 539 540 def create_power_plot(self, currents, txs): 541 """ Creates average current vs tx power plot 542 """ 543 title = '{}_{}_{}_tx_power_sweep'.format( 544 self.test_name, self.dut.model, self.dut.build_info['build_id']) 545 546 plot_utils.monsoon_tx_power_sweep_plot(self.mon_info.data_path, title, 547 currents, txs) 548 549 def power_tel_tx_sweep(self): 550 """ Main function for the Tx power sweep test. 551 552 Produces a plot of power consumption vs tx power 553 """ 554 currents = [] 555 txs = [] 556 iperf_results = [] 557 for tx in range(self.start_dbm, self.end_dbm + 1, self.step): 558 559 self.simulation.set_uplink_tx_power(tx) 560 561 iperf_helpers = self.start_tel_traffic(self.dut) 562 563 # Measure power 564 self.collect_power_data() 565 566 # Wait for iPerf to finish 567 time.sleep(self.IPERF_MARGIN + 2) 568 569 # Collect and check throughput measurement 570 iperf_result = self.get_iperf_results(self.dut, iperf_helpers) 571 572 currents.append(self.avg_current) 573 574 # Get the actual Tx power as measured from the callbox side 575 measured_tx = self.simulation.get_measured_ul_power() 576 577 txs.append(measured_tx) 578 iperf_results.append(iperf_result) 579 580 self.create_power_plot(currents, txs) 581 self.pass_fail_check(currents, txs, iperf_results) 582