• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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