Coverage for tests/test_PluginLogs.py: 18%

267 statements  

« prev     ^ index     » next       coverage.py v7.1.0, created at 2023-02-05 18:07 -0800

1# This file is part of meas_base. 

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 

22import unittest 

23import os 

24import numpy 

25import logging 

26from logging import FileHandler, StreamHandler, Formatter 

27 

28import lsst.geom 

29import lsst.afw.table 

30import lsst.daf.base 

31import lsst.meas.base 

32import lsst.utils.tests 

33from lsst.meas.base.tests import (AlgorithmTestCase, ) 

34from lsst.meas.base.sfm import SingleFramePluginConfig, SingleFramePlugin 

35from lsst.meas.base.forcedMeasurement import ForcedPlugin 

36from lsst.meas.base.pluginRegistry import register 

37from lsst.meas.base import FlagDefinitionList, FlagHandler, MeasurementError 

38 

39ROOT = os.path.abspath(os.path.dirname(__file__)) 

40 

41 

42class LoggingPluginConfig(SingleFramePluginConfig): 

43 """Configuration for sample plugin. 

44 """ 

45 pass 

46 

47 

48@register("test_LoggingPlugin") 

49class LoggingPlugin(SingleFramePlugin): 

50 """Sample Python plugin which has an associated log name. 

51 

52 Notes 

53 ----- 

54 The log name is provided to the plugin by the measurement task which is 

55 running it. This requires that the `hasLogName` attribute must be a member 

56 of the plugin class, and it must be `True`. 

57 """ 

58 hasLogName = True 

59 ConfigClass = LoggingPluginConfig 

60 

61 @classmethod 

62 def getExecutionOrder(cls): 

63 return cls.FLUX_ORDER 

64 

65 # The initializer for the class must accept an optional logName parameter. 

66 def __init__(self, config, name, schema, metadata, logName=None): 

67 SingleFramePlugin.__init__(self, config, name, schema, metadata, logName=logName) 

68 flagDefs = FlagDefinitionList() 

69 self.FAILURE = flagDefs.addFailureFlag() 

70 self.CONTAINS_NAN = flagDefs.add("flag_containsNan", "Measurement area contains a nan") 

71 self.flagHandler = FlagHandler.addFields(schema, name, flagDefs) 

72 self.instFluxKey = schema.addField(name + "_instFlux", "F", doc="flux") 

73 

74 def measure(self, measRecord, exposure): 

75 """Perform measurement. 

76 

77 Notes 

78 ----- 

79 The `measure` method is called by the measurement framework when `run` 

80 is called. If a `MeasurementError` is raised during this method, the 

81 `fail` method will be called to set the error flags. 

82 """ 

83 logging.getLogger(self.getLogName()).info("%s plugin measuring.", self.name) 

84 # Sum the pixels inside the bounding box 

85 centerPoint = lsst.geom.Point2I(int(measRecord.getX()), int(measRecord.getY())) 

86 bbox = lsst.geom.Box2I(centerPoint, lsst.geom.Extent2I(1, 1)) 

87 instFlux = lsst.afw.image.ImageF(exposure.getMaskedImage().getImage(), bbox).getArray().sum() 

88 measRecord.set(self.instFluxKey, instFlux) 

89 

90 # If there was a NaN inside the bounding box, the instFlux will still 

91 # be NaN 

92 if numpy.isnan(instFlux): 

93 raise MeasurementError(self.CONTAINS_NAN.doc, self.CONTAINS_NAN.number) 

94 

95 def fail(self, measRecord, error=None): 

96 """Handle measurement failures. 

97 

98 Notes 

99 ----- 

100 If measurement raises a `MeasurementError`, the error will be passed 

101 to the fail method by the measurement framework. If the error is not 

102 `None`, ``error.cpp`` should correspond to a specific error and the 

103 appropriate error flag will be set. 

104 """ 

105 if error is None: 

106 self.flagHandler.handleFailure(measRecord) 

107 else: 

108 self.flagHandler.handleFailure(measRecord, error.cpp) 

109 

110 

111def directLog(log, file=None): 

112 """Direct the log given to a file or to the console if ``file`` is `None`. 

113 """ 

114 if isinstance(log, lsst.log.Log): 

115 props = "log4j.rootLogger=INFO, FA\n" 

116 if file is None: 

117 props += "log4j.appender.FA=ConsoleAppender\n" 

118 else: 

119 props += "log4j.appender.FA=FileAppender\n" 

120 props += "log4j.appender.FA.Append=false\n" 

121 props += "log4j.appender.FA.file=%s\n"%(file,) 

122 props += "log4j.appender.FA.layout=PatternLayout\n" 

123 props += "log4j.appender.FA.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss.SSS} %p %c %m %X%n\n" 

124 log.configure_prop(props) 

125 else: 

