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