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