126 log.setLevel(logging.INFO) 

127 

128 # Remove existing handlers 

129 for handler in log.handlers: 

130 log.removeHandler(handler) 

131 if isinstance(handler, FileHandler): 

132 handler.close() 

133 

134 # Ignore parent handlers. 

135 log.propagate = 0 

136 

137 if file is None: 

138 handler = StreamHandler() 

139 else: 

140 handler = FileHandler(file) 

141 

142 # Tests check for level name so ensure it is included. 

143 formatter = Formatter(fmt="{name} {levelname}: {message}", style="{") 

144 handler.setFormatter(formatter) 

145 log.addHandler(handler) 

146 

147 # Configure lsst.log to forward all log messages to python. 

148 # This is needed to forward the C++ log test messages to these 

149 # python handlers. 

150 lsst.log.configure_pylog_MDC("INFO", MDC_class=None) 

151 

152 

153class RegisteredPluginsTestCase(AlgorithmTestCase, lsst.utils.tests.TestCase): 

154 """Test all registered Plugins to see if their logName is set as expected. 

155 

156 Those which have the ``hasLogName=True`` attribute will have a ``logName`` 

157 parameter passed to their ``__init__``, and should set the internal 

158 ``_logName`` attribute. If they are wrapped C++ algorithms, the 

159 `getLogName` should also return same ``logName`` as the plugin. 

160 """ 

161 def testSingleFramePlugins(self): 

162 center = lsst.geom.Point2D(50, 50) 

163 bbox = lsst.geom.Box2I(lsst.geom.Point2I(0, 0), 

164 lsst.geom.Extent2I(100, 100)) 

165 dataset = lsst.meas.base.tests.TestDataset(bbox) 

166 dataset.addSource(1000000.0, center) 

167 registry = SingleFramePlugin.registry 

168 dependencies = registry.keys() 

169 task = self.makeSingleFrameMeasurementTask("base_SdssCentroid", dependencies=dependencies) 

170 exposure, catalog = dataset.realize(noise=100.0, schema=task.schema, randomSeed=0) 

171 task.log.setLevel(task.log.ERROR) 

172 task.run(catalog, exposure) 

173 for pluginName in dependencies: 

174 plugin = task.plugins[pluginName] 

175 if hasattr(plugin, "hasLogName") and plugin.hasLogName: 

176 self.assertEqual(plugin.getLogName(), task.log.getChild(pluginName).name) 

177 # if the plugin is cpp, check the cpp Algorithm as well 

178 if hasattr(plugin, "cpp"): 

179 self.assertEqual(plugin.cpp.getLogName(), plugin.getLogName()) 

180 else: 

181 self.assertIsNone(plugin.getLogName()) 

182 

183 def testForcedPlugins(self): 

184 # Test all the ForcedPlugins registered to see if their logName is set 

185 # as expected. 

186 center = lsst.geom.Point2D(50, 50) 

187 bbox = lsst.geom.Box2I(lsst.geom.Point2I(0, 0), 

188 lsst.geom.Extent2I(100, 100)) 

189 dataset = lsst.meas.base.tests.TestDataset(bbox) 

190 dataset.addSource(1000000.0, center) 

191 registry = ForcedPlugin.registry 

192 dependencies = registry.keys() 

193 

194 task = self.makeForcedMeasurementTask("base_SdssCentroid", dependencies=dependencies) 

195 measWcs = dataset.makePerturbedWcs(dataset.exposure.getWcs(), randomSeed=1) 

196 measDataset = dataset.transform(measWcs) 

197 exposure, truthCatalog = measDataset.realize(10.0, measDataset.makeMinimalSchema(), randomSeed=1) 

198 refCat = dataset.catalog 

199 refWcs = dataset.exposure.getWcs() 

200 measCat = task.generateMeasCat(exposure, refCat, refWcs) 

201 task.attachTransformedFootprints(measCat, refCat, exposure, refWcs) 

202 

203 task.log.setLevel(task.log.ERROR) 

204 task.run(measCat, exposure, refCat, refWcs) 

205 for pluginName in dependencies: 

206 plugin = task.plugins[pluginName] 

207 if hasattr(plugin, "hasLogName") and plugin.hasLogName: 

208 child_log = task.log.getChild(pluginName) 

209 self.assertEqual(plugin.getLogName(), child_log.name) 

210 # if the plugin is cpp, check the cpp Algorithm as well 

211 if hasattr(plugin, "cpp"): 

212 self.assertEqual(plugin.cpp.getLogName(), child_log.name) 

213 else: 

214 self.assertIsNone(plugin.getLogName()) 

215 

216 

217class LoggingPythonTestCase(AlgorithmTestCase, lsst.utils.tests.TestCase): 

218 """Test one C++ and one Python plugin which have hasLogName=True. 

219 """ 

220 def setUp(self): 

