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"""
4Classes used in unit tests for LLAMA.
5"""
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
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 """
27 INPUT_RUN = TEST_RUN_INPUTS
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)
36 @run.setter
37 def run(self, run):
38 """Set the run value."""
39 setattr(self, '_run', run)
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``)."""
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."""
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)."""
65 @abstractmethod
66 def execute(self):
67 """Run the actual test (no need to compare outputs; this will be done
68 in ``compare``)."""
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)
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)
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)
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.
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
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()
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 """
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.
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())
166class MS181101abMixin(object):
167 """Tests on an ER14-era test event from the EM Follow-up userguide."""
169 EVENTID = "MS181101ab-2-Initial"
172class S190425zMixin(object):
173 """
174 Tests on S190425z, the first BNS of O3, triggered by GCN Initial notice
175 in this test.
176 """
178 EVENTID = "S190425z-1-Initial"
181class S190412mMixin(object):
182 """Tests on S190412m, a manually-triggered BBH event in early O3."""
184 EVENTID = "S190412m"
187class S190521gMixin(object):
188 """
189 Tests on S190521g, a BBH event in O3, triggered by GCN Preliminary
190 notice in this test.
191 """
193 EVENTID = "S190521g-1-Preliminary"