#!/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()