Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# (c) Stefan Countryman, 2019
3"""
4Test ``FileHandler`` and other related basic LLAMA classes found in
5``llama.filehandler``. Also provides useful methods for testing ``FileHandler``
6subclasses in general.
7"""
9import os
10from inspect import isabstract
11from abc import ABC, abstractmethod
12import tempfile
13from time import time
14from llama.filehandler import FileHandler
15from llama.pipeline import Pipeline
16from llama.event import Event
17from llama.utils import find_in_submodules
20def make_mock_filehandler(name, deps):
21 """Make a mock filehandler for simple mock pipeline tests. Give it the
22 requested ``DEPENDENCIES`` as ``deps``. Will simply write the current timestamp
23 to file."""
24 def _generate(self):
25 with open(self.fullpath, 'w') as outf:
26 outf.write(str(time()))
27 newclass = type(name, (FileHandler,), {"_generate": _generate,
28 "DEPENDENCIES": tuple(deps),
29 "__module__": __name__,
30 "FILENAME": name+'.txt'})
31 FileHandler.set_class_attributes(newclass)
32 return newclass
35MockFh1 = make_mock_filehandler("MockFh1", [])
36MockFh2 = make_mock_filehandler("MockFh2", [MockFh1])
37MockFh3 = make_mock_filehandler("MockFh3", [MockFh1, MockFh2])
38MockFh4 = make_mock_filehandler("MockFh4", [MockFh2, MockFh3])
39MockFh5 = make_mock_filehandler("MockFh5", [MockFh1, MockFh4])
40MockPipeline = Pipeline(MockFh1, MockFh2, MockFh3, MockFh4, MockFh5)
43class AbstractTestObsolescence(ABC):
44 r"""Test our ability to mark files as obsolete in the expected ways (with
45 the expected effects on file generation). These test are run with a mock
46 ``Pipeline`` with topology (directed left-to-right):
48 .. code::
50 ___________
51 / _____ \
52 / / \ \
53 1---2---3---4---5
54 \_____/
56 This allows for tests of a few different scenarios in which file generation
57 ordering can be tested to have the correct ordering. As a trivial example,
58 changing 1 will obsolete all remaining ``FileHandler`` instances, but
59 because they depend on one another in sequence, the regeneration order must
60 be in increasing order of label value. This lets us test methods related to
61 checking file obsolescence and reacting by updating output files.
62 """
64 EVENTID = 'obsolescence'
66 @property
67 def event(self):
68 """The event for this test."""
69 return Event(self.EVENTID, rundir=self.rundir, pipeline=MockPipeline)
71 @property
72 def rundir(self):
73 """The rundir for this test (if it is set). Raises an
74 ``AttributeError`` if none has yet been specified."""
75 return getattr(self, '_rundir')
77 @rundir.setter
78 def rundir(self, rundir):
79 setattr(self, '_rundir', os.path.abspath(rundir))
81 @abstractmethod
82 def main(self):
83 """Run the main part of the test (after the temporary test directory
84 has been set up and the files in the ``MockPipeline`` have been
85 generated). This is where you should put obsolescence-related test
86 logic.
87 """
89 def sorted_by_generation_time(self):
90 """Get a list of the generated FileHandlers in temporal order of file
91 generation."""
92 return sorted((f for f in self.event.files.values() if f.exists()),
93 key=lambda fh: fh.meta.generation_time)
95 def test(self):
96 """Run the ``main`` test after having set everything up in a temporary
97 test directory."""
98 with tempfile.TemporaryDirectory() as tmpdir:
99 self.rundir = tmpdir
100 event = self.event
101 event.init()
102 event.files.MockFh1.generate()
103 event.files.update()
104 assert all(f.exists() for f in event.files.values())
105 self.main()
108class TestObsolescenceAndLocking(AbstractTestObsolescence):
109 """
110 Test ``FileHandler``'s ability to mark files as obsolete, then lock those
111 files, marking them as non-obsolete in perpetuity, then unlock them again,
112 returning to the original state.
113 """
115 def assert_obsolete(self):
116 """
117 Check that everything else is obsolete after changing ``MockFh1``.
118 """
119 files = self.event.files
120 assert files.MockFh2.is_obsolete()
121 assert files.MockFh3.is_obsolete()
122 assert files.MockFh4.is_obsolete()
123 assert files.MockFh5.is_obsolete()
125 def main(self):
126 event = self.event
127 files = event.files
128 git = event.git
129 assert all(not fh.is_obsolete() for fh in files.values())
130 files.MockFh1.delete()
131 files.MockFh1.generate()
132 self.assert_obsolete()
133 files.MockFh2.lock.lock()
134 assert not files.MockFh2.is_obsolete()
135 assert files.MockFh3.is_obsolete()
136 assert files.MockFh4.is_obsolete()
137 assert files.MockFh5.is_obsolete()
138 files.MockFh3.lock.lock()
139 assert not files.MockFh2.is_obsolete()
140 assert not files.MockFh3.is_obsolete()
141 assert not files.MockFh4.is_obsolete()
142 assert files.MockFh5.is_obsolete()
143 files.MockFh5.lock.lock()
144 assert all(not fh.is_obsolete() for fh in files.values())
145 assert not files.update()
146 assert len(git.hashes(files.MockFh1.FILENAME)) == 3
147 assert len(git.hashes(files.MockFh2.FILENAME)) == 1
148 assert len(git.hashes(files.MockFh3.FILENAME)) == 1
149 assert len(git.hashes(files.MockFh4.FILENAME)) == 1
150 assert len(git.hashes(files.MockFh5.FILENAME)) == 1
151 files.MockFh2.lock.unlock()
152 files.MockFh3.lock.unlock()
153 files.MockFh5.lock.unlock()
154 self.assert_obsolete()
157class TestGenerationOrder(AbstractTestObsolescence):
158 """
159 Make sure that, by obsoleting a file, we end up regenerating its
160 descendants in the correct order.
161 """
163 def main(self):
164 """Make sure we regenerate files in the correct order."""
165 event = self.event
166 files = event.files
167 git = event.git
168 assert len(git.hashes(files.MockFh1.FILENAME)) == 1
169 assert len(git.hashes(files.MockFh2.FILENAME)) == 1
170 assert len(git.hashes(files.MockFh3.FILENAME)) == 1
171 assert len(git.hashes(files.MockFh4.FILENAME)) == 1
172 assert len(git.hashes(files.MockFh5.FILENAME)) == 1
173 files.MockFh1.delete()
174 files.MockFh1.generate()
175 assert files.update()
176 assert self.sorted_by_generation_time() == [
177 files.MockFh1,
178 files.MockFh2,
179 files.MockFh3,
180 files.MockFh4,
181 files.MockFh5,
182 ]
183 assert len(git.hashes(files.MockFh1.FILENAME)) == 3
184 assert len(git.hashes(files.MockFh2.FILENAME)) == 2
185 assert len(git.hashes(files.MockFh3.FILENAME)) == 2
186 assert len(git.hashes(files.MockFh4.FILENAME)) == 2
187 assert len(git.hashes(files.MockFh5.FILENAME)) == 2
190def check_required_attributes(filehandler: FileHandler, err: bool = False):
191 """Return ``False`` if ``filehandler`` does not have all of its required
192 class constants (in ``filehandler.required_attributes()``) defined. If
193 ``err`` is ``True``, raise an assertion error instead of returning
194 ``False``. Return ``True`` if all required attributes are set (meaning that
195 this ``FileHandler`` class is valid and ready to use)."""
196 for required in filehandler.required_attributes():
197 if getattr(filehandler, required, None) is None:
198 if err:
199 raise AssertionError("Must define class attribute "
200 f"``{required}`` for ``{filehandler}``")
201 return False
202 return True
205def check_filehandler_definition_consistency(filehandler: FileHandler):
206 """Check whether a ``FileHandler`` has been consistently defined, raising
207 an ``AssertionError`` if not."""
208 check_required_attributes(filehandler, err=True)
209 for atr in ("DEPENDENCIES", "_generate", "DEP_CHECKSUM_KWARGS"):
210 manifest_atr = {getattr(c, atr) for c in filehandler.MANIFEST_TYPES}
211 if len(manifest_atr) != 1:
212 raise AssertionError("All entries in ``MANIFEST_TYPES`` must "
213 f"have the same {atr}; instead, got "
214 f"these: {manifest_atr} from this "
215 f"manifest: {filehandler.MANIFEST_TYPES}")
218def implemented_filehandler(item):
219 """Check whether an object is an implemented ``FileHandler``
220 (not abstract, all required attributes set)."""
221 if isinstance(item, type):
222 if issubclass(item, FileHandler):
223 return ((not isabstract(item))
224 and check_required_attributes(item, err=False))
225 return False
228def test_filehandler_definition_consistency():
229 """Run ``check_filehandler_definition_consistency`` on all ``FileHandler``
230 classes that can be found defined in ``llama`` and its submodules that have
231 their ``required_attributes`` set."""
232 candidates = set(find_in_submodules("llama",
233 implemented_filehandler).values())
234 for filehandler in candidates:
235 check_filehandler_definition_consistency(filehandler)