| #!/usr/bin/env python |
| # -*- coding: 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. |
| # |
| |
| u"""A test module to verify root directory content. |
| |
| This test checks the root directory content on a device is the same as the |
| following structure, or a subset of it. For symlinks, the target value it |
| points to is checked. For *.rc files, the file content is compared, except |
| init.environ.rc because it's *built* from init.environ.rc.in. For other |
| files (e.g., init, .png), we allow their existence and don't check the file |
| content. |
| |
| Root Directory Content: |
| ├── acct |
| ├── adb_keys |
| ├── bugreports -> /data/user_de/0/com.android.shell/files/bugreports |
| ├── cache OR |
| ├── cache -> /data/cache |
| ├── charger -> /sbin/charger |
| ├── config |
| ├── d -> /sys/kernel/debug |
| ├── data |
| ├── default.prop -> system/etc/prop.default |
| ├── dev |
| ├── etc -> /system/etc |
| ├── init |
| ├── init.*.rc |
| ├── lost+found |
| ├── mnt |
| ├── odm |
| ├── oem |
| ├── postinstall |
| ├── proc |
| ├── res |
| │ └── images |
| │ └── charger |
| │ ├── battery_fail.png |
| │ └── battery_scale.png |
| ├── sbin |
| │ ├── charger |
| │ ├── ueventd -> ../init |
| │ └── watchdogd -> ../init |
| ├── sdcard -> /storage/self/primary |
| ├── storage |
| ├── sys |
| ├── system |
| ├── ueventd.rc |
| ├── vendor |
| ├── verity_key |
| """ |
| |
| import filecmp |
| import glob |
| 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.utils.python.file import target_file_utils |
| |
| # The allowed directories in three types: |
| # - mount_point: skip checking its content as it's just a mount point. |
| # - skip_check: skip checking its content for special directories. |
| # - check_content: check its content recursively. |
| _ALLOWED_DIRS = { |
| "/acct": "mount_point", |
| # /cache is created when BOARD_CACHEIMAGE_FILE_SYSTEM_TYPE is defined. |
| "/cache": "mount_point", |
| "/config": "mount_point", |
| "/data": "mount_point", |
| "/dev": "mount_point", |
| "/lost+found": "skip_check", # Skip checking this created by fsck. |
| "/mnt": "mount_point", |
| "/odm": "mount_point", |
| "/oem": "mount_point", |
| # The A/B updater uses a top-level /postinstall directory to mount the new |
| # system before reboot. |
| "/postinstall": "skip_check", |
| "/proc": "mount_point", |
| "/res": "check_content", |
| "/res/images": "check_content", |
| "/res/images/charger": "check_content", |
| "/sbin": "check_content", |
| "/storage": "mount_point", |
| "/sys": "mount_point", |
| # /system is a mount point in non-A/B but a directory in A/B. |
| # No need to check its content in both cases. |
| "/system": "skip_check", |
| "/vendor": "mount_point", |
| } |
| |
| # The allowed symlinks and the target it points to. |
| # The test will check the value of the symlink target. |
| _ALLOWED_SYMLINKS = { |
| "/bugreports": "/data/user_de/0/com.android.shell/files/bugreports", |
| "/cache": "/data/cache", |
| "/charger": "/sbin/charger", |
| "/d": "/sys/kernel/debug", |
| "/default.prop": "system/etc/prop.default", |
| "/etc": "/system/etc", |
| "/sbin/ueventd": "../init", |
| "/sbin/watchdogd": "../init", |
| "/sdcard": "/storage/self/primary", |
| } |
| |
| # The allowed files for existence, where file content won't be checked. |
| # Note that init.environ.rc is generated by replacing some build |
| # environment variables (e.g., BOOTCLASSPATH) from its source: |
| # init.environ.rc.in, therefore its content is also not checked. |
| _ALLOWED_EXISTING_FILES = set(["/adb_keys", |
| "/init", |
| "/init.environ.rc", |
| "/res/images/charger/battery_scale.png", |
| "/res/images/charger/battery_fail.png", |
| "/sbin/charger", |
| "/verity_key"]) |
| |
| |
| class VtsKernelRootDirTest(base_test.BaseTestClass): |
| """A test class to verify root directory content. |
| |
| Attributes: |
| data_file_path: The path to VTS data directory. |
| """ |
| |
| _INIT_RC_FILE_PATH = "vts/testcases/kernel/api/rootdir/init_rc_files" |
| |
| def setUpClass(self): |
| """Initializes data file path, device, shell and adb.""" |
| required_params = [keys.ConfigKeys.IKEY_DATA_FILE_PATH] |
| self.getUserParams(required_params) |
| |
| self._dut = self.android_devices[0] |
| self._shell = self._dut.shell |
| self._adb = self._dut.adb |
| |
| self._dirs_to_check = self._TraverseRootDir() |
| logging.info("Dirs to check: %r", self._dirs_to_check) |
| |
| def setUp(self): |
| """Initializes the temp_dir for adb pull.""" |
| self._temp_dir = tempfile.mkdtemp() |
| logging.info("Create %s", self._temp_dir) |
| |
| def tearDown(self): |
| """Deletes the temporary directory.""" |
| logging.info("Delete %s", self._temp_dir) |
| shutil.rmtree(self._temp_dir) |
| |
| def _ListDir(self, dir_path, file_type="all"): |
| """Lists files in dir_path with specific file_type. |
| |
| Args: |
| dir_path: The current directory to list content. |
| file_type: The file type to list, can be one of "dir", "file", |
| "symlink" or "all". |
| |
| Returns: |
| A set of paths under current directory. |
| """ |
| find_option = "-maxdepth 1" # Only list current directory. |
| find_types = { |
| "dir": "d", |
| "file": "f", |
| "symlink": "l", |
| } |
| if file_type != "all": |
| find_option += " -type %s" % find_types[file_type] |
| |
| # FindFiles will include dir_path if file_type is "all" or "dir". |
| # Excludes dir_path before return. |
| return set(target_file_utils.FindFiles( |
| self._shell, dir_path, "*", find_option)) - set([dir_path]) |
| |
| def _ReadLink(self, path): |
| """Executes readlink on device.""" |
| result = self._shell.Execute("readlink %s" % path) |
| asserts.assertEqual(result[const.EXIT_CODE][0], 0) |
| return result[const.STDOUT][0].strip() |
| |
| def _TraverseRootDir(self, dir_path="/"): |
| """Returns a list of dirs to check the content in it.""" |
| # dir_path is eligible to check when being invoked here. |
| dirs = [dir_path] |
| for d in self._ListDir(dir_path, "dir"): |
| if d in _ALLOWED_DIRS and _ALLOWED_DIRS[d] == "check_content": |
| dirs.extend(self._TraverseRootDir(d)) |
| return dirs |
| |
| def testRootDirs(self): |
| """Checks the subdirs under root directory.""" |
| error_msg = [] |
| |
| for dir_path in self._dirs_to_check: |
| current_dirs = self._ListDir(dir_path, "dir") |
| logging.info("Current dirs: %r", current_dirs) |
| |
| unexpected_dirs = current_dirs - set(_ALLOWED_DIRS) |
| error_msg.extend("Unexpected dir: " + d for d in unexpected_dirs) |
| |
| if error_msg: |
| asserts.fail("UNEXPECTED ROOT DIRS:\n%s" % "\n".join(error_msg)) |
| |
| def testRootSymlinks(self): |
| """Checks the symlinks under root directory.""" |
| error_msg = [] |
| |
| def _CheckSymlinks(dir_path): |
| """Checks the symlinks under dir_path.""" |
| current_symlinks = self._ListDir(dir_path, "symlink") |
| logging.info("Current symlinks: %r", current_symlinks) |
| |
| unexpected_symlinks = current_symlinks - set(_ALLOWED_SYMLINKS) |
| error_msg.extend( |
| "Unexpected symlink: " + l for l in unexpected_symlinks) |
| |
| # Checks symlink target. |
| error_msg.extend( |
| "Invalid symlink target: %s -> %s (expected: %s)" % ( |
| l, target, _ALLOWED_SYMLINKS[l]) for l, target in ( |
| (l, self._ReadLink(l)) for l in ( |
| current_symlinks - unexpected_symlinks)) |
| if target != _ALLOWED_SYMLINKS[l]) |
| |
| for dir_path in self._dirs_to_check: |
| _CheckSymlinks(dir_path) |
| |
| if error_msg: |
| asserts.fail("UNEXPECTED ROOT SYMLINKS:\n%s" % "\n".join(error_msg)) |
| |
| def testRootFiles(self): |
| """Checks the files under root directory.""" |
| error_msg = [] |
| |
| init_rc_dir = os.path.join(self.data_file_path, self._INIT_RC_FILE_PATH) |
| allowed_rc_files = set("/" + os.path.basename(rc_file) for rc_file in |
| glob.glob(os.path.join(init_rc_dir, "*.rc"))) |
| |
| def _CheckFiles(dir_path): |
| """Checks the files under dir_path.""" |
| current_files = self._ListDir(dir_path, "file") |
| logging.info("Current files: %r", current_files) |
| |
| unexpected_files = (current_files - |
| allowed_rc_files - |
| _ALLOWED_EXISTING_FILES) |
| error_msg.extend("Unexpected file: " + f for f in unexpected_files) |
| |
| # Checks file content in *.rc files. |
| for f in current_files - unexpected_files: |
| if f in allowed_rc_files: |
| # adb pull the .rc file from the device. |
| logging.info("adb pull %s %s", f, self._temp_dir) |
| pull_output = self._adb.pull(f, self._temp_dir) |
| logging.debug(pull_output) |
| # Compares the content and trim the leading "/" in f. |
| if not filecmp.cmp(os.path.join(init_rc_dir, f[1:]), |
| os.path.join(self._temp_dir, f[1:])): |
| error_msg.append("Unexpected file content: %s" % f) |
| |
| for dir_path in self._dirs_to_check: |
| _CheckFiles(dir_path) |
| |
| if error_msg: |
| asserts.fail("UNEXPECTED ROOT FILES:\n%s" % "\n".join(error_msg)) |
| |
| def testRootAllFileTypes(self): |
| """Checks there is no path other than dirs, symlinks and files.""" |
| error_msg = [] |
| |
| for dir_path in self._dirs_to_check: |
| unknown_files = (self._ListDir(dir_path) - |
| self._ListDir(dir_path, "dir") - |
| self._ListDir(dir_path, "symlink") - |
| self._ListDir(dir_path, "file")) |
| error_msg.extend("Unexpected path: " + p for p in unknown_files) |
| |
| if error_msg: |
| asserts.fail("UNEXPECTED ROOT PATHS:\n%s" % "\n".join(error_msg)) |
| |
| |
| if __name__ == "__main__": |
| test_runner.main() |