1# Copyright (c) 2013 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 uuid
6import xml.etree.ElementTree as ET
7
8from autotest_lib.client.common_lib import error
9from autotest_lib.server.cros.bluetooth import bluetooth_test
10
11class bluetooth_SDP_ServiceAttributeRequest(bluetooth_test.BluetoothTest):
12    """
13    Verify the correct behaviour of the device when searching for attributes of
14    services.
15    """
16    version = 1
17
18    MAX_REC_CNT                      = 3
19    MAX_ATTR_BYTE_CNT                = 300
20
21    SDP_SERVER_CLASS_ID              = 0x1000
22    SERVICE_RECORD_HANDLE_ATTR_ID    = 0x0000
23
24    GAP_CLASS_ID                     = 0x1800
25    BROWSE_GROUP_LIST_ATTR_ID        = 0x0005
26    PUBLIC_BROWSE_ROOT               = 0x1002
27
28    BLUEZ_URL                        = 'http://www.bluez.org/'
29    DOCUMENTATION_URL_ATTR_ID        = 0x000A
30    CLIENT_EXECUTABLE_URL_ATTR_ID    = 0x000B
31    ICON_URL_ATTR_ID                 = 0x000C
32
33    PROTOCOL_DESCRIPTOR_LIST_ATTR_ID = 0x0004
34    L2CAP_UUID                       = 0x0100
35    ATT_UUID                         = 0x0007
36
37    ATT_PSM                          = 0x001F
38
39    PNP_INFORMATION_CLASS_ID         = 0x1200
40    MIN_ATTR_BYTE_CNT                = 7
41
42    VERSION_NUMBER_LIST_ATTR_ID      = 0x0200
43    SERVICE_DATABASE_STATE_ATTR_ID   = 0x0201
44
45    AVRCP_TG_CLASS_ID                = 0x110C
46    PROFILE_DESCRIPTOR_LIST_ATTR_ID  = 0x0009
47    ADDITIONAL_PROTOCOLLIST_ATTR_ID  = 0x000D
48
49    FAKE_SERVICE_PATH                = '/autotest/fake_service'
50    FAKE_SERVICE_CLASS_ID            = 0xCDEF
51    FAKE_ATTRIBUTE_VALUE             = 42
52    LANGUAGE_BASE_ATTRIBUTE_ID       = 0x0006
53    FAKE_GENERAL_ATTRIBUTE_IDS       = [
54                                        0x0003, # TP/SERVER/SA/BV-04-C
55                                        0x0002, # TP/SERVER/SA/BV-06-C
56                                        0x0007, # TP/SERVER/SA/BV-07-C
57                                        0x0008, # TP/SERVER/SA/BV-10-C
58                                        # TP/SERVER/SA/BV-09-C:
59                                        LANGUAGE_BASE_ATTRIBUTE_ID
60                                       ]
61    FAKE_LANGUAGE_ATTRIBUTE_OFFSETS  = [
62                                        0x0000, # TP/SERVER/SA/BV-12-C
63                                        0x0001, # TP/SERVER/SA/BV-13-C
64                                        0x0002  # TP/SERVER/SA/BV-14-C
65                                       ]
66    NON_EXISTING_ATTRIBUTE_ID        = 0xFEDC
67    BLUETOOTH_BASE_UUID              = 0x0000000000001000800000805F9B34FB
68    SERVICE_CLASS_ID_ATTR_ID         = 0x0001
69
70    ERROR_CODE_INVALID_RECORD_HANDLE = 0x0002
71    ERROR_CODE_INVALID_SYNTAX        = 0x0003
72    ERROR_CODE_INVALID_PDU_SIZE      = 0x0004
73    INVALID_RECORD_HANDLE            = 0xFEEE
74    INVALID_SYNTAX_REQUEST           = '123'
75    INVALID_PDU_SIZE                 = 11
76
77    @staticmethod
78    def assert_equal(actual, expected):
79        """Verify that |actual| is equal to |expected|.
80
81        @param actual: The value we got.
82        @param expected: The value we expected.
83        @raise error.TestFail: If the values are unequal.
84        """
85        if actual != expected:
86            raise error.TestFail(
87                'Expected |%s|, got |%s|' % (expected, actual))
88
89
90    @staticmethod
91    def assert_nonempty_list(value):
92        """Verify that |value| is a list, and that the list is non-empty.
93
94        @param value: The value to check.
95        @raise error.TestFail: If the value is not a list, or is empty.
96        """
97        if not isinstance(value, list):
98            raise error.TestFail('Value is not a list. Got |%s|.' % value)
99
100        if value == []:
101            raise error.TestFail('List is empty')
102
103
104    def get_single_handle(self, class_id):
105        """Send a Service Search Request to get a handle for specific class ID.
106
107        @param class_id: The class that we want a handle for.
108        @return The record handle, as an int.
109        @raise error.TestFail: If we failed to retrieve a handle.
110        """
111        res = self.tester.service_search_request([class_id], self.MAX_REC_CNT)
112        if not (isinstance(res, list) and len(res) > 0):
113            raise error.TestFail(
114                    'Failed to retrieve handle for 0x%x' % class_id)
115        return res[0]
116
117
118    # TODO(quiche): Place this after get_attribute(), so all the tests are
119    # grouped together.
120    def test_record_handle_attribute(self):
121        """Implementation of test TP/SERVER/SA/BV-01-C from SDP Specification.
122
123        @raise error.TestFail: If the DUT failed the test.
124        """
125        # Send Service Search Request to find out record handle for
126        # SDP Server service.
127        record_handle = self.get_single_handle(self.SDP_SERVER_CLASS_ID)
128
129        # Send Service Attribute Request for Service Record Handle Attribute.
130        res = self.tester.service_attribute_request(
131                  record_handle,
132                  self.MAX_ATTR_BYTE_CNT,
133                  [self.SERVICE_RECORD_HANDLE_ATTR_ID])
134
135        # Ensure that returned attribute is correct.
136        self.assert_equal(res,
137                          [self.SERVICE_RECORD_HANDLE_ATTR_ID, record_handle])
138
139
140    def get_attribute(self, class_id, attr_id):
141        """Get a single attribute of a single service
142
143        @param class_id: Class ID of service to check.
144        @param attr_id: ID of attribute to check.
145        @return attribute value if attribute exists, None otherwise
146
147        """
148        record_handle = self.get_single_handle(class_id)
149        res = self.tester.service_attribute_request(
150                  record_handle, self.MAX_ATTR_BYTE_CNT, [attr_id])
151        if isinstance(res, list) and len(res) == 2 and res[0] == attr_id:
152            return res[1]
153        return None
154
155
156    # TODO(quiche): Move this up, to be grouped with the other |assert|
157    # methods.
158    def assert_attribute_equals(self, class_id, attr_id, expected_value):
159        """Verify that |attr_id| of service with |class_id| has |expected_value|
160
161        @param class_id: Class ID of service to check.
162        @param attr_id: ID of attribute to check.
163        @param expected_value: The expected value for the attribute.
164        @raise error.TestFail: If the actual value differs from |expected_value|
165        """
166        self.assert_equal(self.get_attribute(class_id, attr_id),
167                          expected_value)
168
169
170    def test_browse_group_attribute(self):
171        """Implementation of test TP/SERVER/SA/BV-08-C from SDP Specification.
172
173        @raise error.TestFail: If the DUT failed the test.
174        """
175        self.assert_attribute_equals(self.GAP_CLASS_ID,
176                                     self.BROWSE_GROUP_LIST_ATTR_ID,
177                                     [self.PUBLIC_BROWSE_ROOT])
178
179
180    def test_icon_url_attribute(self):
181        """Implementation of test TP/SERVER/SA/BV-11-C from SDP Specification.
182
183        @raise error.TestFail: If the DUT failed the test.
184        """
185        self.assert_attribute_equals(self.GAP_CLASS_ID,
186                                     self.ICON_URL_ATTR_ID,
187                                     self.BLUEZ_URL)
188
189
190    def test_documentation_url_attribute(self):
191        """Implementation of test TP/SERVER/SA/BV-18-C from SDP Specification.
192
193        @raise error.TestFail: If the DUT failed the test.
194        """
195        self.assert_attribute_equals(self.GAP_CLASS_ID,
196                                     self.DOCUMENTATION_URL_ATTR_ID,
197                                     self.BLUEZ_URL)
198
199
200    def test_client_executable_url_attribute(self):
201        """Implementation of test TP/SERVER/SA/BV-19-C from SDP Specification.
202
203        @raise error.TestFail: If the DUT failed the test.
204        """
205        self.assert_attribute_equals(self.GAP_CLASS_ID,
206                                     self.CLIENT_EXECUTABLE_URL_ATTR_ID,
207                                     self.BLUEZ_URL)
208
209
210    def test_protocol_descriptor_list_attribute(self):
211        """Implementation of test TP/SERVER/SA/BV-05-C from SDP Specification.
212
213        @raise error.TestFail: If the DUT failed the test.
214        """
215        value = self.get_attribute(self.GAP_CLASS_ID,
216                                   self.PROTOCOL_DESCRIPTOR_LIST_ATTR_ID)
217
218        # The first-layer protocol is L2CAP, using the PSM for ATT protocol.
219        self.assert_equal(value[0], [self.L2CAP_UUID, self.ATT_PSM])
220
221        # The second-layer protocol is ATT. The additional parameters are
222        # ignored, since they may reasonably vary between implementations.
223        self.assert_equal(value[1][0], self.ATT_UUID)
224
225
226    def test_continuation_state(self):
227        """Implementation of test TP/SERVER/SA/BV-03-C from SDP Specification.
228
229        @raise error.TestFail: If the DUT failed the test.
230        """
231        record_handle = self.get_single_handle(self.PNP_INFORMATION_CLASS_ID)
232        self.assert_nonempty_list(
233            self.tester.service_attribute_request(
234                record_handle, self.MIN_ATTR_BYTE_CNT, [[0, 0xFFFF]]))
235
236
237    def test_version_list_attribute(self):
238        """Implementation of test TP/SERVER/SA/BV-15-C from SDP Specification.
239
240        @raise error.TestFail: If the DUT failed the test.
241        """
242        self.assert_nonempty_list(
243            self.get_attribute(self.SDP_SERVER_CLASS_ID,
244                self.VERSION_NUMBER_LIST_ATTR_ID))
245
246
247    def test_service_database_state_attribute(self):
248        """Implementation of test TP/SERVER/SA/BV-16-C from SDP Specification.
249
250        @raise error.TestFail: If the DUT failed the test.
251        """
252        state = self.get_attribute(self.SDP_SERVER_CLASS_ID,
253                                   self.SERVICE_DATABASE_STATE_ATTR_ID)
254        if not isinstance(state, int):
255            raise error.TestFail('State is not an int: %s' % state)
256
257
258    def test_profile_descriptor_list_attribute(self):
259        """Implementation of test TP/SERVER/SA/BV-17-C from SDP Specification.
260
261        @raise error.TestFail: If list attribute not correct form.
262
263        """
264        profile_list = self.get_attribute(self.PNP_INFORMATION_CLASS_ID,
265                                          self.PROFILE_DESCRIPTOR_LIST_ATTR_ID)
266
267        if not isinstance(profile_list, list):
268            raise error.TestFail('Value is not a list')
269        self.assert_equal(len(profile_list), 1)
270
271        if not isinstance(profile_list[0], list):
272            raise error.TestFail('Item is not a list')
273        self.assert_equal(len(profile_list[0]), 2)
274
275        self.assert_equal(profile_list[0][0], self.PNP_INFORMATION_CLASS_ID)
276
277
278    def test_additional_protocol_descriptor_list_attribute(self):
279        """Implementation of test TP/SERVER/SA/BV-21-C from SDP Specification.
280
281        @raise error.TestFail: If the DUT failed the test.
282
283        """
284
285        """AVRCP is not supported by Chromebook and no need to run this test
286        self.assert_nonempty_list(
287            self.get_attribute(self.AVRCP_TG_CLASS_ID,
288                self.ADDITIONAL_PROTOCOLLIST_ATTR_ID))
289        """
290
291    def test_non_existing_attribute(self):
292        """Implementation of test TP/SERVER/SA/BV-20-C from SDP Specification.
293
294        @raise error.TestFail: If the DUT failed the test.
295        """
296        record_handle = self.get_single_handle(self.FAKE_SERVICE_CLASS_ID)
297        res = self.tester.service_attribute_request(
298                  record_handle, self.MAX_ATTR_BYTE_CNT,
299                  [self.NON_EXISTING_ATTRIBUTE_ID])
300        self.assert_equal(res, [])
301
302
303    def test_fake_attributes(self):
304        """Test values of attributes of the fake service record.
305
306        @raise error.TestFail: If the DUT failed the test.
307        """
308        for attr_id in self.FAKE_GENERAL_ATTRIBUTE_IDS:
309            self.assert_attribute_equals(self.FAKE_SERVICE_CLASS_ID,
310                                         attr_id, self.FAKE_ATTRIBUTE_VALUE)
311
312        for offset in self.FAKE_LANGUAGE_ATTRIBUTE_OFFSETS:
313            record_handle = self.get_single_handle(self.FAKE_SERVICE_CLASS_ID)
314
315            lang_base = self.tester.service_attribute_request(
316                            record_handle, self.MAX_ATTR_BYTE_CNT,
317                            [self.LANGUAGE_BASE_ATTRIBUTE_ID])
318            attr_id = lang_base[1] + offset
319
320            response = self.tester.service_attribute_request(
321                           record_handle, self.MAX_ATTR_BYTE_CNT, [attr_id])
322            self.assert_equal(response, [attr_id, self.FAKE_ATTRIBUTE_VALUE])
323
324
325    def test_invalid_record_handle(self):
326        """Implementation of test TP/SERVER/SA/BI-01-C from SDP Specification.
327
328        @raise error.TestFail: If the DUT failed the test.
329        """
330        res = self.tester.service_attribute_request(
331                  self.INVALID_RECORD_HANDLE, self.MAX_ATTR_BYTE_CNT,
332                  [self.NON_EXISTING_ATTRIBUTE_ID])
333        self.assert_equal(res, self.ERROR_CODE_INVALID_RECORD_HANDLE)
334
335
336    def test_invalid_request_syntax(self):
337        """Implementation of test TP/SERVER/SA/BI-02-C from SDP Specification.
338
339        @raise error.TestFail: If the DUT failed the test.
340        """
341        record_handle = self.get_single_handle(self.SDP_SERVER_CLASS_ID)
342        res = self.tester.service_attribute_request(
343                  record_handle,
344                  self.MAX_ATTR_BYTE_CNT,
345                  [self.SERVICE_RECORD_HANDLE_ATTR_ID],
346                  invalid_request=self.INVALID_SYNTAX_REQUEST)
347        self.assert_equal(res, self.ERROR_CODE_INVALID_SYNTAX)
348
349
350    def test_invalid_pdu_size(self):
351        """Implementation of test TP/SERVER/SA/BI-03-C from SDP Specification.
352
353        @raise error.TestFail: If the DUT failed the test.
354        """
355        record_handle = self.get_single_handle(self.SDP_SERVER_CLASS_ID)
356        res = self.tester.service_attribute_request(
357                  record_handle,
358                  self.MAX_ATTR_BYTE_CNT,
359                  [self.SERVICE_RECORD_HANDLE_ATTR_ID],
360                  forced_pdu_size=self.INVALID_PDU_SIZE)
361        self.assert_equal(res, self.ERROR_CODE_INVALID_PDU_SIZE)
362
363
364    def correct_request(self):
365        """Run basic tests for Service Attribute Request."""
366        # Connect to the DUT via L2CAP using SDP socket.
367        self.tester.connect(self.adapter['Address'])
368        self.test_record_handle_attribute()
369        self.test_browse_group_attribute()
370        self.test_icon_url_attribute()
371        self.test_documentation_url_attribute()
372        self.test_client_executable_url_attribute()
373        self.test_protocol_descriptor_list_attribute()
374        self.test_continuation_state()
375        self.test_version_list_attribute()
376        self.test_service_database_state_attribute()
377        self.test_profile_descriptor_list_attribute()
378        self.test_additional_protocol_descriptor_list_attribute()
379        self.test_fake_attributes()
380        self.test_non_existing_attribute()
381        self.test_invalid_record_handle()
382        self.test_invalid_request_syntax()
383        self.test_invalid_pdu_size()
384
385
386    def build_service_record(self):
387        """Build SDP record manually for the fake service.
388
389        @return resulting record as string
390
391        """
392        value = ET.Element('uint16', {'value': str(self.FAKE_ATTRIBUTE_VALUE)})
393
394        sdp_record = ET.Element('record')
395
396        service_id_attr = ET.Element(
397            'attribute', {'id': str(self.SERVICE_CLASS_ID_ATTR_ID)})
398        service_id_attr.append(
399            ET.Element('uuid', {'value': '0x%X' % self.FAKE_SERVICE_CLASS_ID}))
400        sdp_record.append(service_id_attr)
401
402        for attr_id in self.FAKE_GENERAL_ATTRIBUTE_IDS:
403            attr = ET.Element('attribute', {'id': str(attr_id)})
404            attr.append(value)
405            sdp_record.append(attr)
406
407        for offset in self.FAKE_LANGUAGE_ATTRIBUTE_OFFSETS:
408            attr_id = self.FAKE_ATTRIBUTE_VALUE + offset
409            attr = ET.Element('attribute', {'id': str(attr_id)})
410            attr.append(value)
411            sdp_record.append(attr)
412
413        sdp_record_str = ('<?xml version="1.0" encoding="UTF-8"?>' +
414                          ET.tostring(sdp_record))
415        return sdp_record_str
416
417
418    def run_once(self):
419        # Reset the adapter to the powered on, discoverable state.
420        if not self.device.reset_on():
421            raise error.TestFail('DUT adapter could not be powered on')
422        if not self.device.set_discoverable(True):
423            raise error.TestFail('DUT could not be set as discoverable')
424
425        self.adapter = self.device.get_adapter_properties()
426
427        # Create a fake service record in order to test attributes,
428        # that are not present in any of existing services.
429        uuid128 = ((self.FAKE_SERVICE_CLASS_ID << 96) +
430                   self.BLUETOOTH_BASE_UUID)
431        uuid_str = str(uuid.UUID(int=uuid128))
432        sdp_record = self.build_service_record()
433        self.device.register_profile(self.FAKE_SERVICE_PATH,
434                                     uuid_str,
435                                     {"ServiceRecord": sdp_record})
436
437        # Setup the tester as a generic computer.
438        if not self.tester.setup('computer'):
439            raise error.TestFail('Tester could not be initialized')
440
441        self.correct_request()
442