Hide keyboard shortcuts

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 

2 

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""" 

8 

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 

18 

19 

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 

33 

34 

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) 

41 

42 

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): 

47 

48 .. code:: 

49 

50 ___________ 

51 / _____ \ 

52 / / \ \ 

53 1---2---3---4---5 

54 \_____/ 

55 

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 """ 

63 

64 EVENTID = 'obsolescence' 

65 

66 @property 

67 def event(self): 

68 """The event for this test.""" 

69 return Event(self.EVENTID, rundir=self.rundir, pipeline=MockPipeline) 

70 

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') 

76 

77 @rundir.setter 

78 def rundir(self, rundir): 

79 setattr(self, '_rundir', os.path.abspath(rundir)) 

80 

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 """ 

88 

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) 

94 

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() 

106 

107 

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 """ 

114 

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() 

124 

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() 

155 

156 

157class TestGenerationOrder(AbstractTestObsolescence): 

158 """ 

159 Make sure that, by obsoleting a file, we end up regenerating its 

160 descendants in the correct order. 

161 """ 

162 

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 

188 

189 

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 

203 

204 

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}") 

216 

217 

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 

226 

227 

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)