1#!/usr/bin/env python
2#
3# Copyright (C) 2017 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#
17"""This module is for VTS test cases involving IOmxStore and IOmx::listNodes().
18
19VtsHalMediaOmxStoreV1_0Host derives from base_test.BaseTestClass. It contains
20two independent tests: testListServiceAttributes() and
21testQueryCodecInformation(). The first one tests
22IOmxStore::listServiceAttributes() while the second one test multiple functions
23in IOmxStore as well as check the consistency of the return values with
24IOmx::listNodes().
25
26"""
27
28import logging
29import re
30
31from vts.runners.host import asserts
32from vts.runners.host import test_runner
33from vts.testcases.template.hal_hidl_host_test import hal_hidl_host_test
34
35OMXSTORE_V1_0_HAL = "android.hardware.media.omx@1.0::IOmxStore"
36
37class VtsHalMediaOmxStoreV1_0Host(hal_hidl_host_test.HalHidlHostTest):
38    """Host test class to run the Media_OmxStore HAL."""
39
40    TEST_HAL_SERVICES = {OMXSTORE_V1_0_HAL}
41
42    def setUpClass(self):
43        super(VtsHalMediaOmxStoreV1_0Host, self).setUpClass()
44
45        self.dut.hal.InitHidlHal(
46            target_type='media_omx',
47            target_basepaths=self.dut.libPaths,
48            target_version=1.0,
49            target_package='android.hardware.media.omx',
50            target_component_name='IOmxStore',
51            hw_binder_service_name=self.getHalServiceName(OMXSTORE_V1_0_HAL),
52            bits=int(self.abi_bitness))
53
54        self.omxstore = self.dut.hal.media_omx
55        self.vtypes = self.omxstore.GetHidlTypeInterface('types')
56
57    def testListServiceAttributes(self):
58        """Test IOmxStore::listServiceAttributes().
59
60        Tests that IOmxStore::listServiceAttributes() can be called
61        successfully and returns sensible attributes.
62
63        An attribute has a name (key) and a value. Known attributes (represented
64        by variable "known" below) have certain specifications for valid values.
65        Unknown attributes that start with 'supports-' should only have '0' or
66        '1' as their value. Other unknown attributes do not cause the test to
67        fail, but are reported as warnings in the host log.
68
69        """
70
71        status, attributes = self.omxstore.listServiceAttributes()
72        asserts.assertEqual(self.vtypes.Status.OK, status,
73                            'listServiceAttributes() fails.')
74
75        # known is a dictionary whose keys are the known "key" for a service
76        # attribute pair (see IOmxStore::Attribute), and whose values are the
77        # corresponding regular expressions that will have to match with the
78        # "value" of the attribute pair. If listServiceAttributes() returns an
79        # attribute that has a matching key but an unmatched value, the test
80        # will fail.
81        known = {
82            'max-video-encoder-input-buffers': re.compile('0|[1-9][0-9]*'),
83            'supports-multiple-secure-codecs': re.compile('0|1'),
84            'supports-secure-with-non-secure-codec': re.compile('0|1'),
85        }
86        # unknown is a list of pairs of regular expressions. For each attribute
87        # whose key is not known (i.e., does not match any of the keys in the
88        # "known" variable defined above), that key will be tried for a match
89        # with the first element of each pair of the variable "unknown". If a
90        # match occurs, the value of that same attribute will be tried for a
91        # match with the second element of the pair. If this second match fails,
92        # the test will fail.
93        unknown = [
94            (re.compile(r'supports-[a-z0-9\-]*'), re.compile('0|1')),
95        ]
96
97        # key_set is used to verify that listServiceAttributes() does not return
98        # duplicate attribute names.
99        key_set = set()
100        for attr in attributes:
101            attr_key = attr['key']
102            attr_value = attr['value']
103
104            # attr_key must not have been seen before.
105            asserts.assertTrue(
106                attr_key not in key_set,
107                'Service attribute "' + attr_key + '" has duplicates.')
108            key_set.add(attr_key)
109
110            if attr_key in known:
111                asserts.assertTrue(
112                    known[attr_key].match(attr_value),
113                    'Service attribute "' + attr_key + '" has ' +
114                    'invalid value "' + attr_value + '".')
115            else:
116                matched = False
117                for key_re, value_re in unknown:
118                    if key_re.match(attr_key):
119                        asserts.assertTrue(
120                            value_re.match(attr_value),
121                            'Service attribute "' + attr_key + '" has ' +
122                            'invalid value "' + attr_value + '".')
123                        matched = True
124                if not matched:
125                    logging.warning(
126                        'Unrecognized service attribute "' + attr_key + '" ' +
127                        'with value "' + attr_value + '".')
128
129    def testQueryCodecInformation(self):
130        """Query and verify information from IOmxStore and IOmx::listNodes().
131
132        This function performs three main checks:
133         1. Information about roles and nodes returned from
134            IOmxStore::listRoles() conforms to the specifications in
135            IOmxStore.hal.
136         2. Each node present in the information returned from
137            IOmxStore::listRoles() must be supported by its owner. A node is
138            considered "supported" by its owner if the IOmx instance
139            corresponding to that owner returns that node and all associated
140            roles when IOmx::listNodes() is called.
141         3. The prefix string obtained form IOmxStore::getNodePrefix() must be
142            sensible, and is indeed a prefix of all the node names.
143
144        In step 1, node attributes are validated in the same manner as how
145        service attributes are validated in testListServiceAttributes().
146        Role names and mime types must be recognized by the function get_role()
147        defined below.
148
149        """
150
151        # Basic patterns for matching
152        class Pattern(object):
153            toggle = '(0|1)'
154            string = '(.*)'
155            num = '(0|([1-9][0-9]*))'
156            size = '(' + num + 'x' + num + ')'
157            ratio = '(' + num + ':' + num + ')'
158            range_num = '((' + num + '-' + num + ')|' + num + ')'
159            range_size = '((' + size + '-' + size + ')|' + size + ')'
160            range_ratio = '((' + ratio + '-' + ratio + ')|' + ratio + ')'
161            list_range_num = '(' + range_num + '(,' + range_num + ')*)'
162
163        # Matching rules for node attributes with fixed keys
164        attr_re = {
165            'alignment'                     : Pattern.size,
166            'bitrate-range'                 : Pattern.range_num,
167            'block-aspect-ratio-range'      : Pattern.range_ratio,
168            'block-count-range'             : Pattern.range_num,
169            'block-size'                    : Pattern.size,
170            'blocks-per-second-range'       : Pattern.range_num,
171            'complexity-default'            : Pattern.num,
172            'complexity-range'              : Pattern.range_num,
173            'feature-adaptive-playback'     : Pattern.toggle,
174            'feature-bitrate-control'       : '(VBR|CBR|CQ)[,(VBR|CBR|CQ)]*',
175            'feature-can-swap-width-height' : Pattern.toggle,
176            'feature-intra-refresh'         : Pattern.toggle,
177            'feature-partial-frame'         : Pattern.toggle,
178            'feature-secure-playback'       : Pattern.toggle,
179            'feature-tunneled-playback'     : Pattern.toggle,
180            'frame-rate-range'              : Pattern.range_num,
181            'max-channel-count'             : Pattern.num,
182            'max-concurrent-instances'      : Pattern.num,
183            'max-supported-instances'       : Pattern.num,
184            'pixel-aspect-ratio-range'      : Pattern.range_ratio,
185            'quality-default'               : Pattern.num,
186            'quality-range'                 : Pattern.range_num,
187            'quality-scale'                 : Pattern.string,
188            'sample-rate-ranges'            : Pattern.list_range_num,
189            'size-range'                    : Pattern.range_size,
190        }
191
192        # Matching rules for node attributes with key patterns
193        attr_pattern_re = [
194            ('measured-frame-rate-' + Pattern.size +
195             '-range', Pattern.range_num),
196            (r'feature-[a-zA-Z0-9_\-]+', Pattern.string),
197        ]
198
199        # Matching rules for node names and owners
200        node_name_re = r'[a-zA-Z0-9.\-]+'
201        node_owner_re = r'[a-zA-Z0-9._\-]+'
202
203        # Compile all regular expressions
204        for key in attr_re:
205            attr_re[key] = re.compile(attr_re[key])
206        for index, value in enumerate(attr_pattern_re):
207            attr_pattern_re[index] = (re.compile(value[0]),
208                                      re.compile(value[1]))
209        node_name_re = re.compile(node_name_re)
210        node_owner_re = re.compile(node_owner_re)
211
212        # Mapping from mime types to roles.
213        # These values come from MediaDefs.cpp and OMXUtils.cpp
214        audio_mime_to_role = {
215            '3gpp'             : 'amrnb',
216            'ac3'              : 'ac3',
217            'amr-wb'           : 'amrwb',
218            'eac3'             : 'eac3',
219            'flac'             : 'flac',
220            'g711-alaw'        : 'g711alaw',
221            'g711-mlaw'        : 'g711mlaw',
222            'gsm'              : 'gsm',
223            'mp4a-latm'        : 'aac',
224            'mpeg'             : 'mp3',
225            'mpeg-L1'          : 'mp1',
226            'mpeg-L2'          : 'mp2',
227            'opus'             : 'opus',
228            'raw'              : 'raw',
229            'vorbis'           : 'vorbis',
230        }
231        video_mime_to_role = {
232            '3gpp'             : 'h263',
233            'avc'              : 'avc',
234            'dolby-vision'     : 'dolby-vision',
235            'hevc'             : 'hevc',
236            'mp4v-es'          : 'mpeg4',
237            'mpeg2'            : 'mpeg2',
238            'x-vnd.on2.vp8'    : 'vp8',
239            'x-vnd.on2.vp9'    : 'vp9',
240        }
241        image_mime_to_role = {
242            'vnd.android.heic' : 'heic',
243        }
244        def get_role(is_encoder, mime):
245            """Returns the role based on is_encoder and mime.
246
247            The mapping from a pair (is_encoder, mime) to a role string is
248            defined in frameworks/av/media/libmedia/MediaDefs.cpp and
249            frameworks/av/media/libstagefright/omx/OMXUtils.cpp. This function
250            does essentially the same work as GetComponentRole() in
251            OMXUtils.cpp.
252
253            Args:
254              is_encoder: A boolean indicating whether the role is for an
255                  encoder or a decoder.
256              mime: A string of the desired mime type.
257
258            Returns:
259              A string for the requested role name, or None if mime is not
260              recognized.
261            """
262            mime_suffix = mime[6:]
263            middle = 'encoder.' if is_encoder else 'decoder.'
264            if mime.startswith('audio/'):
265                if mime_suffix not in audio_mime_to_role:
266                    return None
267                prefix = 'audio_'
268                suffix = audio_mime_to_role[mime_suffix]
269            elif mime.startswith('video/'):
270                if mime_suffix not in video_mime_to_role:
271                    return None
272                prefix = 'video_'
273                suffix = video_mime_to_role[mime_suffix]
274            elif mime.startswith('image/'):
275                if mime_suffix not in image_mime_to_role:
276                    return None
277                prefix = 'image_'
278                suffix = image_mime_to_role[mime_suffix]
279            else:
280                return None
281            return prefix + middle + suffix
282
283        # The test code starts here.
284        roles = self.omxstore.listRoles()
285        if len(roles) == 0:
286            logging.warning('IOmxStore has an empty implementation. Skipping...')
287            return
288
289        # A map from a node name to a set of roles.
290        node2roles = {}
291
292        # A map from an owner to a set of node names.
293        owner2nodes = {}
294
295        logging.info('Testing IOmxStore::listRoles()...')
296        # role_set is used for checking if there are duplicate roles.
297        role_set = set()
298        for role in roles:
299            role_name = role['role']
300            mime_type = role['type']
301            is_encoder = role['isEncoder']
302            nodes = role['nodes']
303
304            # The role name must not have duplicates.
305            asserts.assertFalse(
306                role_name in role_set,
307                'Role "' + role_name + '" has duplicates.')
308
309            queried_role = get_role(is_encoder, mime_type)
310            # If mime_type is not recognized, skip it.
311            if queried_role is None:
312                logging.info(
313                    'Unrecognized mime type  "' +
314                    mime_type + '", skipping.')
315                continue
316
317            # Otherwise, type and isEncoder must be consistent with role.
318            asserts.assertEqual(
319                role_name, queried_role,
320                'Role "' + role_name + '" does not match ' +
321                ('an encoder ' if is_encoder else 'a decoder ') +
322                'for mime type "' + mime_type + '"')
323
324            # Save the role name to check for duplicates.
325            role_set.add(role_name)
326
327            # Ignore role.preferPlatformNodes for now.
328
329            # node_set is used for checking if there are duplicate node names
330            # for each role.
331            node_set = set()
332            for node in nodes:
333                node_name = node['name']
334                owner = node['owner']
335                attributes = node['attributes']
336
337                # For each role, the node name must not have duplicates.
338                asserts.assertFalse(
339                    node_name in node_set,
340                    'Node "' + node_name + '" has duplicates for the same ' +
341                    'role "' + queried_role + '".')
342
343                # Check the format of node name
344                asserts.assertTrue(
345                    node_name_re.match(node_name),
346                    'Node name "' + node_name + '" is invalid.')
347                # Check the format of node owner
348                asserts.assertTrue(
349                    node_owner_re.match(owner),
350                    'Node owner "' + owner + '" is invalid.')
351
352                attr_map = {}
353                for attr in attributes:
354                    attr_key = attr['key']
355                    attr_value = attr['value']
356
357                    # For each node and each role, the attribute key must not
358                    # have duplicates.
359                    asserts.assertFalse(
360                        attr_key in attr_map,
361                        'Attribute "' + attr_key +
362                        '" for node "' + node_name +
363                        '"has duplicates.')
364
365                    # Check the value against the corresponding regular
366                    # expression.
367                    if attr_key in attr_re:
368                        asserts.assertTrue(
369                            attr_re[attr_key].match(attr_value),
370                            'Attribute "' + attr_key + '" has ' +
371                            'invalid value "' + attr_value + '".')
372                    else:
373                        key_found = False
374                        for pattern_key, pattern_value in attr_pattern_re:
375                            if pattern_key.match(attr_key):
376                                asserts.assertTrue(
377                                    pattern_value.match(attr_value),
378                                    'Attribute "' + attr_key + '" has ' +
379                                    'invalid value "' + attr_value + '".')
380                                key_found = True
381                                break
382                        if not key_found:
383                            logging.warning(
384                                'Unknown attribute "' +
385                                attr_key + '" with value "' +
386                                attr_value + '".')
387
388                    # Store the key-value pair
389                    attr_map[attr_key] = attr_value
390
391                if node_name not in node2roles:
392                    node2roles[node_name] = {queried_role,}
393                    if owner not in owner2nodes:
394                        owner2nodes[owner] = {node_name,}
395                    else:
396                        owner2nodes[owner].add(node_name)
397                else:
398                    node2roles[node_name].add(queried_role)
399
400        # Verify the information with IOmx::listNodes().
401        # IOmxStore::listRoles() and IOmx::listNodes() should give consistent
402        # information about nodes and roles.
403        logging.info('Verifying with IOmx::listNodes()...')
404        for owner in owner2nodes:
405            # Obtain the IOmx instance for each "owner"
406            omx = self.omxstore.getOmx(owner)
407            asserts.assertTrue(
408                omx,
409                'Cannot obtain IOmx instance "' + owner + '".')
410
411            # Invoke IOmx::listNodes()
412            status, node_info_list = omx.listNodes()
413            asserts.assertEqual(
414                self.vtypes.Status.OK, status,
415                'IOmx::listNodes() fails for IOmx instance "' + owner + '".')
416
417            # Verify that roles for each node match with the information from
418            # IOmxStore::listRoles().
419            node_set = set()
420            for node_info in node_info_list:
421                node = node_info['mName']
422                roles = node_info['mRoles']
423
424                # IOmx::listNodes() should not list duplicate node names.
425                asserts.assertFalse(
426                    node in node_set,
427                    'IOmx::listNodes() lists duplicate nodes "' + node + '".')
428                node_set.add(node)
429
430                # Skip "hidden" nodes, i.e. those that are not advertised by
431                # IOmxStore::listRoles().
432                if node not in owner2nodes[owner]:
433                    logging.warning(
434                        'IOmx::listNodes() lists unknown node "' + node +
435                        '" for IOmx instance "' + owner + '".')
436                    continue
437
438                # All the roles advertised by IOmxStore::listRoles() for this
439                # node must be included in role_set.
440                role_set = set(roles)
441                asserts.assertTrue(
442                    node2roles[node] <= role_set,
443                    'IOmx::listNodes() for IOmx instance "' + owner + '" ' +
444                    'does not report some roles for node "' + node + '": ' +
445                    ', '.join(node2roles[node] - role_set))
446
447                # Try creating the node.
448                status, omxNode = omx.allocateNode(node, None)
449                asserts.assertEqual(
450                    self.vtypes.Status.OK, status,
451                    'IOmx::allocateNode() for IOmx instance "' + owner + '" ' +
452                    'fails to allocate node "' + node +'".')
453                status = omxNode.freeNode()
454
455            # Check that all nodes obtained from IOmxStore::listRoles() are
456            # supported by the their corresponding IOmx instances.
457            node_set_diff = owner2nodes[owner] - node_set
458            asserts.assertFalse(
459                node_set_diff,
460                'IOmx::listNodes() for IOmx instance "' + owner + '" ' +
461                'does not report some expected nodes: ' +
462                ', '.join(node_set_diff) + '.')
463
464        # Check that the prefix is a sensible string.
465        if node2roles:
466            # Call IOmxStore::getNodePrefix().
467            prefix = self.omxstore.getNodePrefix()
468            logging.info('Checking node prefix: ' +
469                         'IOmxStore::getNodePrefix() returns "' + prefix + '".')
470
471            asserts.assertTrue(
472                node_name_re.match(prefix),
473                '"' + prefix + '" is not a valid prefix for node names.')
474
475            # Check that all node names have the said prefix.
476            for node in node2roles:
477                asserts.assertTrue(
478                    node.startswith(prefix),
479                    'Node "' + node + '" does not start with ' +
480                    'prefix "' + prefix + '".')
481
482if __name__ == '__main__':
483    test_runner.main()
484