diff options
Diffstat (limited to 'tests/install/test.py')
-rw-r--r-- | tests/install/test.py | 186 |
1 files changed, 186 insertions, 0 deletions
diff --git a/tests/install/test.py b/tests/install/test.py new file mode 100644 index 0000000..996ae09 --- /dev/null +++ b/tests/install/test.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 + +# Copyright 2021 The Bazel Authors. All rights reserved. +# +# 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 itertools +import os +import unittest +import stat +import subprocess + +from rules_python.python.runfiles import runfiles +from pkg.private import manifest + + +class PkgInstallTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.runfiles = runfiles.Create() + # Somewhat of an implementation detail, but it works. I think. + manifest_file = cls.runfiles.Rlocation("rules_pkg/tests/install/test_installer_install_script-install-manifest.json") + + with open(manifest_file, 'r') as fh: + manifest_entries = manifest.read_entries_from(fh) + cls.manifest_data = {} + + for entry in manifest_entries: + cls.manifest_data[entry.dest] = entry + cls.installdir = os.path.join(os.getenv("TEST_TMPDIR"), "installdir") + env = {} + env.update(cls.runfiles.EnvVars()) + subprocess.check_call([ + cls.runfiles.Rlocation("rules_pkg/tests/install/test_installer"), + "--destdir", cls.installdir, + "--verbose", + ], + env=env) + + def entity_type_at_path(self, path): + if os.path.islink(path): + return manifest.ENTRY_IS_LINK + elif os.path.isfile(path): + return manifest.ENTRY_IS_FILE + elif os.path.isdir(path): + return manifest.ENTRY_IS_DIR + else: + # We can't infer what TreeArtifacts are by looking at them -- the + # build system is not aware of their contents. + raise ValueError("Entity {} is not a link, file, or directory") + + def assertEntryTypeMatches(self, entry, actual_path): + actual_entry_type = self.entity_type_at_path(actual_path) + self.assertEqual(actual_entry_type, entry.type, + "Entity {} should be a {}, but was actually {}".format( + entry.dest, + manifest.entry_type_to_string(entry.type), + manifest.entry_type_to_string(actual_entry_type), + )) + + def assertEntryModeMatches(self, entry, actual_path): + # TODO: permissions in windows are... tricky. Don't bother + # testing for them if we're in it for the time being + if os.name == 'nt': + return + + actual_mode = stat.S_IMODE(os.stat(actual_path).st_mode) + expected_mode = int(entry.mode, 8) + self.assertEqual(actual_mode, expected_mode, + "Entry {} has mode {:04o}, expected {:04o}".format( + entry.dest, actual_mode, expected_mode, + )) + + def test_manifest_matches(self): + unowned_dirs = set() + owned_dirs = set() + + # Figure out what directories we are supposed to own, and which ones we + # aren't. + # + # Unowned directories are created implicitly by requesting other + # elements be created or installed. + # + # Owned directories are created explicitly with the pkg_mkdirs rule. + for dest, data in self.manifest_data.items(): + if data.type == manifest.ENTRY_IS_DIR: + owned_dirs.add(dest) + + # TODO(nacl): The initial stage of the accumulation returns an empty string, + # which end up in the set representing the root of the manifest. + # This may not be the best thing. + unowned_dirs.update([p for p in itertools.accumulate(os.path.dirname(dest).split('/'), + func=lambda accum, new: accum + '/' + new)]) + + # In the above loop, unowned_dirs contains all possible directories that + # are in the manifest. Prune them here. + unowned_dirs -= owned_dirs + + # TODO: check for ownership (user, group) + found_entries = {dest: False for dest in self.manifest_data.keys()} + for root, dirs, files in os.walk(self.installdir): + rel_root_path = os.path.relpath(root, self.installdir) + + # The rest of this uses string comparison. To reduce potential + # confusion, ensure that the "." doesn't show up elsewhere. + # + # TODO(nacl) consider using pathlib here, which will reduce the + # need for path cleverness. + if rel_root_path == '.': + rel_root_path = '' + + # TODO(nacl): check for treeartifacts here. If so, prune `dirs`, + # and set the rest aside for future processing. + + # Directory ownership tests + if len(files) == 0 and len(dirs) == 0: + # Empty directories must be explicitly requested by something + if rel_root_path not in self.manifest_data: + self.fail("Directory {} not in manifest".format(rel_root_path)) + + entry = self.manifest_data[rel_root_path] + self.assertEntryTypeMatches(entry, root) + self.assertEntryModeMatches(entry, root) + + found_entries[rel_root_path] = True + else: + # There's something in here. Depending on how it was set up, it + # could either be owned or unowned. + if rel_root_path in self.manifest_data: + entry = self.manifest_data[rel_root_path] + self.assertEntryTypeMatches(entry, root) + self.assertEntryModeMatches(entry, root) + + found_entries[rel_root_path] = True + else: + # If any unowned directories are here, they must be the + # prefix of some entity in the manifest. + self.assertIn(rel_root_path, unowned_dirs) + + for f in files: + # The path on the filesystem in which the file actually exists. + + # TODO(#382): This part of the test assumes that the path + # separator is '/', which is not the case in Windows. However, + # paths emitted in the JSON manifests may also be using + # '/'-separated paths. + # + # Confirm the degree to which this is a problem, and remedy as + # needed. It maybe worth setting the keys in the manifest_data + # dictionary to pathlib.Path or otherwise converting them to + # native paths. + fpath = os.path.normpath("/".join([root, f])) + # The path inside the manifest (relative to the install + # destdir). + rel_fpath = os.path.normpath("/".join([rel_root_path, f])) + if rel_fpath not in self.manifest_data: + self.fail("Entity {} not in manifest".format(rel_fpath)) + + entry = self.manifest_data[rel_fpath] + self.assertEntryTypeMatches(entry, fpath) + self.assertEntryModeMatches(entry, fpath) + + found_entries[rel_fpath] = True + + # TODO(nacl): check for TreeArtifacts + + num_missing = 0 + for dest, present in found_entries.items(): + if present is False: + print("Entity {} is missing from the tree".format(dest)) + num_missing += 1 + self.assertEqual(num_missing, 0) + + +if __name__ == "__main__": + unittest.main() |