Coverage for python/lsst/verify/tasks/testUtils.py: 29%

116 statements  

« prev     ^ index     » next       coverage.py v7.5.0, created at 2024-05-04 02:34 -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 

22__all__ = ["MetadataMetricTestCase", "ApdbMetricTestCase"] 

23 

24import abc 

25 

26import unittest.mock 

27from unittest.mock import patch 

28 

29import lsst.utils.tests 

30import lsst.pex.config as pexConfig 

31from lsst.pipe.base import TaskMetadata 

32from lsst.dax.apdb import ApdbConfig 

33 

34from lsst.verify.tasks import MetricComputationError 

35 

36 

37class MetricTaskTestCase(lsst.utils.tests.TestCase, metaclass=abc.ABCMeta): 

38 """Unit test base class for tests of `tasks.MetricTask`. 

39 

40 This class provides tests of the generic ``MetricTask`` API. Subclasses 

41 must override `makeTask`, and may add extra tests for class-specific 

42 functionality. If subclasses override `setUp`, they must call 

43 `MetricTaskTestCase.setUp`. 

44 """ 

45 @classmethod 

46 @abc.abstractmethod 

47 def makeTask(cls): 

48 """Construct the task to be tested. 

49 

50 This overridable method will be called during test setup. 

51 

52 Returns 

53 ------- 

54 task : `lsst.verify.tasks.MetricTask` 

55 A new MetricTask object to test. 

56 """ 

57 

58 task = None 

59 """The ``MetricTask`` being tested by this object 

60 (`tasks.MetricTask`). 

61 

62 This attribute is initialized automatically. 

63 """ 

64 

65 taskClass = None 

66 """The type of `task` (`tasks.MetricTask`-type). 

67 

68 This attribute is initialized automatically. 

69 """ 

70 

71 def setUp(self): 

72 """Setup common to all MetricTask tests. 

73 

74 Notes 

75 ----- 

76 This implementation calls `makeTask`, then initializes `task` 

77 and `taskClass`. 

78 """ 

79 self.task = self.makeTask() 

80 self.taskClass = type(self.task) 

81 

82 def testOutputDatasetName(self): 

83 config = self.task.config 

84 connections = config.connections.ConnectionsClass(config=config) 

85 dataset = connections.measurement.name 

86 

87 self.assertTrue(dataset.startswith("metricvalue_")) 

88 self.assertNotIn(".", dataset) 

89 

90 self.assertIn(config.connections.package, dataset) 

91 self.assertIn(config.connections.metric, dataset) 

92 

93 def testConfigValidation(self): 

94 config = self.task.config 

95 config.connections.metric = "verify.DummyMetric" 

96 with self.assertRaises(ValueError): 

97 config.validate() 

98 

99 

100class MetadataMetricTestCase(MetricTaskTestCase): 

101 """Unit test base class for tests of `MetadataMetricTask`. 

102 

103 Notes 

104 ----- 

105 Subclasses must override 

106 `~lsst.verify.tasks.MetricTaskTestCase.makeTask` for the concrete task 

107 being tested. 

108 """ 

109 

110 @staticmethod 

111 def _takesScalarMetadata(task): 

112 connections = task.config.connections.ConnectionsClass(config=task.config) 

113 return not connections.metadata.multiple 

114 

115 def testValidRun(self): 

116 """Test how run delegates to the abstract methods. 

117 """ 

118 mockKey = "unitTestKey" 

119 with patch.object(self.task, "getInputMetadataKeys", 

120 return_value={"unused": mockKey}), \ 

121 patch.object(self.task, "makeMeasurement") as mockWorkhorse: 

122 if self._takesScalarMetadata(self.task): 

123 metadata1 = TaskMetadata() 

124 metadata1[mockKey] = 42 

125 

126 self.task.run(metadata1) 

127 mockWorkhorse.assert_called_once_with({"unused": 42}) 

128 else: 

129 metadata1 = TaskMetadata() 

130 metadata1[mockKey] = 42 

131 metadata2 = TaskMetadata() 

132 metadata2[mockKey] = "Sphere" 

133 self.task.run([metadata1, metadata2]) 

134 mockWorkhorse.assert_called_once_with( 

135 [{"unused": value} for value in [42, "Sphere"]]) 

136 

137 def testAmbiguousRun(self): 

