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

4Classes used in unit tests for LLAMA. 

5""" 

6 

7import os 

8import tempfile 

9import shutil 

10from abc import ABC, abstractmethod, abstractproperty 

11import warnings 

12from llama.event import Event 

13from llama.test import TEST_RUN_INPUTS 

14from llama.run import Run 

15 

16 

17class AbstractFileGenerationComparator(ABC): 

18 """ 

19 A class for running a test that produces some sort of expected output 

20 files for a given event and testing whether those output files are as 

21 expected by comparing them to reference files that are assumed correct. 

22 This class should be used to implement unit tests for the actual file 

23 generation steps of each step in the pipeline (including external 

24 triggering). 

25 """ 

26 

27 INPUT_RUN = TEST_RUN_INPUTS 

28 

29 @property 

30 def run(self): 

31 """A temporary ``Run`` instance for tests (probably using a ``pytest`` 

32 fixture) where the test outputs for this test's ``EVENTID`` will 

33 reside. Feel free to override this with a custom directory.""" 

34 return getattr(self, '_run', None) 

35 

36 @run.setter 

37 def run(self, run): 

38 """Set the run value.""" 

39 setattr(self, '_run', run) 

40 

41 # pylint: disable=invalid-name 

42 @abstractproperty 

43 def EVENTID(self): # pylint: disable=invalid-name 

44 """The ``eventid`` (i.e. the directory name) of the scenario that is 

45 being tested. Must correspond to an ``Event`` in ``TEST_RUN_INPUTS`` 

46 (i.e. ``EVENTID`` must be a directory in ``run.rundir`` for some 

47 ``run`` in ``TEST_RUN_INPUTS``).""" 

48 

49 # pylint: disable=invalid-name 

50 @abstractproperty 

51 def STARTING_MANIFEST(self): # pylint: disable=invalid-name 

52 """A tuple of specific ``FileHandler`` classes whose files should be 

53 copied from the input directory in ``TEST_RUN_INPUTS`` to the temporary 

54 output test directory. This will set the starting state for the 

55 test.""" 

56 

57 @abstractproperty 

58 def pipeline(self): # pylint: disable=invalid-name 

59 """A ``Pipeline`` instance defining the output files 

60 generated by this test. These are the output files that will be 

61 compared by ``AbstractFileGenerationComparator.compare`` to make sure 

62 that all outputs are as expected (and, as a sanity check, that no 

63 inputs have been mutated).""" 

64 

65 @abstractmethod 

66 def execute(self): 

67 """Run the actual test (no need to compare outputs; this will be done 

68 in ``compare``).""" 

69 

70 @property 

71 def inputevent(self): 

72 """An ``Event`` instance corresponding to the input files of this 

73 test.""" 

74 return Event(self.EVENTID, rundir=self.INPUT_RUN.rundir, 

75 pipeline=self.pipeline) 

76 

77 @property 

78 def event(self): 

79 """An ``Event`` instance corresponding to the output files of this 

80 test. Use this instance to get e.g. file locations if necessary.""" 

81 return Event(self.EVENTID, rundir=self.run.rundir, 

82 pipeline=self.pipeline) 

83 

84 def provision(self): 

85 """Link or copy input files from ``STARTING_MANIFEST`` into 

86 ``event.eventdir`` so that they are available for ``execute``. The test 

87 directory is presumed to exist.""" 

88 for filehandler in self.STARTING_MANIFEST: 

89 source = filehandler(self.inputevent) 

90 dest = filehandler(self.event) 

91 try: 

92 os.link(source, dest) 

93 except IOError: 

94 shutil.copyfile(source, dest) 

95 

96 def compare(self): 

97 """Compare the outputs of ``execute`` in the test ``eventdir`` to the 

98 expected values in the input ``Event`` from ``TEST_RUN_INPUTS``. Checks 

99 whether the file contents are strictly equal. 

100 

101 Returns 

102 ------- 

103 events_equal : bool 

104 ``True`` if the output is exactly as expected. ``False`` in any 

105 other case. 

106 """ 

107 _match, mismatch, errors = self.event.compare_contents(self.inputevent) 

108 if mismatch or errors: 

109 diffs = "\n".join( 

110 "{}:\n{}".format( 

111 name, 

112 fh.diff_contents(type(fh)(self.inputevent)) 

113 ) for name, fh in mismatch.items() 

114 ) 

115 input_contents = os.listdir(self.inputevent.eventdir) 

116 output_contents = os.listdir(self.event.eventdir) 

117 warnings.warn(("Not all files matched.\nMismatches: {}\nErrors: " 

118 "{}\nDiffs:\n{}Input dir contents:\n{}\nOutput dir " 

119 "contents:\n{}\n").format(mismatch, errors, diffs, 

120 input_contents, 

121 output_contents)) 

122 return False 

123 return True 

124 

125 def test(self): 

126 """Run this test with ``execute`` and then check whether outputs are as 

127 expected with ``compare``.""" 

128 with tempfile.TemporaryDirectory() as tmpdir: 

129 print("CREATED TMPDIR FOR ", self.EVENTID, ": ", tmpdir) 

130 self.run = Run(tmpdir) 

131 print("SET self.run for ", self, " TO ", self.run) 

132 self.provision() 

133 print("PROVISIONED TMPDIR FOR ", self.EVENTID, ", CONTENTS: ", 

134 os.listdir(tmpdir)) 

135 self.execute() 

136 print("EXECUTED TEST FOR ", self, " TMPDIR CONTENTS: ", 

137 os.listdir(tmpdir)) 

138 print("TMP EVENTDIR CONTENTS: ", os.listdir(self.event.eventdir)) 

139 assert self.compare() 

140 

141 

142class AbstractFileGenerationChecker(AbstractFileGenerationComparator): 

143 """ 

144 Just like ``AbstractFileGenerationComparator``, but does not compare file 

145 contents of outputs to a nominal value; instead, just checks whether the 

146 expected output files exist. Use this for files that do not depend 

147 deterministically on their inputs (and cannot therefore be compared with a 

148 simple diff to expected outputs). 

149 """ 

150 

151 def compare(self): 

152 """Check whether running ``execute`` in the test ``eventdir`` has 

153 produced all of the expected output files. Does not check contents. Use 

154 this for files whose contents do not deterministically depend on input 

155 files. 

156 

157 Returns 

158 ------- 

159 events_equal : bool 

160 ``True`` if the output is exactly as expected. ``False`` in any 

161 other case. 

162 """ 

163 return all(f.exists() for f in self.event.files.values()) 

164 

165 

166class MS181101abMixin(object): 

167 """Tests on an ER14-era test event from the EM Follow-up userguide.""" 

168 

169 EVENTID = "MS181101ab-2-Initial" 

170 

171 

172class S190425zMixin(object): 

173 """ 

174 Tests on S190425z, the first BNS of O3, triggered by GCN Initial notice 

175 in this test. 

176 """ 

177 

178 EVENTID = "S190425z-1-Initial" 

179 

180 

181class S190412mMixin(object): 

182 """Tests on S190412m, a manually-triggered BBH event in early O3.""" 

183 

184 EVENTID = "S190412m" 

185 

186 

187class S190521gMixin(object): 

188 """ 

189 Tests on S190521g, a BBH event in O3, triggered by GCN Preliminary 

190 notice in this test. 

191 """ 

192 

193 EVENTID = "S190521g-1-Preliminary"