| #!/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 logging |
| import os |
| import shutil |
| import tempfile |
| |
| from google.protobuf import text_format |
| |
| from vts.runners.host import asserts |
| from vts.runners.host import base_test |
| from vts.runners.host import const |
| from vts.runners.host import keys |
| from vts.runners.host import test_runner |
| from vts.testcases.vndk.golden import vndk_data |
| from vts.testcases.vndk.proto import VndkAbiDump_pb2 as VndkAbiDump |
| from vts.utils.python.controllers import android_device |
| from vts.utils.python.library import elf_parser |
| from vts.utils.python.library import vtable_parser |
| from vts.utils.python.library.vtable import vtable_dumper |
| |
| |
| def _IterateFiles(root_dir): |
| """A generator yielding relative and full paths in a directory. |
| |
| Args: |
| root_dir: The directory to search. |
| |
| Yields: |
| A tuple of (relative_path, full_path) for each regular file. |
| relative_path is the relative path to root_dir. full_path is the path |
| starting with root_dir. |
| """ |
| for dir_path, dir_names, file_names in os.walk(root_dir): |
| if dir_path == root_dir: |
| rel_dir = "" |
| else: |
| rel_dir = os.path.relpath(dir_path, root_dir) |
| for file_name in file_names: |
| yield (os.path.join(rel_dir, file_name), |
| os.path.join(dir_path, file_name)) |
| |
| |
| 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. |
| """ |
| _ODM_LIB_DIR_32 = "/odm/lib" |
| _ODM_LIB_DIR_64 = "/odm/lib64" |
| _VENDOR_LIB_DIR_32 = "/vendor/lib" |
| _VENDOR_LIB_DIR_64 = "/vendor/lib64" |
| _SYSTEM_LIB_DIR_32 = "/system/lib" |
| _SYSTEM_LIB_DIR_64 = "/system/lib64" |
| |
| 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. |
| """ |
| test_cmd = "test -d " + target_dir |
| logging.info("adb shell %s", test_cmd) |
| result = self._dut.adb.shell(test_cmd, no_except=True) |
| if result[const.EXIT_CODE]: |
| logging.info("%s doesn't exist. Create %s.", target_dir, host_dir) |
| os.mkdir(host_dir, 0750) |
| return |
| logging.info("adb pull %s %s", target_dir, host_dir) |
| self._dut.adb.pull(target_dir, host_dir) |
| |
| def _DiffSymbols(self, dump_path, lib_path): |
| """Checks if a library includes all symbols in a dump. |
| |
| Args: |
| dump_path: The path to the dump file containing list of symbols. |
| lib_path: The path to the library. |
| |
| Returns: |
| A list of strings, the global symbols that are in the dump but not |
| in the library. |
| |
| Raises: |
| IOError if fails to load the dump. |
| elf_parser.ElfError if fails to load the library. |
| """ |
| with open(dump_path, "r") as dump_file: |
| dump_symbols = set(line.strip() for line in dump_file |
| if line.strip()) |
| parser = elf_parser.ElfParser(lib_path) |
| try: |
| lib_symbols = parser.ListGlobalDynamicSymbols(include_weak=True) |
| finally: |
| parser.Close() |
| return sorted(dump_symbols.difference(lib_symbols)) |
| |
| def _DiffVtables(self, dump_path, lib_path): |
| """Checks if a library includes all vtable entries in a dump. |
| |
| Args: |
| dump_path: The path to the dump file containing vtables. |
| lib_path: The path to the library. |
| |
| Returns: |
| A list of tuples (VTABLE, SYMBOL, EXPECTED_OFFSET, ACTUAL_OFFSET). |
| ACTUAL_OFFSET can be "missing" or numbers separated by comma. |
| |
| Raises: |
| IOError if fails to load the dump. |
| vtable_parser.VtableError if fails to load the library. |
| """ |
| parser = vtable_parser.VtableParser( |
| os.path.join(self.data_file_path, "host")) |
| with open(dump_path, "r") as dump_file: |
| dump_vtables = parser.ParseVtablesFromString(dump_file.read()) |
| |
| lib_vtables = parser.ParseVtablesFromLibrary(lib_path) |
| # TODO(b/78316564): The dumper doesn't support SHT_ANDROID_RELA. |
| if not lib_vtables and self.run_as_compliance_test: |
| logging.warning("%s: Cannot dump vtables", |
| os.path.relpath(lib_path, self._temp_dir)) |
| return [] |
| logging.debug("%s: %s", lib_path, lib_vtables) |
| diff = [] |
| for vtable, dump_symbols in dump_vtables.iteritems(): |
| lib_inv_vtable = dict() |
| if vtable in lib_vtables: |
| for off, sym in lib_vtables[vtable]: |
| if sym not in lib_inv_vtable: |
| lib_inv_vtable[sym] = [off] |
| else: |
| lib_inv_vtable[sym].append(off) |
| for off, sym in dump_symbols: |
| if sym not in lib_inv_vtable: |
| diff.append((vtable, sym, str(off), "missing")) |
| elif off not in lib_inv_vtable[sym]: |
| diff.append((vtable, sym, str(off), |
| ",".join(str(x) for x in lib_inv_vtable[sym]))) |
| return diff |
| |
| def _DiffAbi(self, dump_path, lib_path): |
| """Checks if a library includes all symbols and vtable entries in a dump. |
| |
| Args: |
| dump_path: The path to the dump file. |
| lib_path: The path to the library. |
| |
| Returns: |
| A tuple of ([string], [(VTABLE, OFFSET, EXPECTED_SYMBOL, ACTUAL)]). |
| The first element of the tuple contains a list of strings, the |
| global symbols that are in the dump but not in the library. |
| The second element of the tuple contains a list of vtable |
| differences. ACTUAL can be 'missing', a list of symbol names or an |
| ELF virtual address. |
| |
| Raises: |
| IOError if fails to load the dump. |
| text_format.ParseError if fails to parse dump message. |
| vtable_dumper.VtableError if fails to dump vtable from the library. |
| elf_parser.ElfError if fails to load the library. |
| """ |
| # Read reference dump and collect global symbols. |
| abi_dump = VndkAbiDump.AbiDump() |
| with open(dump_path, 'r') as dump_file: |
| text_format.Merge(dump_file.read(), abi_dump) |
| global_symbols = {e.name for e in abi_dump.symbols |
| if e.binding == VndkAbiDump.Symbol.GLOBAL} |
| |
| vtable_not_function_kind = [ |
| VndkAbiDump.VTableEntry.VCALLOFFSET, |
| VndkAbiDump.VTableEntry.VBASEOFFSET, |
| VndkAbiDump.VTableEntry.OFFSETTOTOP, |
| VndkAbiDump.VTableEntry.RTTI, |
| ] |
| vtable_function_kind = [ |
| VndkAbiDump.VTableEntry.VFUNCPOINTER, |
| VndkAbiDump.VTableEntry.DELETINGDTORPOINTER, |
| VndkAbiDump.VTableEntry.COMPLETEDTORPOINTER, |
| ] |
| # Dump symbols and vtables from library. |
| with vtable_dumper.VtableDumper(lib_path) as dumper: |
| lib_vtables = {vtable.name: vtable |
| for vtable in dumper.DumpVtables()} |
| lib_symbols = dumper.ListGlobalDynamicSymbols(include_weak=True) |
| |
| symbols_diff = sorted(global_symbols.difference(lib_symbols)) |
| |
| lib_rel_path = os.path.relpath(lib_path, self._temp_dir) |
| logging.debug('vtables of %s:\n%s', lib_rel_path, |
| '\n\n'.join(str(vtable) |
| for _, vtable in lib_vtables.items())) |
| vtables_diff = [] |
| for vtable in abi_dump.vtables: |
| # If vtable isn't a global symbol, then skip this vtable. |
| if vtable.name not in global_symbols: |
| continue |
| |
| # Collect vtable entries from library dump. |
| if vtable.name in lib_vtables: |
| lib_vtable = {entry.offset: entry |
| for entry in lib_vtables[vtable.name].entries} |
| else: |
| lib_vtable = dict() |
| # Compare reference dump with library dump |
| for vtable_entry in vtable.vtable_entries: |
| off = vtable_entry.offset |
| sym = vtable_entry.name |
| |
| def _LogDiff(expected, actual): |
| """Helper function to log diffs.""" |
| vtables_diff.append((vtable.name, str(off), |
| str(expected), str(actual))) |
| |
| # Entry kind mustn't be UNDEFINED |
| if vtable_entry.kind == VndkAbiDump.VTableEntry.UNDEFINED: |
| logging.warning("%s: Unexpected VTableEntry kind %s", |
| dump_path, |
| text_format.MessageToString(vtable_entry)) |
| continue |
| # Handle non-function vtable entries |
| if vtable_entry.kind in vtable_not_function_kind: |
| # We don't check RTTI, vcalloffset, vbaseoffset and |
| # offsettotop. |
| continue |
| # Else vtable entry must be in vtable_function_kind |
| if sym not in global_symbols: |
| if vtable_entry.is_pure and off not in lib_vtable: |
| # 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. |
| logging.warning("%s: Expected pure virtual " |
| "function in %s offset %s", |
| lib_rel_path, |
| vtable.name, |
| off) |
| continue |
| # Else vtable entry is a global symbol |
| if off not in lib_vtable: |
| _LogDiff(sym, 'missing') |
| continue |
| # Else a vtable entry exists at lib_vtable[off] |
| entry = lib_vtable[off] |
| if not entry.names: |
| _LogDiff(sym, hex(entry.value).rstrip('L')) |
| continue |
| if sym not in entry.names: |
| _LogDiff(sym, entry.names) |
| continue |
| # End of: for vtable_entry in vtable.vtable_entries: |
| return symbols_diff, 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: |
| An integer, number of incompatible libraries. |
| """ |
| error_count = 0 |
| symbol_dumps = dict() |
| vtable_dumps = dict() |
| abi_dumps = dict() |
| lib_paths = dict() |
| for dump_rel_path, dump_path in _IterateFiles(dump_dir): |
| if dump_rel_path.endswith("_symbol.dump"): |
| lib_name = dump_rel_path.rpartition("_symbol.dump")[0] |
| symbol_dumps[lib_name] = dump_path |
| elif dump_rel_path.endswith("_vtable.dump"): |
| lib_name = dump_rel_path.rpartition("_vtable.dump")[0] |
| vtable_dumps[lib_name] = dump_path |
| elif dump_rel_path.endswith(".abi.dump"): |
| lib_name = dump_rel_path.rpartition(".abi.dump")[0] |
| abi_dumps[lib_name] = dump_path |
| else: |
| logging.warning("Unknown dump: %s", dump_path) |
| continue |
| lib_paths[lib_name] = None |
| |
| for lib_dir in lib_dirs: |
| for lib_rel_path, lib_path in _IterateFiles(lib_dir): |
| try: |
| vndk_dir = next(x for x in ("vndk", "vndk-sp") if |
| lib_rel_path.startswith(x + os.path.sep)) |
| lib_name = lib_rel_path.replace( |
| vndk_dir, vndk_dir + "-" + dump_version, 1) |
| except StopIteration: |
| lib_name = lib_rel_path |
| |
| if lib_name in lib_paths and not lib_paths[lib_name]: |
| lib_paths[lib_name] = lib_path |
| |
| for lib_name, lib_path in lib_paths.iteritems(): |
| if not lib_path: |
| logging.info("%s: Not found on target", lib_name) |
| continue |
| rel_path = os.path.relpath(lib_path, self._temp_dir) |
| |
| has_exception = False |
| missing_symbols = [] |
| vtable_diff = [] |
| abi_symbols_diff = [] |
| abi_vtables_diff = [] |
| # Compare symbols |
| if lib_name in symbol_dumps: |
| try: |
| missing_symbols = self._DiffSymbols( |
| symbol_dumps[lib_name], lib_path) |
| except (IOError, elf_parser.ElfError): |
| logging.exception("%s: Cannot diff symbols", rel_path) |
| has_exception = True |
| # Compare vtables |
| if lib_name in vtable_dumps: |
| try: |
| vtable_diff = self._DiffVtables( |
| vtable_dumps[lib_name], lib_path) |
| except (IOError, vtable_parser.VtableError): |
| logging.exception("%s: Cannot diff vtables", rel_path) |
| has_exception = True |
| # Compare abidump (lsdump) |
| if lib_name in abi_dumps: |
| try: |
| abi_diff = self._DiffAbi(abi_dumps[lib_name], lib_path) |
| abi_symbols_diff, abi_vtables_diff = abi_diff |
| except (IOError, text_format.ParseError, |
| vtable_dumper.VtableError): |
| logging.exception("%s: Cannot diff abidump", 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 symbol expected actual\n%s", |
| rel_path, |
| "\n".join(" ".join(x) for x in vtable_diff)) |
| if abi_symbols_diff: |
| logging.error("%s: AbiDump Missing Symbols:\n%s", |
| rel_path, "\n".join(abi_symbols_diff)) |
| if abi_vtables_diff: |
| logging.error("%s: Abidump Vtable Difference:\n" |
| "vtable offset expected actual\n%s", |
| rel_path, |
| "\n".join(" ".join(e) for e in abi_vtables_diff)) |
| if (has_exception or missing_symbols or vtable_diff |
| or abi_symbols_diff or abi_vtables_diff): |
| error_count += 1 |
| else: |
| logging.info("%s: Pass", rel_path) |
| return error_count |
| |
| 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) |
| |
| odm_lib_dir = os.path.join( |
| self._temp_dir, "odm_lib_dir_" + self.abi_bitness) |
| vendor_lib_dir = os.path.join( |
| self._temp_dir, "vendor_lib_dir_" + self.abi_bitness) |
| system_lib_dir = os.path.join( |
| self._temp_dir, "system_lib_dir_" + self.abi_bitness) |
| logging.info("host lib dir: %s %s %s", |
| odm_lib_dir, vendor_lib_dir, system_lib_dir) |
| self._PullOrCreateDir( |
| getattr(self, "_ODM_LIB_DIR_" + self.abi_bitness), |
| odm_lib_dir) |
| self._PullOrCreateDir( |
| getattr(self, "_VENDOR_LIB_DIR_" + self.abi_bitness), |
| vendor_lib_dir) |
| self._PullOrCreateDir( |
| getattr(self, "_SYSTEM_LIB_DIR_" + self.abi_bitness), |
| system_lib_dir) |
| |
| error_count = self._ScanLibDirs( |
| dump_dir, [odm_lib_dir, vendor_lib_dir, system_lib_dir], dump_version) |
| asserts.assertEqual(error_count, 0, |
| "Total number of errors: " + str(error_count)) |
| |
| |
| if __name__ == "__main__": |
| test_runner.main() |