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

98 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2022-09-20 02:28 -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 

30from lsst.pipe.base import TaskMetadata 

31from lsst.dax.apdb import ApdbConfig 

32 

33from lsst.verify.tasks import MetricComputationError 

34 

35 

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

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

38 

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

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

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

42 `MetricTaskTestCase.setUp`. 

43 """ 

44 @classmethod 

45 @abc.abstractmethod 

46 def makeTask(cls): 

47 """Construct the task to be tested. 

48 

49 This overridable method will be called during test setup. 

50 

51 Returns 

52 ------- 

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

54 A new MetricTask object to test. 

55 """ 

56 

57 task = None 

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

59 (`tasks.MetricTask`). 

60 

61 This attribute is initialized automatically. 

62 """ 

63 

64 taskClass = None 

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

66 

67 This attribute is initialized automatically. 

68 """ 

69 

70 def setUp(self): 

71 """Setup common to all MetricTask tests. 

72 

73 Notes 

74 ----- 

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

76 and `taskClass`. 

77 """ 

78 self.task = self.makeTask() 

79 self.taskClass = type(self.task) 

80 

81 def testOutputDatasetName(self): 

82 config = self.task.config 

83 connections = config.connections.ConnectionsClass(config=config) 

84 dataset = connections.measurement.name 

85 

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

87 self.assertNotIn(".", dataset) 

88 

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

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

91 

92 def testConfigValidation(self): 

93 config = self.task.config 

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

95 with self.assertRaises(ValueError): 

96 config.validate() 

97 

98 

99class MetadataMetricTestCase(MetricTaskTestCase): 

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

101 

102 Notes 

103 ----- 

104 Subclasses must override 

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

106 being tested. 

107 """ 

108 

109 @staticmethod 

110 def _takesScalarMetadata(task): 

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

112 return not connections.metadata.multiple 

113 

114 def testValidRun(self): 

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

116 """ 

117 mockKey = "unitTestKey" 

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

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

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

121 if self._takesScalarMetadata(self.task): 

122 metadata1 = TaskMetadata() 

123 metadata1[mockKey] = 42 

124 

125 self.task.run(metadata1) 

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

127 mockWorkhorse.reset_mock() 

128 self.task.run(None) 

129 mockWorkhorse.assert_called_once_with({"unused": None}) 

130 else: 

131 metadata1 = TaskMetadata() 

132 metadata1[mockKey] = 42 

133 metadata2 = TaskMetadata() 

134 metadata2[mockKey] = "Sphere" 

135 self.task.run([metadata1, None, metadata2]) 

136 mockWorkhorse.assert_called_once_with( 

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

138 

139 def testAmbiguousRun(self): 

140 mockKey = "unitTestKey" 

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

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

143 metadata = TaskMetadata() 

144 metadata[mockKey + "1"] = 42 

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

146 with self.assertRaises(MetricComputationError): 

147 if self._takesScalarMetadata(self.task): 

148 self.task.run(metadata) 

149 else: 

150 self.task.run([metadata]) 

151 

152 def testPassThroughRun(self): 

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

154 side_effect=MetricComputationError): 

155 with self.assertRaises(MetricComputationError): 

156 if self._takesScalarMetadata(self.task): 

157 self.task.run(None) 

158 else: 

159 self.task.run([None]) 

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