221 bbox = lsst.geom.Box2I(lsst.geom.Point2I(0, 0), lsst.geom.Point2I(100, 100)) 

222 self.dataset = lsst.meas.base.tests.TestDataset(bbox) 

223 self.dataset.addSource(instFlux=1E5, centroid=lsst.geom.Point2D(25, 25)) 

224 config = lsst.meas.base.SingleFrameMeasurementConfig() 

225 config.slots.centroid = None 

226 config.slots.apFlux = None 

227 config.slots.calibFlux = None 

228 config.slots.gaussianFlux = None 

229 config.slots.modelFlux = None 

230 config.slots.psfFlux = None 

231 config.slots.shape = None 

232 config.slots.psfShape = None 

233 self.config = config 

234 

235 def tearDown(self): 

236 del self.config 

237 del self.dataset 

238 

239 def testLoggingPythonPlugin(self): 

240 algName = "test_LoggingPlugin" 

241 schema = self.dataset.makeMinimalSchema() 

242 self.config.plugins = [algName] 

243 task = lsst.meas.base.SingleFrameMeasurementTask(schema=schema, config=self.config) 

244 # test that the plugin's logName has been propagated to the plugin 

245 self.assertEqual(task.plugins[algName].getLogName(), task.log.getChild(algName).name) 

246 log = task.log.getChild(algName) 

247 with lsst.utils.tests.getTempFilePath(".log") as pluginLogName: 

248 directLog(log, pluginLogName) 

249 exposure, cat = self.dataset.realize(noise=0.0, schema=schema, randomSeed=2) 

250 task.run(cat, exposure) 

251 directLog(log, None) 

252 # direct back to console, closing log files 

253 with open(pluginLogName) as fin: 

254 lines = fin.read() 

255 # test that the sample plugin has correctly logged to where we 

256 # expected it to. 

257 self.assertGreaterEqual(lines.find("measuring"), 0, lines) 

258 

259 def testLoggingCppPlugin(self): 

260 # PsfFlux is known to log an ``ERROR`` if a Psf is not attached 

261 algName = "base_PsfFlux" 

262 self.config.plugins = [algName] 

263 

264 schema = self.dataset.makeMinimalSchema() 

265 task = lsst.meas.base.SingleFrameMeasurementTask(schema=schema, config=self.config) 

266 log = task.log.getChild(algName) 

267 log.setLevel(log.ERROR) 

268 

269 # test that the plugin's logName has been propagated to the plugin 

270 self.assertEqual(task.plugins[algName].getLogName(), log.name) 

271 self.assertEqual(task.plugins[algName].cpp.getLogName(), log.name) 

272 with lsst.utils.tests.getTempFilePath(".log") as pluginLogName: 

273 directLog(log, pluginLogName) 

274 exposure, cat = self.dataset.realize(noise=0.0, schema=schema, randomSeed=3) 

275 exposure.setPsf(None) 

276 # This call throws an error, so be prepared for it 

277 try: 

278 task.run(cat, exposure) 

279 except Exception: 

280 pass 

281 directLog(log, None) 

282 # direct back to console, closing log files 

283 with open(pluginLogName) as fin: 

284 lines = fin.read() 

285 

286 # test that the sample plugin has correctly logged to where we 

287 # expected it to. 

288 self.assertGreaterEqual(lines.find("ERROR"), 0, lines) 

289 

290 

291class SingleFrameTestCase(AlgorithmTestCase, lsst.utils.tests.TestCase): 

292 

293 def setUp(self): 

294 # object in corner to trigger EDGE error 

295 self.center = lsst.geom.Point2D(5, 5) 

296 self.bbox = lsst.geom.Box2I(lsst.geom.Point2I(0, 0), 

297 lsst.geom.Extent2I(100, 100)) 

298 self.dataset = lsst.meas.base.tests.TestDataset(self.bbox) 

299 self.dataset.addSource(1000000.0, self.center) 

300 self.task = self.makeSingleFrameMeasurementTask("base_SdssCentroid") 

301 self.log = self.task.log.getChild("base_SdssCentroid") 

302 self.exposure, self.catalog = self.dataset.realize(10.0, self.task.schema, randomSeed=4) 

303 

304 def tearDown(self): 

305 del self.center 

306 del self.bbox 

307 del self.dataset 

308 del self.task 

309 del self.log 

310 del self.exposure 

311 del self.catalog 

312 

313 def testSeparatePluginLogs(self): 

314 """Check that the task log and the plugin log are truly separate. 

315 """ 

316 taskLogName = os.path.join(ROOT, 'testSeparatePluginLogs-task.log') 

317 directLog(self.task.log, taskLogName) 

318 self.task.log.info("Testing") 

319 with lsst.utils.tests.getTempFilePath(".log") as pluginLogName: 

320 directLog(self.log, pluginLogName) 

