#!/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()