• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright (c) 2015 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5from __future__ import absolute_import
6
7import array
8import logging
9import multiprocessing
10import struct
11import unittest
12from unittest.mock import patch
13
14import common
15
16from autotest_lib.client.cros.cellular.mbim_compliance import mbim_channel
17from autotest_lib.client.cros.cellular.mbim_compliance import mbim_errors
18
19
20class MBIMChannelTestCase(unittest.TestCase):
21    """ Test cases for the MBIMChannel class. """
22
23    def setUp(self):
24        # Arguments passed to MBIMChannel. Irrelevant for these tests, mostly.
25        self._device = None
26        self._interface_number = 0
27        self._interrupt_endpoint_address = 0x01
28        self._in_buffer_size = 100
29
30        self._setup_mock_subprocess()
31
32        patcher = patch('multiprocessing.Queue')
33        self._mock_request_queue = patcher.start()
34        self.addCleanup(patcher.stop)
35
36        self._channel._request_queue = self._mock_request_queue
37
38        # On the other hand, just grab the real response queue.
39        self._response_queue = self._channel._response_queue
40
41        # Decrease timeouts to small values to speed up tests.
42        self._channel.FRAGMENT_TIMEOUT_S = 0.2
43        self._channel.TRANSACTION_TIMEOUT_S = 0.5
44
45
46    def tearDown(self):
47        self._channel.close()
48
49
50    def _setup_mock_subprocess(self):
51        """
52        Setup long-term expectations on the mocked out subprocess.
53
54        These expectations are only met when |self._channel.close| is called in
55        |tearDown|.
56
57        """
58        patcher = patch.object(multiprocessing, 'Process')
59        mock_process = patcher.start()
60        self.addCleanup(patcher.stop)
61        mock_process.return_value = mock_process
62
63        # Each API call into MBIMChannel results in an aliveness ping to the
64        # subprocess.
65        # Finally, when |self._channel| is destructed, it will attempt to
66        # terminate the |mock_process|, with increasingly drastic actions.
67        mock_process.is_alive.return_value = True
68
69        self._channel = mbim_channel.MBIMChannel(
70                self._device,
71                self._interface_number,
72                self._interrupt_endpoint_address,
73                self._in_buffer_size,
74                mock_process)
75
76
77    def test_creation(self):
78        """ A trivial test that we mocked out the |Process| class correctly. """
79        self._setup_mock_subprocess()
80
81
82    def test_unfragmented_packet_successful(self):
83        """ Test that we can synchronously send an unfragmented packet. """
84        packet = self._get_unfragmented_packet(1)
85        response_packet = self._get_unfragmented_packet(1)
86        self._expect_transaction([packet], [response_packet])
87        self._verify_transaction_successful([packet], [response_packet])
88
89
90    def test_unfragmented_packet_timeout(self):
91        """ Test the case when an unfragmented packet receives no response. """
92        packet = self._get_unfragmented_packet(1)
93        self._expect_transaction([packet])
94        self._verify_transaction_failed([packet])
95
96
97    def test_single_fragment_successful(self):
98        """ Test that we can synchronously send a fragmented packet. """
99        fragment = self._get_fragment(1, 1, 0)
100        response_fragment = self._get_fragment(1, 1, 0)
101        self._expect_transaction([fragment], [response_fragment])
102        self._verify_transaction_successful([fragment], [response_fragment])
103
104
105    def test_single_fragment_timeout(self):
106        """ Test the case when a fragmented packet receives no response. """
107        fragment = self._get_fragment(1, 1, 0)
108        self._expect_transaction([fragment])
109        self._verify_transaction_failed([fragment])
110
111
112    def test_single_fragment_corrupted_reply(self):
113        """ Test the case when the response has a corrupted fragment header. """
114        fragment = self._get_fragment(1, 1, 0)
115        response_fragment = self._get_fragment(1, 1, 0)
116        response_fragment = response_fragment[:len(response_fragment)-1]
117        self._expect_transaction([fragment], [response_fragment])
118        self._verify_transaction_failed([fragment])
119
120
121    def test_multiple_fragments_successful(self):
122        """ Test that we can send/recieve multi-fragment packets. """
123        fragment_0 = self._get_fragment(1, 2, 0)
124        fragment_1 = self._get_fragment(1, 2, 1)
125        response_fragment_0 = self._get_fragment(1, 2, 0)
126        response_fragment_1 = self._get_fragment(1, 2, 1)
127        self._expect_transaction([fragment_0, fragment_1],
128                                 [response_fragment_0, response_fragment_1])
129        self._verify_transaction_successful(
130                [fragment_0, fragment_1],
131                [response_fragment_0, response_fragment_1])
132
133
134    def test_multiple_fragments_incorrect_total_fragments(self):
135        """ Test the case when one of the fragment reports incorrect total. """
136        fragment = self._get_fragment(1, 1, 0)
137        response_fragment_0 = self._get_fragment(1, 2, 0)
138        # total_fragment should have been 2, but is 99.
139        response_fragment_1 = self._get_fragment(1, 99, 1)
140        self._expect_transaction([fragment],
141                                 [response_fragment_0, response_fragment_1])
142        self._verify_transaction_failed([fragment])
143
144
145    def test_multiple_fragments_reordered_reply_1(self):
146        """ Test the case when the first fragemnt reports incorrect index. """
147        fragment = self._get_fragment(1, 1, 0)
148        # Incorrect first fragment number.
149        response_fragment = self._get_fragment(1, 2, 1)
150        self._expect_transaction([fragment], [response_fragment])
151        self._verify_transaction_failed([fragment])
152
153
154    def test_multiple_fragments_reordered_reply_2(self):
155        """ Test the case when a follow up fragment reports incorrect index. """
156        fragment = self._get_fragment(1, 1, 0)
157        response_fragment_0 = self._get_fragment(1, 2, 0)
158        # Incorrect second fragment number.
159        response_fragment_1 = self._get_fragment(1, 2, 99)
160        self._expect_transaction([fragment],
161                                 [response_fragment_0, response_fragment_1])
162        self._verify_transaction_failed([fragment])
163
164
165    def test_multiple_fragments_insufficient_reply_timeout(self):
166        """ Test the case when we recieve only part of the response. """
167        fragment = self._get_fragment(1, 1, 0)
168        # The second fragment will never arrive.
169        response_fragment_0 = self._get_fragment(1, 2, 0)
170        self._expect_transaction([fragment], [response_fragment_0])
171        self._verify_transaction_successful([fragment], [response_fragment_0])
172
173
174    def test_unfragmented_packet_notification(self):
175        """ Test the case when a notification comes before the response. """
176        packet = self._get_unfragmented_packet(1)
177        response = self._get_unfragmented_packet(1)
178        notification = self._get_unfragmented_packet(0)
179        self._expect_transaction([packet], [notification, response])
180        self._verify_transaction_successful([packet], [response])
181        self.assertEqual([[notification]],
182                         self._channel.get_outstanding_packets())
183
184
185    def test_fragmented_notification(self):
186        """ Test the case when a fragmented notification preceeds response. """
187        packet_fragment_0 = self._get_fragment(1, 2, 0)
188        packet_fragment_1 = self._get_fragment(1, 2, 1)
189        response_fragment_0 = self._get_fragment(1, 2, 0)
190        response_fragment_1 = self._get_fragment(1, 2, 1)
191        notification_0_fragment_0 = self._get_fragment(0, 2, 0)
192        notification_0_fragment_1 = self._get_fragment(0, 2, 1)
193        notification_1_fragment_0 = self._get_fragment(99, 2, 0)
194        notification_1_fragment_1 = self._get_fragment(99, 2, 1)
195
196        self._expect_transaction(
197                [packet_fragment_0, packet_fragment_1],
198                [notification_0_fragment_0, notification_0_fragment_1,
199                 notification_1_fragment_0, notification_1_fragment_1,
200                 response_fragment_0, response_fragment_1])
201        self._verify_transaction_successful(
202                [packet_fragment_0, packet_fragment_1],
203                [response_fragment_0, response_fragment_1])
204        self.assertEqual(
205                [[notification_0_fragment_0, notification_0_fragment_1],
206                 [notification_1_fragment_0, notification_1_fragment_1]],
207                self._channel.get_outstanding_packets())
208
209
210    def test_multiple_packets_rollover_notification(self):
211        """
212        Test the case when we receive incomplete response, followed by
213        fragmented notifications.
214
215        We have to be smart enough to realize that the incorrect fragment
216        recieved at the end of the response belongs to the next notification
217        instead.
218
219        """
220        packet = self._get_fragment(1, 1, 0)
221        # The second fragment never comes, instead we get a notification
222        # fragment.
223        response_fragment_0 = self._get_fragment(1, 2, 0)
224        notification_0_fragment_0 = self._get_fragment(0, 2, 0)
225        notification_0_fragment_1 = self._get_fragment(0, 2, 1)
226        notification_1_fragment_0 = self._get_fragment(99, 2, 0)
227        notification_1_fragment_1 = self._get_fragment(99, 2, 1)
228
229        self._expect_transaction(
230                [packet],
231                [response_fragment_0,
232                 notification_0_fragment_0, notification_0_fragment_1,
233                 notification_1_fragment_0, notification_1_fragment_1])
234        self._verify_transaction_successful(
235                [packet],
236                [response_fragment_0])
237        self.assertEqual(
238                [[notification_0_fragment_0, notification_0_fragment_1],
239                 [notification_1_fragment_0, notification_1_fragment_1]],
240                self._channel.get_outstanding_packets())
241
242
243    def test_data(self):
244        """ Test that data is transferred transaperntly. """
245        packet = self._get_unfragmented_packet(1)
246        packet.fromlist([0xFF, 0xFF, 0xFF, 0xFF, 0xDD, 0xDD, 0xDD, 0xDD])
247        response_packet = self._get_unfragmented_packet(1)
248        response_packet.fromlist([0xAA, 0xAA, 0xBB, 0xBB])
249        self._expect_transaction([packet], [response_packet])
250        self._verify_transaction_successful([packet], [response_packet])
251
252
253    def test_flush_successful(self):
254        """ Test that flush clears all queues. """
255        packet = self._get_unfragmented_packet(1)
256        response = self._get_unfragmented_packet(1)
257        notification_1 = self._get_fragment(0, 1, 0)
258        self._response_queue.put_nowait(notification_1)
259        self._mock_request_queue.qsize.return_value = 1
260        self._mock_request_queue.empty.return_value = False
261
262        def put_response():
263            """Side effect for mock"""
264            self._response_queue.put_nowait(response)
265
266        self._mock_request_queue.empty.side_effect = [None, put_response]
267        self._channel.flush()
268        self.assertEqual(0, self._response_queue.qsize())
269
270
271    def test_flush_failed(self):
272        """ Test the case when the request queue fails to empty out. """
273        packet = self._get_unfragmented_packet(1)
274        self._mock_request_queue.qsize.return_value = 1
275        self._mock_request_queue.empty.return_value = False
276        self.assertRaises(
277                mbim_errors.MBIMComplianceChannelError,
278                self._channel.flush)
279
280
281    def _queue_responses(self, responses):
282        """ Helper method for |_expect_transaction|. Do not use directly. """
283        for response in responses:
284            self._response_queue.put_nowait(response)
285
286
287    def _expect_transaction(self, requests, responses=None):
288        """
289        Helper method to setup expectations on the queues.
290
291        @param requests: A list of packets to expect on the |_request_queue|.
292        @param respones: An optional list of packets to respond with after the
293                last request.
294
295        """
296
297        last_request = requests[len(requests) - 1]
298        if responses:
299
300            def put_if_last_request(msg):
301                """Side effect for mock"""
302                if msg == last_request:
303                    self._queue_responses(responses)
304
305            self._mock_request_queue.put_nowait.side_effect = put_if_last_request
306
307
308    def _verify_transaction_successful(self, requests, responses):
309        """
310        Helper method to assert that the transaction was successful.
311
312        @param requests: List of packets sent.
313        @param responses: List of packets expected back.
314        """
315        self.assertEqual(responses,
316                         self._channel.bidirectional_transaction(*requests))
317
318
319    def _verify_transaction_failed(self, requests):
320        """
321        Helper method to assert that the transaction failed.
322
323        @param requests: List of packets sent.
324
325        """
326        self.assertRaises(mbim_errors.MBIMComplianceChannelError,
327                          self._channel.bidirectional_transaction,
328                          *requests)
329
330
331    def _get_unfragmented_packet(self, transaction_id):
332        """ Creates a packet that has no fragment header. """
333        packet_format = '<LLL' # This does not contain a fragment header.
334        packet = self._create_buffer(struct.calcsize(packet_format))
335        struct.pack_into(packet_format,
336                         packet,
337                         0,
338                         0x00000000,  # 0x0 does not need fragments.
339                         struct.calcsize(packet_format),
340                         transaction_id)
341        return packet
342
343
344    def _get_fragment(self, transaction_id, total_fragments, current_fragment):
345        """ Creates a fragment with the given fields. """
346        fragment_header_format = '<LLLLL'
347        message_type = 0x00000003  # MBIM_COMMAND_MSG has fragments.
348        fragment = self._create_buffer(struct.calcsize(fragment_header_format))
349        struct.pack_into(fragment_header_format,
350                         fragment,
351                         0,
352                         message_type,
353                         struct.calcsize(fragment_header_format),
354                         transaction_id,
355                         total_fragments,
356                         current_fragment)
357        return fragment
358
359
360    def _create_buffer(self, size):
361        """ Create an array of the give size initialized to 0x00. """
362        return array.array('B', b'\x00' * size)
363
364
365if __name__ == '__main__':
366    logging.basicConfig(level=logging.DEBUG)
367    unittest.main()
368