Coverage for tests/test_inspectjob.py: 22%
98 statements
« prev ^ index » next coverage.py v6.4.2, created at 2022-08-01 01:31 -0700
« prev ^ index » next coverage.py v6.4.2, created at 2022-08-01 01:31 -0700
1# This file is part of verify.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
22from io import StringIO
23import re
24import unittest.mock
26import astropy.units as u
28from lsst.verify import Job, Metric, Measurement, ThresholdSpecification
29from lsst.verify.bin.inspectjob import inspect_job
32@unittest.mock.patch("sys.stdout", new_callable=StringIO)
33class InspectJobTestCase(unittest.TestCase):
35 @classmethod
36 def setUpClass(cls):
37 # Do not use re.DOTALL; some tests assume it's not set
38 cls.regex_flags = re.IGNORECASE
40 def setUp(self):
41 self.job = Job()
42 self.job.metrics.insert(Metric("foo.boringmetric", "",
43 u.percent,
44 tags=["redundant"]))
45 self.job.metrics.insert(Metric("foo.fancymetric", "",
46 u.meter,
47 tags=["vital"]))
48 self.job.measurements.insert(Measurement("foo.fancymetric",
49 2.0 * u.meter))
50 self.job.measurements.insert(Measurement("foo.fanciermetric",
51 3.5 * u.second))
52 self.job.measurements["foo.fanciermetric"].notes["fanciness"] \
53 = "moderate"
54 self.job.measurements.insert(Measurement("foo.fanciestmetric",
55 3.1415927 * u.kilogram))
56 self.job.meta["bar"] = "high"
57 self.job.meta["shape"] = "rotund"
58 self.job.specs.insert(ThresholdSpecification("utterly_ridiculous",
59 1e10 * u.meter,
60 ">"))
61 # MUST run inspect_job inside test case to capture output
63 def test_metrics(self, mock_stdout):
64 """Test that inspect_job only mentions metrics with measurements."
65 """
66 inspect_job(self.job)
67 self.assertNotIn("foo.boringmetric", mock_stdout.getvalue())
68 self.assertIn("foo.fancymetric", mock_stdout.getvalue())
69 self.assertIn("foo.fanciermetric", mock_stdout.getvalue())
70 self.assertIn("foo.fanciestmetric", mock_stdout.getvalue())
72 def _check_measurement(self, measurement, output):
73 # Test for metric name, followed by value and units
74 # None of the examples are dimensionless, so can ignore that case
75 regex = r"%s\W+?(?P<value>[\d.-]+ \w+)" % (measurement.metric_name)
76 match = re.search(regex, output, flags=self.regex_flags)
78 error = "Can't find %s and value on same row." \
79 % measurement.metric_name
80 self.assertIsNotNone(match, msg=error)
82 value = match.group("value")
83 try:
84 trailing = re.match(r"\d+\.(\d+)", value, flags=self.regex_flags)
85 decimals = len(trailing.group(1))
86 except TypeError:
87 decimals = 0
88 # Don't test # of decimal places; trailing zeros may be dropped
89 self.assertEqual(str(measurement.quantity.round(decimals)), value)
91 def test_measurements(self, mock_stdout):
92 """Test that inspect_job dumps measurements with and without metadata.
93 """
94 inspect_job(self.job)
95 output = mock_stdout.getvalue()
96 # MeasurementSet.values does not exist
97 for _, measurement in self.job.measurements.items():
98 self._check_measurement(measurement, output)
100 def test_measurement_metadata(self, mock_stdout):
101 """Test that inspect_job dumps measurement-level metadata on the same
102 line as their measurement.
103 """
104 inspect_job(self.job)
105 output = mock_stdout.getvalue()
106 for metric_name, measurement in self.job.measurements.items():
107 line = re.search("^.*%s.*$" % metric_name,
108 output,
109 flags=self.regex_flags | re.MULTILINE)
110 error = "Can't find measurement %s" % metric_name
111 self.assertIsNotNone(line, msg=error)
112 line = line.group()
114 for key in measurement.notes:
115 regex = r"(?P<keyname>[\w\.]+)\W+%s" % (measurement.notes[key])
116 match = re.search(regex, line, flags=self.regex_flags)
117 self.assertIsNotNone(match,
118 msg="Can't find metadata %s." % key)
119 reportedMetadataName = match.group('keyname')
120 fullMetadataName = "%s.%s" % (measurement.metric_name,
121 reportedMetadataName)
122 self.assertEqual(fullMetadataName, key)
124 def _check_metadata(self, key, value, output):
125 regex = r"%s.+%s" % (key, value)
126 match = re.search(regex, output, flags=self.regex_flags)
127 self.assertIsNotNone(match, msg="Can't find metadata %s" % key)
129 def test_top_metadata(self, mock_stdout):
130 """Test that inspect_job dumps top-level metadata.
131 """
132 inspect_job(self.job)
133 output = mock_stdout.getvalue()
134 for key, value in [("bar", "high"),
135 ("shape", "rotund")]:
136 self._check_metadata(key, value, output)
138 def test_specs(self, mock_stdout):
139 """Test that inspect_job does not dump specifications."
140 """
141 self.assertNotIn("utterly_ridiculous", mock_stdout.getvalue())
143 def test_empty(self, mock_stdout):
144 """Test that inspect_job can handle files with neither metrics nor
145 metadata.
146 """
147 inspect_job(Job())
148 # No specific output expected, so test passes if inspect_job
149 # didn't raise.
151 def test_metadataonly(self, mock_stdout):
152 """Test that inspect_job can handle files with metadata but no metrics.
153 """
154 # Job and its components were not designed to support deletion, so
155 # create a new Job from scratch to ensure it's a valid object.
156 job = Job()
157 job.metrics.insert(Metric("foo.boringmetric", "",
158 u.percent,
159 tags=["redundant"]))
160 job.metrics.insert(Metric("foo.fancymetric", "",
161 u.meter,
162 tags=["vital"]))
163 job.meta["bar"] = "high"
164 job.meta["shape"] = "rotund"
165 job.specs.insert(ThresholdSpecification("utterly_ridiculous",
166 1e10 * u.meter,
167 ">"))
169 inspect_job(job)
170 output = mock_stdout.getvalue()
171 for key, value in [("bar", "high"),
172 ("shape", "rotund")]:
173 self._check_metadata(key, value, output)
175 def test_metricsonly(self, mock_stdout):
176 """Test that inspect_job can handle files with metrics but no metadata.
177 """
178 # Job and its components were not designed to support deletion, so
179 # create a new Job from scratch to ensure it's a valid object.
180 job = Job()
181 job.metrics.insert(Metric("foo.boringmetric", "",
182 u.percent,
183 tags=["redundant"]))
184 job.metrics.insert(Metric("foo.fancymetric", "",
185 u.meter,
186 tags=["vital"]))
187 job.measurements.insert(Measurement("foo.fancymetric",
188 2.0 * u.meter))
189 job.measurements.insert(Measurement("foo.fanciermetric",
190 3.5 * u.second))
191 job.measurements["foo.fanciermetric"].notes["fanciness"] = "moderate"
192 job.measurements.insert(Measurement("foo.fanciestmetric",
193 3.1415927 * u.kilogram))
195 inspect_job(job)
196 output = mock_stdout.getvalue()
197 # MeasurementSet.values does not exist
198 for _, measurement in job.measurements.items():
199 self._check_measurement(measurement, output)
202if __name__ == "__main__": 202 ↛ 203line 202 didn't jump to line 203, because the condition on line 202 was never true
203 unittest.main()