aboutsummaryrefslogtreecommitdiff
blob: 4e1b26dbdeee4901fc73c5d036e2d26b38f1dc9e (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
import os
import textwrap
from time import sleep
from unittest.mock import patch

import pytest
from pkgcheck.addons import init_addon
from pkgcheck.addons.eclass import Eclass, EclassAddon
from pkgcheck.base import PkgcheckUserException
from pkgcheck.addons.caches import CacheDisabled
from snakeoil.fileutils import touch
from snakeoil.osutils import pjoin


class TestEclass:

    @pytest.fixture(autouse=True)
    def _setup(self, tmp_path):
        path = str(tmp_path / 'foo.eclass')
        with open(path, 'w') as f:
            f.write(textwrap.dedent("""\
                # eclass header
                foo () { :; }
            """))
        self.eclass1 = Eclass('foo', path)
        path = str(tmp_path / 'bar.eclass')
        self.eclass2 = Eclass('bar', path)

    def test_lines(self):
        assert self.eclass1.lines == ('# eclass header\n', 'foo () { :; }\n')
        assert self.eclass2.lines == ()

    def test_lt(self):
        assert self.eclass2 < self.eclass1
        assert self.eclass1 < 'zoo.eclass'

    def test_hash(self):
        eclasses = {self.eclass1, self.eclass2}
        assert self.eclass1 in eclasses and self.eclass2 in eclasses
        assert {self.eclass1, self.eclass1} == {self.eclass1}

    def test_eq(self):
        assert self.eclass1 == self.eclass1
        assert self.eclass1 == self.eclass1.path
        assert not self.eclass1 == self.eclass2


class TestEclassAddon:

    @pytest.fixture(autouse=True)
    def _setup(self, tool, tmp_path, repo):
        self.repo = repo
        self.cache_dir = str(tmp_path)

        self.eclass_dir = pjoin(repo.location, 'eclass')

        args = ['scan', '--cache-dir', self.cache_dir, '--repo', repo.location]
        options, _ = tool.parse_args(args)
        self.addon = EclassAddon(options)
        self.cache_file = self.addon.cache_file(self.repo)

    def test_cache_disabled(self, tool):
        args = ['scan', '--cache', 'no', '--repo', self.repo.location]
        options, _ = tool.parse_args(args)
        with pytest.raises(CacheDisabled, match='eclass cache support required'):
            init_addon(EclassAddon, options)

    def test_no_eclasses(self):
        self.addon.update_cache()
        assert not os.path.exists(self.cache_file)
        assert not self.addon.eclasses
        assert not self.addon.deprecated

    def test_eclasses(self):
        # non-eclass files are ignored
        for f in ('foo.eclass', 'bar'):
            touch(pjoin(self.eclass_dir, f))
        self.addon.update_cache()
        assert list(self.addon.eclasses) == ['foo']
        assert not self.addon.deprecated

    def test_cache_load(self):
        touch(pjoin(self.eclass_dir, 'foo.eclass'))
        self.addon.update_cache()
        assert list(self.addon.eclasses) == ['foo']

        with patch('pkgcheck.addons.caches.CachedAddon.save_cache') as save_cache:
            self.addon.update_cache()
            # verify the cache was loaded and not regenerated
            save_cache.assert_not_called()
            self.addon.update_cache(force=True)
            # and is regenerated on a forced cache update
            save_cache.assert_called_once()

    def test_outdated_cache(self):
        touch(pjoin(self.eclass_dir, 'foo.eclass'))
        self.addon.update_cache()
        assert list(self.addon.eclasses) == ['foo']

        # increment cache version and dump cache
        cache = self.addon.load_cache(self.cache_file)
        cache.version += 1
        self.addon.save_cache(cache, self.cache_file)

        # verify cache load causes regen
        with patch('pkgcheck.addons.caches.CachedAddon.save_cache') as save_cache:
            self.addon.update_cache()
            save_cache.assert_called_once()

    def test_eclass_changes(self):
        """The cache stores eclass mtimes and regenerates entries if they differ."""
        eclass_path = pjoin(self.eclass_dir, 'foo.eclass')
        touch(eclass_path)
        self.addon.update_cache()
        assert list(self.addon.eclasses) == ['foo']
        sleep(1)
        with open(eclass_path, 'w') as f:
            f.write('# changed eclass\n')
        with patch('pkgcheck.addons.caches.CachedAddon.save_cache') as save_cache:
            self.addon.update_cache()
            save_cache.assert_called_once()

    def test_error_loading_cache(self):
        touch(pjoin(self.eclass_dir, 'foo.eclass'))
        self.addon.update_cache()
        assert list(self.addon.eclasses) == ['foo']

        with patch('pkgcheck.addons.caches.pickle.load') as pickle_load:
            # catastrophic errors are raised
            pickle_load.side_effect = MemoryError('unpickling failed')
            with pytest.raises(MemoryError, match='unpickling failed'):
                self.addon.update_cache()

            # but various load failure exceptions cause cache regen
            pickle_load.side_effect = Exception('unpickling failed')
            with patch('pkgcheck.addons.caches.CachedAddon.save_cache') as save_cache:
                self.addon.update_cache()
                save_cache.assert_called_once()

    def test_error_dumping_cache(self):
        touch(pjoin(self.eclass_dir, 'foo.eclass'))
        # verify IO related dump failures are raised
        with patch('pkgcheck.addons.caches.pickle.dump') as pickle_dump:
            pickle_dump.side_effect = IOError('unpickling failed')
            with pytest.raises(PkgcheckUserException, match='failed dumping eclass cache'):
                self.addon.update_cache()

    def test_eclass_removal(self):
        for name in ('foo', 'bar'):
            touch(pjoin(self.eclass_dir, f'{name}.eclass'))
        self.addon.update_cache()
        assert sorted(self.addon.eclasses) == ['bar', 'foo']
        os.unlink(pjoin(self.eclass_dir, 'bar.eclass'))
        self.addon.update_cache()
        assert list(self.addon.eclasses) == ['foo']

    def test_deprecated(self):
        with open(pjoin(self.eclass_dir, 'foo.eclass'), 'w') as f:
            f.write(textwrap.dedent("""
                # @ECLASS: foo.eclass
                # @MAINTAINER:
                # Random Person <random.person@random.email>
                # @AUTHOR:
                # Random Person <random.person@random.email>
                # @BLURB: Example deprecated eclass with replacement.
                # @DEPRECATED: foo2
        """))
        self.addon.update_cache()
        assert list(self.addon.eclasses) == ['foo']
        assert self.addon.deprecated == {'foo': 'foo2'}