summaryrefslogtreecommitdiff
path: root/abi/vts_vndk_abi_test.py
diff options
context:
space:
mode:
Diffstat (limited to 'abi/vts_vndk_abi_test.py')
-rw-r--r--abi/vts_vndk_abi_test.py361
1 files changed, 361 insertions, 0 deletions
diff --git a/abi/vts_vndk_abi_test.py b/abi/vts_vndk_abi_test.py
new file mode 100644
index 0000000..56a5220
--- /dev/null
+++ b/abi/vts_vndk_abi_test.py
@@ -0,0 +1,361 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 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
+import unittest
+
+from vts.testcases.vndk import utils
+from vts.testcases.vndk.golden import vndk_data
+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(unittest.TestCase):
+ """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.
+ """
+
+ def setUp(self):
+ """Initializes data file path, device, and temporary directory."""
+ serial_number = os.environ.get("ANDROID_SERIAL", "")
+ self.assertTrue(serial_number, "$ANDROID_SERIAL is empty")
+ self._dut = utils.AndroidDevice(serial_number)
+ self._temp_dir = tempfile.mkdtemp()
+
+ def tearDown(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 self._dut.IsDirectory(target_dir):
+ 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.AdbPull(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.
+ expected_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.
+ bitness: 32 or 64, the size of the vtable entries.
+
+ 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.items()))
+
+ 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 * dumper.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_zip, dump_paths, lib_dirs, dump_version):
+ """Compares dump files with libraries copied from device.
+
+ Args:
+ dump_zip: A zip_file.ZipFile object containing the dumps.
+ dump_paths: A dict of {library name: dump resource path}.
+ 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 = []
+ lib_paths = dict()
+ for lib_dir in lib_dirs:
+ for parent_dir, dir_names, lib_names in os.walk(lib_dir):
+ for lib_name in lib_names:
+ 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.items():
+ 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 dump_zip.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, bitness):
+ """Checks ABI compliance of VNDK libraries.
+
+ Args:
+ bitness: 32 or 64, the bitness of the tested libraries.
+ """
+ self.assertTrue(self._dut.IsRoot(), "This test requires adb root.")
+ primary_abi = self._dut.GetCpuAbiList()[0]
+ binder_bitness = self._dut.GetBinderBitness()
+ self.assertTrue(binder_bitness, "Cannot determine binder bitness.")
+ dump_version = self._dut.GetVndkVersion()
+ self.assertTrue(dump_version, "Cannot determine VNDK version.")
+
+ dump_paths = vndk_data.GetAbiDumpPathsFromResources(
+ dump_version,
+ binder_bitness,
+ primary_abi,
+ bitness)
+ self.assertTrue(
+ dump_paths,
+ "No dump files. version: %s ABI: %s bitness: %d" % (
+ dump_version, primary_abi, bitness))
+
+ target_dirs = vndk_utils.GetVndkExtDirectories(bitness)
+ target_dirs += vndk_utils.GetVndkSpExtDirectories(bitness)
+ target_dirs += [vndk_utils.GetVndkDirectory(bitness, dump_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)
+
+ with vndk_data.AbiDumpResource() as dump_resource:
+ assert_lines = self._ScanLibDirs(dump_resource.zip_file,
+ dump_paths, 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))
+ self.fail("\n".join(assert_lines))
+
+ def testAbiCompatibility32(self):
+ """Checks ABI compliance of 32-bit VNDK libraries."""
+ self._TestAbiCompatibility(32)
+
+ def testAbiCompatibility64(self):
+ """Checks ABI compliance of 64-bit VNDK libraries."""
+ if self._dut.GetCpuAbiList(64):
+ self._TestAbiCompatibility(64)
+ else:
+ logging.info("Skip the test as the device doesn't support 64-bit "
+ "ABI.")
+
+
+if __name__ == "__main__":
+ unittest.main()