Add VTS test for VNDK ABI
am: 0908a48473
Change-Id: I89e6d3a11ed4b11400633caa5baf19ec28aab489
diff --git a/abi/Android.mk b/abi/Android.mk
new file mode 100644
index 0000000..d04ff54
--- /dev/null
+++ b/abi/Android.mk
@@ -0,0 +1,27 @@
+#
+# 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.
+#
+
+LOCAL_PATH := $(call my-dir)
+
+include $(call all-subdir-makefiles)
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE := VtsVndkAbi
+
+VTS_CONFIG_SRC_DIR := testcases/vndk/abi
+include test/vts/tools/build/Android.host_config.mk
+
diff --git a/abi/AndroidTest.xml b/abi/AndroidTest.xml
new file mode 100644
index 0000000..5dd21e0
--- /dev/null
+++ b/abi/AndroidTest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<configuration description="Config for VTS VNDK ABI test cases">
+ <target_preparer class="com.android.compatibility.common.tradefed.targetprep.VtsFilePusher">
+ <option name="push-group" value="HostDrivenTest.push" />
+ </target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.VtsPythonVirtualenvPreparer">
+ </target_preparer>
+ <test class="com.android.tradefed.testtype.VtsMultiDeviceTest">
+ <option name="test-module-name" value="VtsVndkAbi" />
+ <option name="test-case-path" value="vts/testcases/vndk/abi/VtsVndkAbiTest" />
+ </test>
+</configuration>
+
diff --git a/abi/VtsVndkAbiTest.py b/abi/VtsVndkAbiTest.py
new file mode 100644
index 0000000..2dd6797
--- /dev/null
+++ b/abi/VtsVndkAbiTest.py
@@ -0,0 +1,268 @@
+#!/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 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.runners.host import utils
+from vts.utils.python.controllers import android_device
+from vts.utils.python.library import elf_parser
+from vts.utils.python.library import vtable_parser
+
+
+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.
+ _vendor_lib: The directory to which /vendor/lib is copied.
+ _vendor_lib64: The directory to which /vendor/lib64 is copied.
+ _system_lib: The directory to which /system/lib is copied.
+ _system_lib64: The directory to which /system/lib64 is copied.
+ _sdk_version: String, the SDK version supported by the device.
+ data_file_path: The path to VTS data directory.
+ """
+ _TARGET_VENDOR_LIB = "/vendor/lib"
+ _TARGET_VENDOR_LIB64 = "/vendor/lib64"
+ _TARGET_SYSTEM_LIB = "/system/lib"
+ _TARGET_SYSTEM_LIB64 = "/system/lib64"
+ _DUMP_DIR = os.path.join("vts", "testcases", "vndk", "golden")
+
+ 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.registerController(android_device)[0]
+ self._temp_dir = tempfile.mkdtemp()
+ self._vendor_lib = os.path.join(self._temp_dir, "vendor_lib")
+ self._vendor_lib64 = os.path.join(self._temp_dir, "vendor_lib64")
+ self._system_lib = os.path.join(self._temp_dir, "system_lib")
+ self._system_lib64 = os.path.join(self._temp_dir, "system_lib64")
+ logging.info("host lib dir: %s %s %s %s",
+ self._vendor_lib, self._vendor_lib64,
+ self._system_lib, self._system_lib64)
+ self._PullOrCreateDir(self._TARGET_VENDOR_LIB, self._vendor_lib)
+ self._PullOrCreateDir(self._TARGET_SYSTEM_LIB, self._system_lib)
+ if self._dut.is64Bit:
+ self._PullOrCreateDir(self._TARGET_VENDOR_LIB64, self._vendor_lib64)
+ self._PullOrCreateDir(self._TARGET_SYSTEM_LIB64, self._system_lib64)
+
+ cmd = "getprop ro.build.version.sdk"
+ logging.info("adb shell %s", cmd)
+ self._sdk_version = self._dut.adb.shell(cmd).rstrip()
+ logging.info("sdk version: %s", self._sdk_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)
+ pull_output = self._dut.adb.pull(target_dir, host_dir)
+ logging.debug(pull_output)
+
+ 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()
+ finally:
+ parser.Close()
+ logging.debug("%s: %s", lib_path, lib_symbols)
+ 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)
+ 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 _ScanLibDirs(self, dump_dir, lib_dirs):
+ """Compares dump files with libraries copied from device.
+
+ Args:
+ dump_dir: The directory containing dump files.
+ lib_dirs: The list of directories containing libraries.
+
+ Returns:
+ An integer, number of incompatible libraries.
+ """
+ error_count = 0
+ symbol_dumps = dict()
+ vtable_dumps = dict()
+ lib_paths = dict()
+ for root_dir, file_name in utils.iterate_files(dump_dir):
+ dump_path = os.path.join(root_dir, file_name)
+ if file_name.endswith("_symbol.dump"):
+ lib_name = file_name.rpartition("_symbol.dump")[0]
+ symbol_dumps[lib_name] = dump_path
+ elif file_name.endswith("_vtable.dump"):
+ lib_name = file_name.rpartition("_vtable.dump")[0]
+ vtable_dumps[lib_name] = dump_path
+ else:
+ logging.warning("Unknown dump: " + dump_path)
+ continue
+ lib_paths[lib_name] = None
+
+ for lib_dir in lib_dirs:
+ for root_dir, lib_name in utils.iterate_files(lib_dir):
+ if lib_name in lib_paths and not lib_paths[lib_name]:
+ lib_paths[lib_name] = os.path.join(root_dir, lib_name)
+
+ 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 = []
+ # 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
+
+ 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 has_exception or missing_symbols or vtable_diff:
+ error_count += 1
+ else:
+ logging.info("%s: Pass", rel_path)
+ return error_count
+
+ def testAbiCompatibility(self):
+ """Checks ABI compliance of vendor-available libraries."""
+ abi = self._dut.cpu_abi
+ if abi.startswith("arm"):
+ abi_32, abi_64 = "arm", "arm64"
+ elif abi.startswith("x86"):
+ abi_32, abi_64 = "x86", "x86_64"
+ elif abi.startswith("mips"):
+ abi_32, abi_64 = "mips", "mips64"
+ else:
+ asserts.fail("Unknown ABI " + abi)
+ dump_dir_32 = os.path.join(self._DUMP_DIR, self._sdk_version, abi_32)
+ dump_dir_64 = os.path.join(self._DUMP_DIR, self._sdk_version, abi_64)
+
+ logging.info("Check 32-bit libraries")
+ asserts.assertTrue(os.path.isdir(dump_dir_32),
+ "No dump files for SDK version " + self._sdk_version)
+ error_count = self._ScanLibDirs(dump_dir_32, [self._vendor_lib,
+ self._system_lib])
+ if self._dut.is64Bit:
+ logging.info("Check 64-bit libraries")
+ asserts.assertTrue(os.path.isdir(dump_dir_64),
+ "No dump files for SDK version " + self._sdk_version)
+ error_count += self._ScanLibDirs(dump_dir_64, [self._vendor_lib64,
+ self._system_lib64])
+ asserts.assertEqual(error_count, 0,
+ "Total number of errors: " + str(error_count))
+
+
+if __name__ == "__main__":
+ test_runner.main()
diff --git a/abi/__init__.py b/abi/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/abi/__init__.py