You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
360 lines
14 KiB
360 lines
14 KiB
#!/usr/bin/env python
|
|
#
|
|
# Copyright (C) 2017 The Android Open Source Project
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
#
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import shutil
|
|
import tempfile
|
|
|
|
from vts.runners.host import asserts
|
|
from vts.runners.host import base_test
|
|
from vts.runners.host import keys
|
|
from vts.runners.host import test_runner
|
|
from vts.runners.host import utils
|
|
from vts.testcases.vndk.golden import vndk_data
|
|
from vts.utils.python.file import target_file_utils
|
|
from vts.utils.python.library import elf_parser
|
|
from vts.utils.python.library.vtable import vtable_dumper
|
|
from vts.utils.python.vndk import vndk_utils
|
|
|
|
|
|
class VtsVndkAbiTest(base_test.BaseTestClass):
|
|
"""A test module to verify ABI compliance of vendor libraries.
|
|
|
|
Attributes:
|
|
_dut: the AndroidDevice under test.
|
|
_temp_dir: The temporary directory for libraries copied from device.
|
|
_vndk_version: String, the VNDK version supported by the device.
|
|
data_file_path: The path to VTS data directory.
|
|
"""
|
|
|
|
def setUpClass(self):
|
|
"""Initializes data file path, device, and temporary directory."""
|
|
required_params = [keys.ConfigKeys.IKEY_DATA_FILE_PATH]
|
|
self.getUserParams(required_params)
|
|
self._dut = self.android_devices[0]
|
|
self._temp_dir = tempfile.mkdtemp()
|
|
self._vndk_version = self._dut.vndk_version
|
|
|
|
def tearDownClass(self):
|
|
"""Deletes the temporary directory."""
|
|
logging.info("Delete %s", self._temp_dir)
|
|
shutil.rmtree(self._temp_dir)
|
|
|
|
def _PullOrCreateDir(self, target_dir, host_dir):
|
|
"""Copies a directory from device. Creates an empty one if not exist.
|
|
|
|
Args:
|
|
target_dir: The directory to copy from device.
|
|
host_dir: The directory to copy to host.
|
|
"""
|
|
if not target_file_utils.IsDirectory(target_dir, self._dut.shell):
|
|
logging.info("%s doesn't exist. Create %s.", target_dir, host_dir)
|
|
os.makedirs(host_dir)
|
|
return
|
|
parent_dir = os.path.dirname(host_dir)
|
|
if parent_dir and not os.path.isdir(parent_dir):
|
|
os.makedirs(parent_dir)
|
|
logging.info("adb pull %s %s", target_dir, host_dir)
|
|
self._dut.adb.pull(target_dir, host_dir)
|
|
|
|
def _ToHostPath(self, target_path):
|
|
"""Maps target path to host path in self._temp_dir."""
|
|
return os.path.join(self._temp_dir, *target_path.strip("/").split("/"))
|
|
|
|
@staticmethod
|
|
def _LoadGlobalSymbolsFromDump(dump_obj):
|
|
"""Loads global symbols from a dump object.
|
|
|
|
Args:
|
|
dump_obj: A dict, the dump in JSON format.
|
|
|
|
Returns:
|
|
A set of strings, the symbol names.
|
|
"""
|
|
symbols = set()
|
|
for key in ("elf_functions", "elf_objects"):
|
|
symbols.update(
|
|
symbol.get("name", "") for symbol in dump_obj.get(key, []) if
|
|
symbol.get("binding", "global") == "global")
|
|
return symbols
|
|
|
|
def _DiffElfSymbols(self, dump_obj, parser):
|
|
"""Checks if a library includes all symbols in a dump.
|
|
|
|
Args:
|
|
dump_obj: A dict, the dump in JSON format.
|
|
parser: An elf_parser.ElfParser that loads the library.
|
|
|
|
Returns:
|
|
A list of strings, the global symbols that are in the dump but not
|
|
in the library.
|
|
|
|
Raises:
|
|
elf_parser.ElfError if fails to load the library.
|
|
"""
|
|
dump_symbols = self._LoadGlobalSymbolsFromDump(dump_obj)
|
|
lib_symbols = parser.ListGlobalDynamicSymbols(include_weak=True)
|
|
return sorted(dump_symbols.difference(lib_symbols))
|
|
|
|
@staticmethod
|
|
def _DiffVtableComponent(offset, expected_symbol, vtable):
|
|
"""Checks if a symbol is in a vtable entry.
|
|
|
|
Args:
|
|
offset: An integer, the offset of the expected symbol.
|
|
exepcted_symbol: A string, the name of the expected symbol.
|
|
vtable: A dict of {offset: [entry]} where offset is an integer and
|
|
entry is an instance of vtable_dumper.VtableEntry.
|
|
|
|
Returns:
|
|
A list of strings, the actual possible symbols if expected_symbol
|
|
does not match the vtable entry.
|
|
None if expected_symbol matches the entry.
|
|
"""
|
|
if offset not in vtable:
|
|
return []
|
|
|
|
entry = vtable[offset]
|
|
if not entry.names:
|
|
return [hex(entry.value).rstrip('L')]
|
|
|
|
if expected_symbol not in entry.names:
|
|
return entry.names
|
|
|
|
def _DiffVtableComponents(self, dump_obj, dumper):
|
|
"""Checks if a library includes all vtable entries in a dump.
|
|
|
|
Args:
|
|
dump_obj: A dict, the dump in JSON format.
|
|
dumper: An vtable_dumper.VtableDumper that loads the library.
|
|
|
|
Returns:
|
|
A list of tuples (VTABLE, OFFSET, EXPECTED_SYMBOL, ACTUAL).
|
|
ACTUAL can be "missing", a list of symbol names, or an ELF virtual
|
|
address.
|
|
|
|
Raises:
|
|
vtable_dumper.VtableError if fails to dump vtable from the library.
|
|
"""
|
|
function_kinds = [
|
|
"function_pointer",
|
|
"complete_dtor_pointer",
|
|
"deleting_dtor_pointer"
|
|
]
|
|
non_function_kinds = [
|
|
"vcall_offset",
|
|
"vbase_offset",
|
|
"offset_to_top",
|
|
"rtti",
|
|
"unused_function_pointer"
|
|
]
|
|
default_vtable_component_kind = "function_pointer"
|
|
|
|
global_symbols = self._LoadGlobalSymbolsFromDump(dump_obj)
|
|
|
|
lib_vtables = {vtable.name: vtable
|
|
for vtable in dumper.DumpVtables()}
|
|
logging.debug("\n\n".join(str(vtable)
|
|
for _, vtable in lib_vtables.iteritems()))
|
|
|
|
vtables_diff = []
|
|
for record_type in dump_obj.get("record_types", []):
|
|
# Since Android R, unique_id has been replaced with linker_set_key.
|
|
# unique_id starts with "_ZTI"; linker_set_key starts with "_ZTS".
|
|
type_name_symbol = record_type.get("unique_id", "")
|
|
if type_name_symbol:
|
|
vtable_symbol = type_name_symbol.replace("_ZTS", "_ZTV", 1)
|
|
else:
|
|
type_name_symbol = record_type.get("linker_set_key", "")
|
|
vtable_symbol = type_name_symbol.replace("_ZTI", "_ZTV", 1)
|
|
|
|
# Skip if the vtable symbol isn't global.
|
|
if vtable_symbol not in global_symbols:
|
|
continue
|
|
|
|
# Collect vtable entries from library dump.
|
|
if vtable_symbol in lib_vtables:
|
|
lib_vtable = {entry.offset: entry
|
|
for entry in lib_vtables[vtable_symbol].entries}
|
|
else:
|
|
lib_vtable = dict()
|
|
|
|
for index, entry in enumerate(record_type.get("vtable_components",
|
|
[])):
|
|
entry_offset = index * int(self.abi_bitness) // 8
|
|
entry_kind = entry.get("kind", default_vtable_component_kind)
|
|
entry_symbol = entry.get("mangled_component_name", "")
|
|
entry_is_pure = entry.get("is_pure", False)
|
|
|
|
if entry_kind in non_function_kinds:
|
|
continue
|
|
|
|
if entry_kind not in function_kinds:
|
|
logging.warning("%s: Unexpected vtable entry kind %s",
|
|
vtable_symbol, entry_kind)
|
|
|
|
if entry_symbol not in global_symbols:
|
|
# Itanium cxx abi doesn't specify pure virtual vtable
|
|
# entry's behaviour. However we can still do some checks
|
|
# based on compiler behaviour.
|
|
# Even though we don't check weak symbols, we can still
|
|
# issue a warning when a pure virtual function pointer
|
|
# is missing.
|
|
if entry_is_pure and entry_offset not in lib_vtable:
|
|
logging.warning("%s: Expected pure virtual function"
|
|
"in %s offset %s",
|
|
vtable_symbol, vtable_symbol,
|
|
entry_offset)
|
|
continue
|
|
|
|
diff_symbols = self._DiffVtableComponent(
|
|
entry_offset, entry_symbol, lib_vtable)
|
|
if diff_symbols is None:
|
|
continue
|
|
|
|
vtables_diff.append(
|
|
(vtable_symbol, str(entry_offset), entry_symbol,
|
|
(",".join(diff_symbols) if diff_symbols else "missing")))
|
|
|
|
return vtables_diff
|
|
|
|
def _ScanLibDirs(self, dump_dir, lib_dirs, dump_version):
|
|
"""Compares dump files with libraries copied from device.
|
|
|
|
Args:
|
|
dump_dir: The directory containing dump files.
|
|
lib_dirs: The list of directories containing libraries.
|
|
dump_version: The VNDK version of the dump files. If the device has
|
|
no VNDK version or has extension in vendor partition,
|
|
this method compares the unversioned VNDK directories
|
|
with the dump directories of the given version.
|
|
|
|
Returns:
|
|
A list of strings, the incompatible libraries.
|
|
"""
|
|
error_list = []
|
|
dump_paths = dict()
|
|
lib_paths = dict()
|
|
for parent_dir, dump_name in utils.iterate_files(dump_dir):
|
|
dump_path = os.path.join(parent_dir, dump_name)
|
|
if dump_path.endswith(".dump"):
|
|
lib_name = dump_name.rpartition(".dump")[0]
|
|
dump_paths[lib_name] = dump_path
|
|
else:
|
|
logging.warning("Unknown dump: %s", dump_path)
|
|
|
|
for lib_dir in lib_dirs:
|
|
for parent_dir, lib_name in utils.iterate_files(lib_dir):
|
|
if lib_name not in lib_paths:
|
|
lib_paths[lib_name] = os.path.join(parent_dir, lib_name)
|
|
|
|
for lib_name, dump_path in dump_paths.iteritems():
|
|
if lib_name not in lib_paths:
|
|
logging.info("%s: Not found on target", lib_name)
|
|
continue
|
|
lib_path = lib_paths[lib_name]
|
|
rel_path = os.path.relpath(lib_path, self._temp_dir)
|
|
|
|
has_exception = False
|
|
missing_symbols = []
|
|
vtable_diff = []
|
|
|
|
try:
|
|
with open(dump_path, "r") as dump_file:
|
|
dump_obj = json.load(dump_file)
|
|
with vtable_dumper.VtableDumper(lib_path) as dumper:
|
|
missing_symbols = self._DiffElfSymbols(
|
|
dump_obj, dumper)
|
|
vtable_diff = self._DiffVtableComponents(
|
|
dump_obj, dumper)
|
|
except (IOError,
|
|
elf_parser.ElfError,
|
|
vtable_dumper.VtableError) as e:
|
|
logging.exception("%s: Cannot diff ABI", rel_path)
|
|
has_exception = True
|
|
|
|
if missing_symbols:
|
|
logging.error("%s: Missing Symbols:\n%s",
|
|
rel_path, "\n".join(missing_symbols))
|
|
if vtable_diff:
|
|
logging.error("%s: Vtable Difference:\n"
|
|
"vtable offset expected actual\n%s",
|
|
rel_path,
|
|
"\n".join(" ".join(e) for e in vtable_diff))
|
|
if (has_exception or missing_symbols or vtable_diff):
|
|
error_list.append(rel_path)
|
|
else:
|
|
logging.info("%s: Pass", rel_path)
|
|
return error_list
|
|
|
|
@staticmethod
|
|
def _GetLinkerSearchIndex(target_path):
|
|
"""Returns the key for sorting linker search paths."""
|
|
index = 0
|
|
for prefix in ("/odm", "/vendor", "/apex"):
|
|
if target_path.startswith(prefix):
|
|
return index
|
|
index += 1
|
|
return index
|
|
|
|
def testAbiCompatibility(self):
|
|
"""Checks ABI compliance of VNDK libraries."""
|
|
primary_abi = self._dut.getCpuAbiList()[0]
|
|
binder_bitness = self._dut.getBinderBitness()
|
|
asserts.assertTrue(binder_bitness,
|
|
"Cannot determine binder bitness.")
|
|
dump_version = (self._vndk_version if self._vndk_version else
|
|
vndk_data.LoadDefaultVndkVersion(self.data_file_path))
|
|
asserts.assertTrue(dump_version,
|
|
"Cannot load default VNDK version.")
|
|
|
|
dump_dir = vndk_data.GetAbiDumpDirectory(
|
|
self.data_file_path,
|
|
dump_version,
|
|
binder_bitness,
|
|
primary_abi,
|
|
self.abi_bitness)
|
|
asserts.assertTrue(
|
|
dump_dir,
|
|
"No dump files. version: %s ABI: %s bitness: %s" % (
|
|
self._vndk_version, primary_abi, self.abi_bitness))
|
|
logging.info("dump dir: %s", dump_dir)
|
|
|
|
target_dirs = vndk_utils.GetVndkExtDirectories(self.abi_bitness)
|
|
target_dirs += vndk_utils.GetVndkSpExtDirectories(self.abi_bitness)
|
|
target_dirs += [vndk_utils.GetVndkDirectory(self.abi_bitness,
|
|
self._vndk_version)]
|
|
target_dirs.sort(key=self._GetLinkerSearchIndex)
|
|
|
|
host_dirs = [self._ToHostPath(x) for x in target_dirs]
|
|
for target_dir, host_dir in zip(target_dirs, host_dirs):
|
|
self._PullOrCreateDir(target_dir, host_dir)
|
|
|
|
assert_lines = self._ScanLibDirs(dump_dir, host_dirs, dump_version)
|
|
if assert_lines:
|
|
error_count = len(assert_lines)
|
|
if error_count > 20:
|
|
assert_lines = assert_lines[:20] + ["..."]
|
|
assert_lines.append("Total number of errors: " + str(error_count))
|
|
asserts.fail("\n".join(assert_lines))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
test_runner.main()
|