aboutsummaryrefslogtreecommitdiff
path: root/tests/install/test.py
blob: 996ae09195b4f15dbcdd4f5a76f875f9908a4916 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
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()