Coverage for tests/test_inspectjob.py: 22%

98 statements  

« prev     ^ index     » next       coverage.py v6.4, created at 2022-05-24 02:51 -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/>. 

21 

22from io import StringIO 

23import re 

24import unittest.mock 

25 

26import astropy.units as u 

27 

28from lsst.verify import Job, Metric, Measurement, ThresholdSpecification 

29from lsst.verify.bin.inspectjob import inspect_job 

30 

31 

32@unittest.mock.patch("sys.stdout", new_callable=StringIO) 

33class InspectJobTestCase(unittest.TestCase): 

34 

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 

39 

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 

62 

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

71 

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) 

77 

78 error = "Can't find %s and value on same row." \ 

79 % measurement.metric_name 

80 self.assertIsNotNone(match, msg=error) 

81 

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) 

90 

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) 

99 

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

113 

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) 

123 

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) 

128 

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) 

137 

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

142 

143 def test_empty(self, mock_stdout): 

144 """Test that inspect_job can handle files with neither metrics nor metadata. 

145 """ 

146 inspect_job(Job()) 

147 # No specific output expected, so test passes if inspect_job 

148 # didn't raise. 

149 

150 def test_metadataonly(self, mock_stdout): 

151 """Test that inspect_job can handle files with metadata but no metrics. 

152 """ 

153 # Job and its components were not designed to support deletion, so 

154 # create a new Job from scratch to ensure it's a valid object. 

155 job = Job() 

156 job.metrics.insert(Metric("foo.boringmetric", "", 

157 u.percent, 

158 tags=["redundant"])) 

159 job.metrics.insert(Metric("foo.fancymetric", "", 

160 u.meter, 

161 tags=["vital"])) 

162 job.meta["bar"] = "high" 

163 job.meta["shape"] = "rotund" 

164 job.specs.insert(ThresholdSpecification("utterly_ridiculous", 

165 1e10 * u.meter, 

166 ">")) 

167 

168 inspect_job(job) 

169 output = mock_stdout.getvalue() 

170 for key, value in [("bar", "high"), 

171 ("shape", "rotund")]: 

172 self._check_metadata(key, value, output) 

173 

174 def test_metricsonly(self, mock_stdout): 

175 """Test that inspect_job can handle files with metrics but no metadata. 

176 """ 

177 # Job and its components were not designed to support deletion, so 

178 # create a new Job from scratch to ensure it's a valid object. 

179 job = Job() 

180 job.metrics.insert(Metric("foo.boringmetric", "", 

181 u.percent, 

182 tags=["redundant"])) 

183 job.metrics.insert(Metric("foo.fancymetric", "", 

184 u.meter, 

185 tags=["vital"])) 

186 job.measurements.insert(Measurement("foo.fancymetric", 

187 2.0 * u.meter)) 

188 job.measurements.insert(Measurement("foo.fanciermetric", 

189 3.5 * u.second)) 

190 job.measurements["foo.fanciermetric"].notes["fanciness"] = "moderate" 

191 job.measurements.insert(Measurement("foo.fanciestmetric", 

192 3.1415927 * u.kilogram)) 

193 

194 inspect_job(job) 

195 output = mock_stdout.getvalue() 

196 # MeasurementSet.values does not exist 

197 for _, measurement in job.measurements.items(): 

198 self._check_measurement(measurement, output) 

199 

200 

201if __name__ == "__main__": 201 ↛ 202line 201 didn't jump to line 202, because the condition on line 201 was never true

202 unittest.main()