321 self.log.setLevel(self.log.DEBUG) 

322 self.task.run(self.catalog, self.exposure) 

323 # direct back to console, closing log files 

324 directLog(self.log, None) 

325 directLog(self.task.log, None) 

326 with open(taskLogName) as fin: 

327 lines = fin.read() 

328 os.unlink(taskLogName) 

329 self.assertGreaterEqual(lines.find("Testing"), 0) 

330 with open(pluginLogName) as fin: 

331 lines = fin.read() 

332 self.assertGreaterEqual(lines.find("MeasurementError"), 0) 

333 

334 def testSetPluginLevel(self): 

335 """Test setting the plugin log level. 

336 

337 Specifically, we set it to the ``ERROR`` level. 

338 """ 

339 with lsst.utils.tests.getTempFilePath(".log") as pluginLogName: 

340 directLog(self.log, pluginLogName) 

341 self.log.setLevel(self.log.ERROR) 

342 self.task.run(self.catalog, self.exposure) 

343 # direct back to console, closing log files 

344 directLog(self.log, None) 

345 with open(pluginLogName) as fin: 

346 lines = fin.read() 

347 self.assertLess(lines.find("MeasurementError"), 0) 

348 

349 

350class ForcedTestCase(AlgorithmTestCase, lsst.utils.tests.TestCase): 

351 

352 def setUp(self): 

353 # object in corner to trigger EDGE error 

354 self.center = lsst.geom.Point2D(0, 0) 

355 self.bbox = lsst.geom.Box2I(lsst.geom.Point2I(0, 0), 

356 lsst.geom.Extent2I(100, 100)) 

357 self.dataset = lsst.meas.base.tests.TestDataset(self.bbox) 

358 self.dataset.addSource(1000000.0, self.center) 

359 self.task = self.makeForcedMeasurementTask("base_SdssCentroid") 

360 self.log = self.task.log.getChild("base_SdssCentroid") 

361 measWcs = self.dataset.makePerturbedWcs(self.dataset.exposure.getWcs(), randomSeed=5) 

362 measDataset = self.dataset.transform(measWcs) 

363 self.exposure, truthCatalog = measDataset.realize(10.0, measDataset.makeMinimalSchema(), randomSeed=5) 

364 self.refCat = self.dataset.catalog 

365 self.refWcs = self.dataset.exposure.getWcs() 

366 self.measCat = self.task.generateMeasCat(self.exposure, self.refCat, self.refWcs) 

367 self.task.attachTransformedFootprints(self.measCat, self.refCat, self.exposure, self.refWcs) 

368 

369 def tearDown(self): 

370 del self.center 

371 del self.bbox 

372 del self.dataset 

373 del self.task 

374 del self.log 

375 del self.exposure 

376 del self.measCat 

377 del self.refCat 

378 del self.refWcs 

379 

380 def testSeparatePluginLog(self): 

381 """Check that the task log and the plugin log are truly separate. 

382 """ 

383 taskLogName = os.path.join(ROOT, 'testSeparatePluginLog-task.log') 

384 directLog(self.task.log, taskLogName) 

385 self.task.log.info("Testing") 

386 with lsst.utils.tests.getTempFilePath(".log") as pluginLogName: 

387 directLog(self.log, pluginLogName) 

388 self.log.setLevel(self.log.DEBUG) 

389 self.task.run(self.measCat, self.exposure, self.refCat, self.refWcs) 

390 # direct back to console, closing log files 

391 directLog(self.log, None) 

392 directLog(self.task.log, None) 

393 with open(taskLogName) as fin: 

394 lines = fin.read() 

395 os.unlink(taskLogName) 

396 self.assertGreaterEqual(lines.find("Testing"), 0) 

397 with open(pluginLogName) as fin: 

398 lines = fin.read() 

399 self.assertGreaterEqual(lines.find("MeasurementError"), 0) 

400 

401 def testSetPluginLevel(self): 

402 """Test setting the plugin log level. 

403 

404 Specifically, we set it to the ``ERROR`` level. 

405 """ 

406 with lsst.utils.tests.getTempFilePath(".log") as pluginLogName: 

407 directLog(self.log, pluginLogName) 

408 self.log.setLevel(self.log.ERROR) 

409 self.task.run(self.measCat, self.exposure, self.refCat, self.refWcs) 

410 # direct back to console, closing log files 

411 directLog(self.log, None) 

412 with open(pluginLogName) as fin: 

413 lines = fin.read() 

414 self.assertLess(lines.find("MeasurementError"), 0) 

415 

416 

417class TestMemory(lsst.utils.tests.MemoryTestCase): 

418 pass 

419 

420 

421def setup_module(module): 

422 lsst.utils.tests.init() 

423 

424 

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

426 lsst.utils.tests.init() 

427 unittest.main()