Add test case for VNDK dependency
Hsin-Yi Chen [Sat, 18 Feb 2017 09:58:36 +0000 (17:58 +0800)]
Add VendorDependencyTest for the 2nd rule mentioned in the issue.

Test: vts-tradefed run commandAndExit vts-vndk
Bug: 35121072
Change-Id: I6a6f2985da695dde60d53f4b8c0937afe1e81374

__init__.py [new file with mode: 0644]
dependency/Android.mk [new file with mode: 0644]
dependency/AndroidTest.xml [new file with mode: 0644]
dependency/VtsVndkDependencyTest.py [new file with mode: 0644]
dependency/__init__.py [new file with mode: 0644]
dependency/elf_parser.py [new file with mode: 0644]

diff --git a/__init__.py b/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/dependency/Android.mk b/dependency/Android.mk
new file mode 100644 (file)
index 0000000..15af37a
--- /dev/null
@@ -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 := VtsVndkDependencyTest
+
+VTS_CONFIG_SRC_DIR := testcases/vndk/dependency
+include test/vts/tools/build/Android.host_config.mk
+
diff --git a/dependency/AndroidTest.xml b/dependency/AndroidTest.xml
new file mode 100644 (file)
index 0000000..706e0ef
--- /dev/null
@@ -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 dependency 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="VtsVndkDependencyTest" />
+        <option name="test-case-path" value="vts/testcases/vndk/dependency/VtsVndkDependencyTest" />
+    </test>
+</configuration>
+
diff --git a/dependency/VtsVndkDependencyTest.py b/dependency/VtsVndkDependencyTest.py
new file mode 100644 (file)
index 0000000..36f5241
--- /dev/null
@@ -0,0 +1,147 @@
+#!/usr/bin/env python3.4
+#
+# 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 re
+import shutil
+import tempfile
+
+from vts.runners.host import asserts
+from vts.runners.host import base_test_with_webdb
+from vts.runners.host import test_runner
+from vts.runners.host import utils
+from vts.utils.python.controllers import android_device
+from vts.testcases.vndk.dependency import elf_parser
+
+class VtsVndkDependencyTest(base_test_with_webdb.BaseTestWithWebDbClass):
+    """A test case to verify vendor library dependency.
+
+    Attributes:
+        _temp_dir: The temporary directory to which the vendor partition is
+            copied.
+        _vendor_libs: Collection of strings. The names of the shared libraries
+            on vendor partition.
+    """
+    _SHELL_NAME = "vendor_dep_test_shell"
+    _VENDOR_PATH = "/vendor"
+    _LOW_LEVEL_NDK = [
+        "libc.so",
+        "libm.so",
+        "libz.so",
+        "liblog.so",
+        "libdl.so",
+        "libstdc++.so"
+    ]
+    _SAME_PROCESS_NDK = [re.compile(p) for p in [
+        "libEGL_.*\\.so$",
+        "libGLESv1_CM_.*\\.so$",
+        "libGLESv2_.*\\.so$",
+        "libGLESv3_.*\\.so$",
+        "vulkan.*\\.so$",
+        "libRSDriver.*\\.so$",
+        "libPVRRS\\.so$",
+        "gralloc-mapper@\\d+.\\d+-impl\\.so$",
+    ]]
+
+    def setUpClass(self):
+        """Initializes device and temporary directory."""
+        self.dut = self.registerController(android_device)[0]
+        self.dut.shell.InvokeTerminal(self._SHELL_NAME)
+        self._temp_dir = tempfile.mkdtemp()
+        self._vendor_libs = []
+
+    def tearDownClass(self):
+        """Deletes the temporary directory."""
+        logging.info("Delete %s", self._temp_dir)
+        shutil.rmtree(self._temp_dir)
+
+    def _isAllowedDependency(self, lib_name):
+        """Checks whether a library dependency is allowed.
+
+        A vendor library/executable is only allowed to depend on
+        - Low-level NDK
+        - Same-process NDK
+        - Other libraries on vendor partition
+
+        Args:
+            lib_name: String. The name of the depended library.
+
+        Returns:
+            A boolean representing whether the library is allowed.
+        """
+        if lib_name in self._vendor_libs or lib_name in self._LOW_LEVEL_NDK:
+            return True
+        for pattern in self._SAME_PROCESS_NDK:
+            if pattern.match(lib_name):
+                return True
+        return False
+
+    def _listSharedLibraries(self, path):
+        """Finds all shared libraries under a directory.
+
+        Args:
+            path: String. The path to search.
+
+        Returns:
+            Set of strings. The names of the found libraries.
+        """
+        results = set()
+        for root_dir, dir_names, file_names in os.walk(path):
+            for file_name in file_names:
+                if file_name.endswith(".so"):
+                    results.add(file_name)
+        return results
+
+    def testElfDependency(self):
+        """Scans library/executable dependency on vendor partition."""
+        if not elf_parser.ElfParser.isSupported():
+            asserts.fail("readelf is not available")
+        logging.info("adb pull %s %s", self._VENDOR_PATH, self._temp_dir)
+        pull_output = self.dut.adb.pull(self._VENDOR_PATH, self._temp_dir)
+        logging.debug(pull_output)
+        self._vendor_libs = self._listSharedLibraries(self._temp_dir)
+        logging.info("Vendor libraries: " + str(self._vendor_libs))
+        error_count = 0
+        for root_dir, dir_names, file_names in os.walk(self._temp_dir):
+            for file_name in file_names:
+                file_path = os.path.join(root_dir, file_name)
+                elf = elf_parser.ElfParser(file_path)
+                if not elf.isValid():
+                    logging.info("%s is not an ELF file", file_path)
+                    continue
+                try:
+                    dep_libs = elf.listDependencies()
+                except OSError as e:
+                    error_count += 1
+                    logging.exception("Cannot read %s: %s", file_path, str(e))
+                    continue
+                logging.info("%s depends on: %s", file_path, str(dep_libs))
+                disallowed_libs = filter(
+                        lambda x: not self._isAllowedDependency(x), dep_libs)
+                if len(disallowed_libs) == 0:
+                    continue
+                error_count += 1
+                logging.error("%s depends on disallowed libs: %s",
+                        file_path.replace(self._temp_dir, "", 1),
+                        str(disallowed_libs))
+        asserts.assertEqual(error_count, 0,
+                "Total number of errors: " + str(error_count))
+
+if __name__ == "__main__":
+    test_runner.main()
+
diff --git a/dependency/__init__.py b/dependency/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/dependency/elf_parser.py b/dependency/elf_parser.py
new file mode 100644 (file)
index 0000000..50e2584
--- /dev/null
@@ -0,0 +1,81 @@
+#!/usr/bin/env python3.4
+#
+# 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 os
+import re
+
+from vts.runners.host import utils
+
+class ElfParser(object):
+    """This class reads an ELF file by parsing output of the command readelf.
+
+    Attributes:
+        _file_path: The path to the ELF file.
+    """
+
+    def __init__(self, file_path):
+        self._file_path = file_path
+
+    @staticmethod
+    def isSupported():
+        """Checks whether readelf is available."""
+        try:
+            utils.exe_cmd("readelf", "--version")
+            return True
+        except OSError:
+            return False
+
+    def isValid(self):
+        """Checks size and first 4 bytes of the ELF file.
+
+        Returns:
+            A boolean representing whether _file_path is a valid ELF.
+        """
+        try:
+            size = os.path.getsize(self._file_path)
+            # must be larger than 32-bit file header
+            if size < 52:
+                return False
+        except OSError:
+            return False
+        try:
+            with open(self._file_path, "rb") as f:
+                magic = f.read(4)
+                if list(bytearray(magic)) != [0x7f, 0x45, 0x4c, 0x46]:
+                    return False
+        except IOError:
+            return False
+        return True
+
+    def listDependencies(self):
+        """Lists the shared libraries that the ELF depends on.
+
+        Returns:
+            List of strings. The names of the depended libraries.
+
+        Raises:
+            OSError if readelf fails.
+        """
+        pattern = re.compile("\\(NEEDED\\)\\s*Shared library: \[(.+)\]")
+        output = utils.exe_cmd("readelf", "--dynamic", self._file_path)
+        results = []
+        for line in output.split("\n"):
+            match = pattern.search(line)
+            if match:
+                results.append(match.group(1))
+        return results
+