Coverage for tests/test_PluginLogs.py: 19%
267 statements
« prev ^ index » next coverage.py v6.4, created at 2022-05-27 11:34 +0000
« prev ^ index » next coverage.py v6.4, created at 2022-05-27 11:34 +0000
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/>.
22import unittest
23import os
24import numpy
25import logging
26from logging import FileHandler, StreamHandler, Formatter
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
39ROOT = os.path.abspath(os.path.dirname(__file__))
42class LoggingPluginConfig(SingleFramePluginConfig):
43 """Configuration for sample plugin.
44 """
45 pass
48@register("test_LoggingPlugin")
49class LoggingPlugin(SingleFramePlugin):
50 """Sample Python plugin which has an associated log name.
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
61 @classmethod
62 def getExecutionOrder(cls):
63 return cls.FLUX_ORDER
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")
74 def measure(self, measRecord, exposure):
75 """Perform measurement.
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)
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)
95 def fail(self, measRecord, error=None):
96 """Handle measurement failures.
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)
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)
128 # Remove existing handlers
129 for handler in log.handlers:
130 log.removeHandler(handler)
131 if isinstance(handler, FileHandler):
132 handler.close()
134 # Ignore parent handlers.
135 log.propagate = 0
137 if file is None:
138 handler = StreamHandler()
139 else:
140 handler = FileHandler(file)
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)
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)
153class RegisteredPluginsTestCase(AlgorithmTestCase, lsst.utils.tests.TestCase):
154 """Test all registered Plugins to see if their logName is set as expected.
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())
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()
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)
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())
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
235 def tearDown(self):
236 del self.config
237 del self.dataset
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)
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]
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)
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()
286 # test that the sample plugin has correctly logged to where we
287 # expected it to.
288 self.assertGreaterEqual(lines.find("ERROR"), 0, lines)
291class SingleFrameTestCase(AlgorithmTestCase, lsst.utils.tests.TestCase):
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)
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
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)
334 def testSetPluginLevel(self):
335 """Test setting the plugin log level.
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)
350class ForcedTestCase(AlgorithmTestCase, lsst.utils.tests.TestCase):
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)
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
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)
401 def testSetPluginLevel(self):
402 """Test setting the plugin log level.
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)
417class TestMemory(lsst.utils.tests.MemoryTestCase):
418 pass
421def setup_module(module):
422 lsst.utils.tests.init()
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()