138 mockKey = "unitTestKey" 

139 with patch.object(self.task, "getInputMetadataKeys", 

140 return_value={"unused": mockKey}): 

141 metadata = TaskMetadata() 

142 metadata[mockKey + "1"] = 42 

143 metadata[mockKey + "2"] = "Sphere" 

144 with self.assertRaises(MetricComputationError): 

145 if self._takesScalarMetadata(self.task): 

146 self.task.run(metadata) 

147 else: 

148 self.task.run([metadata]) 

149 

150 def testPassThroughRun(self): 

151 with patch.object(self.task, "makeMeasurement", 

152 side_effect=MetricComputationError): 

153 metadata1 = TaskMetadata() 

154 metadata1["unitTestKey"] = 42 

155 with self.assertRaises(MetricComputationError): 

156 if self._takesScalarMetadata(self.task): 

157 self.task.run(metadata1) 

158 else: 

159 self.task.run([metadata1]) 

160 

161 def testDimensionsOverride(self): 

162 config = self.task.config 

163 expectedDimensions = {"instrument", "visit"} 

164 config.metadataDimensions = expectedDimensions 

165 

166 connections = config.connections.ConnectionsClass(config=config) 

167 self.assertSetEqual(set(connections.dimensions), 

168 expectedDimensions) 

169 self.assertIn(connections.metadata, 

170 connections.allConnections.values()) 

171 self.assertSetEqual(set(connections.metadata.dimensions), 

172 expectedDimensions) 

173 

174 

175class ApdbMetricTestCase(MetricTaskTestCase): 

176 """Unit test base class for tests of `ApdbMetricTask`. 

177 

178 Notes 

179 ----- 

180 Subclasses must override 

181 `~lsst.verify.tasks.MetricTaskTestCase.makeTask` for the concrete task 

182 being tested. Subclasses that use a custom DbLoader should also 

183 override `makeDbInfo`. 

184 """ 

185 

186 @classmethod 

187 def makeDbInfo(cls): 

188 """Return an object that can be passed as input to an `ApdbMetricTask`. 

189 

190 This method is intended for generic tests that simply need to call 

191 ``run`` on some valid input. If a test depends on specific input, it 

192 should create that input directly. 

193 

194 The default implementation creates a `~lsst.pex.config.Config` that 

195 will be accepted by `~lsst.verify.tasks.DirectApdbLoader`. Test suites 

196 that use a different loader should override this method. 

197 """ 

198 return ApdbConfig() 

199 

200 def testValidRun(self): 

201 info = self.makeDbInfo() 

202 with patch.object(self.task, "makeMeasurement") as mockWorkhorse: 

203 self.task.run([info]) 

204 mockWorkhorse.assert_called_once() 

205 

206 def testDataIdRun(self): 

207 info = self.makeDbInfo() 

208 with patch.object(self.task, "makeMeasurement") as mockWorkhorse: 

209 dataId = {'visit': 42} 

210 self.task.run([info], outputDataId=dataId) 

211 mockWorkhorse.assert_called_once_with( 

212 unittest.mock.ANY, {'visit': 42}) 

213 

214 def testPassThroughRun(self): 

215 with patch.object(self.task, "makeMeasurement", 

216 side_effect=MetricComputationError): 

217 info = self.makeDbInfo() 

218 with self.assertRaises(MetricComputationError): 

219 self.task.run([info]) 

220 

221 # TODO: remove on DM-43419 

222 def testConfigApdbRead(self): 

223 config = self.taskClass.ConfigClass() 

224 with self.assertWarns(FutureWarning): 

225 config.doReadMarker = True 

226 config.freeze() 

227 config.validate() 

228 

229 # TODO: remove on DM-43419 

230 def testConfigApdbFileOk(self): 

231 config = self.taskClass.ConfigClass() 

232 config.doReadMarker = False 

233 config.apdb_config_url = "some/file/path.yaml" 

234 config.freeze() 

235 config.validate() 

236 

237 # TODO: remove on DM-43419 

238 def testConfigApdbFileInvalid(self): 

239 config = self.taskClass.ConfigClass() 

240 config.doReadMarker = False 

241 # Don't set apdb_config_url 

242 config.freeze() 

243 with self.assertRaises(pexConfig.FieldValidationError): 

244 config.validate()