Coverage for tests/test_plotImageSubtractionCutouts.py: 18%
222 statements
« prev ^ index » next coverage.py v7.5.1, created at 2024-05-14 10:33 +0000
« prev ^ index » next coverage.py v7.5.1, created at 2024-05-14 10:33 +0000
1# This file is part of analysis_ap.
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 os
23import pickle
24import sys
25import tempfile
26import unittest
28import lsst.afw.table
29import lsst.geom
30import lsst.meas.base.tests
31import lsst.utils.tests
32import pandas as pd
33import PIL
34from lsst.analysis.ap import plotImageSubtractionCutouts
35from lsst.meas.algorithms import SourceDetectionTask
37# Sky center chosen to test metadata annotations (3-digit RA and negative Dec).
38skyCenter = lsst.geom.SpherePoint(245.0, -45.0, lsst.geom.degrees)
40# A two-row mock APDB DiaSource table.
41DATA = pd.DataFrame(
42 data={
43 "diaSourceId": [506428274000265570, 527736141479149732],
44 "ra": [skyCenter.getRa().asDegrees()+0.0001, skyCenter.getRa().asDegrees()-0.0001],
45 "dec": [skyCenter.getDec().asDegrees()+0.0001, skyCenter.getDec().asDegrees()-0.001],
46 "detector": [50, 60],
47 "visit": [1234, 5678],
48 "instrument": ["TestMock", "TestMock"],
49 "band": ['r', 'g'],
50 "psfFlux": [1234.5, 1234.5],
51 "psfFluxErr": [123.5, 123.5],
52 "snr": [10.0, 11.0],
53 "psfChi2": [40.0, 50.0],
54 "psfNdata": [10, 100],
55 "apFlux": [2222.5, 3333.4],
56 "apFluxErr": [222.5, 333.4],
57 "scienceFlux": [2222000.5, 33330000.4],
58 "scienceFluxErr": [22200.5, 333000.4],
59 "isDipole": [True, False],
60 "reliability": [0, 1.0],
61 # First diaSource has all flags set, and second diaSource has none
62 "slot_PsfFlux_flag": [1, 0],
63 "slot_PsfFlux_flag_noGoodPixels": [1, 0],
64 "slot_PsfFlux_flag_edge": [1, 0],
65 "slot_ApFlux_flag": [1, 0],
66 "slot_ApFlux_flag_apertureTruncated": [1, 0],
67 "ip_diffim_forced_PsfFlux_flag": [1, 0],
68 "ip_diffim_forced_PsfFlux_flag_noGoodPixels": [1, 0],
69 "ip_diffim_forced_PsfFlux_flag_edge": [1, 0],
70 "pixelFlags_edge": [1, 0],
71 "pixelFlags_interpolated": [1, 0],
72 "pixelFlags_interpolatedCenter": [1, 0],
73 "pixelFlags_saturated": [1, 0],
74 "pixelFlags_saturatedCenter": [1, 0],
75 "pixelFlags_cr": [1, 0],
76 "pixelFlags_crCenter": [1, 0],
77 "pixelFlags_bad": [1, 0],
78 "pixelFlags_suspect": [1, 0],
79 "pixelFlags_suspectCenter": [1, 0],
80 "slot_Centroid_flag": [1, 0],
81 "slot_Shape_flag": [1, 0],
82 "slot_Shape_flag_no_pixels": [1, 0],
83 "slot_Shape_flag_not_contained": [1, 0],
84 "slot_Shape_flag_parent_source": [1, 0],
85 }
86)
89def make_mock_catalog(image):
90 """Make a simple SourceCatalog from the image, containing Footprints.
91 """
92 schema = lsst.afw.table.SourceTable.makeMinimalSchema()
93 table = lsst.afw.table.SourceTable.make(schema)
94 detect = SourceDetectionTask()
95 return detect.run(table, image).sources
98class TestPlotImageSubtractionCutouts(lsst.utils.tests.TestCase):
99 """Test that PlotImageSubtractionCutoutsTask generates images and manifest
100 files correctly.
101 """
102 def setUp(self):
103 bbox = lsst.geom.Box2I(lsst.geom.Point2I(0, 0), lsst.geom.Point2I(100, 100))
104 # source at the center of the image
105 self.centroid = lsst.geom.Point2D(50, 50)
106 dataset = lsst.meas.base.tests.TestDataset(bbox, crval=skyCenter)
107 self.scale = 0.3 # arbitrary arcseconds/pixel
108 dataset.addSource(instFlux=1e5, centroid=self.centroid)
109 self.science, self.scienceCat = dataset.realize(
110 noise=1000.0, schema=dataset.makeMinimalSchema()
111 )
112 self.template, self.templateCat = dataset.realize(
113 noise=5.0, schema=dataset.makeMinimalSchema()
114 )
115 # A simple image difference to have something to plot.
116 self.difference = lsst.afw.image.ExposureF(self.science, deep=True)
117 self.difference.image -= self.template.image
119 def test_generate_image(self):
120 """Test that we get some kind of image out.
122 It's useful to have a person look at the output via:
123 im.show()
124 """
125 # output_path does nothing here, since we never write the file to disk.
126 cutouts = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask(output_path="")
127 cutout = cutouts.generate_image(self.science, self.template, self.difference, skyCenter, self.scale)
128 with PIL.Image.open(cutout) as im:
129 # NOTE: uncomment this to show the resulting image.
130 # im.show()
131 # NOTE: the dimensions here are determined by the matplotlib figure
132 # size (in inches) and the dpi (default=100), plus borders.
133 self.assertEqual((im.height, im.width), (233, 630))
135 def test_generate_image_larger_cutout(self):
136 """A different cutout size: the resulting cutout image is the same
137 size but shows more pixels.
138 """
139 config = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask.ConfigClass()
140 config.sizes = [100]
141 # output_path does nothing here, since we never write the file to disk.
142 cutouts = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask(config=config, output_path="")
143 cutout = cutouts.generate_image(self.science, self.template, self.difference, skyCenter, self.scale)
144 with PIL.Image.open(cutout) as im:
145 # NOTE: uncomment this to show the resulting image.
146 # im.show()
147 # NOTE: the dimensions here are determined by the matplotlib figure
148 # size (in inches) and the dpi (default=100), plus borders.
149 self.assertEqual((im.height, im.width), (233, 630))
151 def test_generate_image_metadata(self):
152 """Test that we can add metadata to the image; it changes the height
153 a lot, and the width a little for the text boxes.
155 It's useful to have a person look at the output via:
156 im.show()
157 """
158 config = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask.ConfigClass()
159 config.add_metadata = True
160 # output_path does nothing here, since we never write the file to disk.
161 cutouts = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask(config=config, output_path="")
162 cutout = cutouts.generate_image(self.science,
163 self.template,
164 self.difference,
165 skyCenter,
166 self.scale,
167 source=DATA.iloc[0])
168 with PIL.Image.open(cutout) as im:
169 # NOTE: uncomment this to show the resulting image.
170 # im.show()
171 # NOTE: the dimensions here are determined by the matplotlib figure
172 # size (in inches) and the dpi (default=100), plus borders.
173 self.assertEqual((im.height, im.width), (343, 645))
175 # A cutout without any flags: the dimensions should be unchanged.
176 cutout = cutouts.generate_image(self.science,
177 self.template,
178 self.difference,
179 skyCenter,
180 self.scale,
181 source=DATA.iloc[1])
182 with PIL.Image.open(cutout) as im:
183 # NOTE: uncomment this to show the resulting image.
184 # im.show()
185 # NOTE: the dimensions here are determined by the matplotlib figure
186 # size (in inches) and the dpi (default=100), plus borders.
187 self.assertEqual((im.height, im.width), (343, 645))
189 def test_generate_image_multisize_cutouts_without_metadata(self):
190 """Multiple cutout sizes: the resulting image is larger in size
191 and contains cutouts of multiple sizes.
192 """
193 config = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask.ConfigClass()
194 config.sizes = [32, 64]
195 # output_path does nothing here, since we never write the file to disk.
196 cutouts = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask(config=config, output_path="")
197 cutout = cutouts.generate_image(self.science, self.template, self.difference, skyCenter, self.scale)
198 with PIL.Image.open(cutout) as im:
199 # NOTE: uncomment this to show the resulting image.
200 # im.show()
201 # NOTE: the dimensions here are determined by the matplotlib figure
202 # size (in inches) and the dpi (default=100), plus borders.
203 self.assertEqual((im.height, im.width), (450, 630))
205 def test_generate_image_multisize_cutouts_with_metadata(self):
206 """Test that we can add metadata to the image; it changes the height
207 a lot, and the width a little for the text boxes.
209 It's useful to have a person look at the output via:
210 im.show()
211 """
212 config = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask.ConfigClass()
213 config.add_metadata = True
214 config.sizes = [32, 64]
215 # output_path does nothing here, since we never write the file to disk.
216 cutouts = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask(config=config, output_path="")
217 cutout = cutouts.generate_image(self.science,
218 self.template,
219 self.difference,
220 skyCenter,
221 self.scale,
222 source=DATA.iloc[0])
223 with PIL.Image.open(cutout) as im:
224 # NOTE: uncomment this to show the resulting image.
225 # im.show()
226 # NOTE: the dimensions here are determined by the matplotlib figure
227 # size (in inches) and the dpi (default=100), plus borders.
228 self.assertEqual((im.height, im.width), (576, 645))
230 # A cutout without any flags: the dimensions should be unchanged.
231 cutout = cutouts.generate_image(self.science,
232 self.template,
233 self.difference,
234 skyCenter,
235 self.scale,
236 source=DATA.iloc[1])
237 with PIL.Image.open(cutout) as im:
238 # NOTE: uncomment this to show the resulting image.
239 # im.show()
240 # NOTE: the dimensions here are determined by the matplotlib figure
241 # size (in inches) and the dpi (default=100), plus borders.
242 self.assertEqual((im.height, im.width), (576, 645))
244 def test_write_images(self):
245 """Test that images get written to a temporary directory."""
246 butler = unittest.mock.Mock(spec=lsst.daf.butler.Butler)
247 # We don't care what the output images look like here, just that
248 # butler.get() returns an Exposure for every call.
249 butler.get.return_value = self.science
251 with tempfile.TemporaryDirectory() as path:
252 config = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask.ConfigClass()
253 cutouts = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask(config=config,
254 output_path=path)
255 result = cutouts.write_images(DATA, butler)
256 self.assertEqual(result, list(DATA["diaSourceId"]))
257 for file in ("images/506428274000260000/506428274000265570.png",
258 "images/527736141479140000/527736141479149732.png"):
259 filename = os.path.join(path, file)
260 self.assertTrue(os.path.exists(filename))
261 with PIL.Image.open(filename) as image:
262 self.assertEqual(image.format, "PNG")
264 def test_use_footprint(self):
265 """Test the use_footprint config option, generating a fake diaSrc
266 catalog that contains footprints that get used instead of config.sizes.
267 """
268 butler = unittest.mock.Mock(spec=lsst.daf.butler.Butler)
270 def mock_get(dataset, dataId, *args, **kwargs):
271 if "_diaSrc" in dataset:
272 # The science image is the only mock image with a source in it.
273 catalog = make_mock_catalog(self.science)
274 # Assign the matching source id to the detection.
275 match = DATA["visit"] == dataId["visit"]
276 catalog["id"] = DATA["diaSourceId"].to_numpy()[match][0]
277 return catalog
278 else:
279 return self.science
281 butler.get.side_effect = mock_get
283 with tempfile.TemporaryDirectory() as path:
284 config = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask.ConfigClass()
285 config.use_footprint = True
286 cutouts = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask(config=config,
287 output_path=path)
288 result = cutouts.write_images(DATA, butler)
289 self.assertEqual(result, list(DATA["diaSourceId"]))
290 for file in ("images/506428274000260000/506428274000265570.png",
291 "images/527736141479140000/527736141479149732.png"):
292 filename = os.path.join(path, file)
293 self.assertTrue(os.path.exists(filename))
294 with PIL.Image.open(filename) as image:
295 self.assertEqual(image.format, "PNG")
297 def test_write_images_exception(self):
298 """Test that write_images() catches errors in loading data."""
299 butler = unittest.mock.Mock(spec=lsst.daf.butler.Butler)
300 err = "Dataset not found"
301 butler.get.side_effect = LookupError(err)
303 with tempfile.TemporaryDirectory() as path:
304 config = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask.ConfigClass()
305 cutouts = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask(config=config,
306 output_path=path)
308 with self.assertLogs("lsst.plotImageSubtractionCutouts", "ERROR") as cm:
309 cutouts.write_images(DATA, butler)
310 self.assertIn(
311 "LookupError processing diaSourceId 506428274000265570: Dataset not found", cm.output[0]
312 )
313 self.assertIn(
314 "LookupError processing diaSourceId 527736141479149732: Dataset not found", cm.output[1]
315 )
317 def check_make_manifest(self, url_root, url_list):
318 """Check that make_manifest returns an appropriate DataFrame."""
319 data = [5, 10, 20]
320 config = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask.ConfigClass()
321 config.url_root = url_root
322 # output_path does nothing here
323 cutouts = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask(config=config, output_path="")
324 manifest = cutouts._make_manifest(data)
325 self.assertEqual(manifest["metadata:diaSourceId"].to_list(), [5, 10, 20])
326 self.assertEqual(manifest["location:1"].to_list(), url_list)
328 def test_make_manifest(self):
329 # check without an ending slash
330 root = "http://example.org/zooniverse"
331 url_list = [
332 f"{root}/images/5.png",
333 f"{root}/images/10.png",
334 f"{root}/images/20.png",
335 ]
336 self.check_make_manifest(root, url_list)
338 # check with an ending slash
339 root = "http://example.org/zooniverse/"
340 url_list = [
341 f"{root}images/5.png",
342 f"{root}images/10.png",
343 f"{root}images/20.png",
344 ]
345 self.check_make_manifest(root, url_list)
347 def test_pickle(self):
348 """Test that the task is pickleable (necessary for multiprocessing).
349 """
350 config = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask.ConfigClass()
351 config.sizes = [63]
352 cutouts = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask(config=config,
353 output_path="something")
354 other = pickle.loads(pickle.dumps(cutouts))
355 self.assertEqual(cutouts.config.sizes, other.config.sizes)
356 self.assertEqual(cutouts._output_path, other._output_path)
359class TestPlotImageSubtractionCutoutsMain(lsst.utils.tests.TestCase):
360 """Test the commandline interface main() function via mocks."""
361 def setUp(self):
362 datadir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data/")
363 self.sqlitefile = os.path.join(datadir, "apdb.sqlite3")
364 self.repo = "/not/a/real/butler"
365 self.collection = "mockRun"
366 self.outputPath = "/an/output/path"
367 self.configFile = os.path.join(datadir, "plotImageSubtractionCutoutsConfig.py")
368 self.instrument = "LATISS"
370 # DM-39501: mock butler, until detector/visit are in APDB.
371 butlerPatch = unittest.mock.patch("lsst.daf.butler.Butler")
372 self._butler = butlerPatch.start()
373 self.addCleanup(butlerPatch.stop)
374 # DM-39501: mock unpacker, until detector/visit are in APDB.
375 import lsst.obs.lsst
376 universe = lsst.daf.butler.DimensionUniverse()
377 data_id = lsst.daf.butler.DataCoordinate.standardize({"instrument": self.instrument},
378 universe=universe)
379 # ObservationDimensionPacker is harder to use in a butler-less
380 # environment, so we need a temporary dependency on obs_lsst until
381 # this all goes away on DM-39501.
382 packer = lsst.obs.lsst.RubinDimensionPacker(data_id, is_exposure=False)
383 instrumentPatch = unittest.mock.patch.object(lsst.pipe.base.Instrument,
384 "make_default_dimension_packer",
385 return_value=packer)
386 self._instrument = instrumentPatch.start()
387 self.addCleanup(instrumentPatch.stop)
389 def test_main_args(self):
390 """Test typical arguments to main()."""
391 args = [
392 "plotImageSubtractionCutouts",
393 f"--sqlitefile={self.sqlitefile}",
394 f"--collections={self.collection}",
395 f"-C={self.configFile}",
396 f"--instrument={self.instrument}",
397 self.repo,
398 self.outputPath,
399 ]
400 with unittest.mock.patch.object(
401 plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask, "run", autospec=True
402 ) as run, unittest.mock.patch.object(sys, "argv", args):
403 plotImageSubtractionCutouts.main()
404 self.assertEqual(self._butler.call_args.args, (self.repo,))
405 self.assertEqual(
406 self._butler.call_args.kwargs, {"collections": [self.collection]}
407 )
408 # NOTE: can't easily test the `data` arg to run, as select_sources
409 # reads in a random order every time.
410 self.assertEqual(run.call_args.args[2], self._butler.return_value)
412 def test_main_args_no_collections(self):
413 """Test with no collections argument."""
414 args = [
415 "plotImageSubtractionCutouts",
416 f"--sqlitefile={self.sqlitefile}",
417 f"-C={self.configFile}",
418 f"--instrument={self.instrument}",
419 self.repo,
420 self.outputPath,
421 ]
422 with unittest.mock.patch.object(
423 plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask, "run", autospec=True
424 ) as run, unittest.mock.patch.object(sys, "argv", args):
425 plotImageSubtractionCutouts.main()
426 self.assertEqual(self._butler.call_args.args, (self.repo,))
427 self.assertEqual(self._butler.call_args.kwargs, {"collections": None})
428 self.assertIsInstance(run.call_args.args[1], pd.DataFrame)
429 self.assertEqual(run.call_args.args[2], self._butler.return_value)
431 def test_main_collection_list(self):
432 """Test passing a list of collections."""
433 collections = ["mock1", "mock2", "mock3"]
434 args = [
435 "plotImageSubtractionCutouts",
436 f"--sqlitefile={self.sqlitefile}",
437 f"--instrument={self.instrument}",
438 self.repo,
439 self.outputPath,
440 f"-C={self.configFile}",
441 "--collections",
442 ]
443 args.extend(collections)
444 with unittest.mock.patch.object(
445 plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask, "run", autospec=True
446 ) as run, unittest.mock.patch.object(sys, "argv", args):
447 plotImageSubtractionCutouts.main()
448 self.assertEqual(self._butler.call_args.args, (self.repo,))
449 self.assertEqual(
450 self._butler.call_args.kwargs, {"collections": collections}
451 )
452 self.assertIsInstance(run.call_args.args[1], pd.DataFrame)
453 self.assertEqual(run.call_args.args[2], self._butler.return_value)
455 def test_main_args_limit_offset(self):
456 """Test typical arguments to main()."""
457 args = [
458 "plotImageSubtractionCutouts",
459 f"--sqlitefile={self.sqlitefile}",
460 f"--collections={self.collection}",
461 f"-C={self.configFile}",
462 f"--instrument={self.instrument}",
463 "--all",
464 "--limit=5",
465 self.repo,
466 self.outputPath,
467 ]
468 with unittest.mock.patch.object(
469 plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask,
470 "write_images",
471 autospec=True,
472 return_value=[5]
473 ) as write_images, unittest.mock.patch.object(
474 plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask,
475 "write_manifest",
476 autospec=True
477 ) as write_manifest, unittest.mock.patch.object(sys, "argv", args):
478 plotImageSubtractionCutouts.main()
479 self.assertEqual(self._butler.call_args.args, (self.repo,))
480 self.assertEqual(
481 self._butler.call_args.kwargs, {"collections": [self.collection]}
482 )
483 self.assertIsInstance(write_images.call_args.args[1], pd.DataFrame)
484 self.assertEqual(write_images.call_args.args[2], self._butler.return_value)
485 # The test apdb contains 290 DiaSources, so we get the return of
486 # `write_images` (enforced as `5` above) 58 times.
487 self.assertEqual(write_manifest.call_args.args[1], 58*[5])
489 @unittest.skip("Mock and multiprocess don't mix: https://github.com/python/cpython/issues/100090")
490 def test_main_args_multiprocessing(self):
491 """Test running with multiprocessing.
492 """
493 args = [
494 "plotImageSubtractionCutouts",
495 f"--sqlitefile={self.sqlitefile}",
496 f"--collections={self.collection}",
497 "-j2",
498 f"-C={self.configFile}",
499 f"--instrument={self.instrument}",
500 self.repo,
501 self.outputPath,
502 ]
503 with unittest.mock.patch.object(
504 plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask, "run", autospec=True
505 ) as run, unittest.mock.patch.object(sys, "argv", args):
506 plotImageSubtractionCutouts.main()
507 self.assertEqual(self._butler.call_args.args, (self.repo,))
508 self.assertEqual(self._butler.call_args.kwargs, {"collections": [self.collection]})
509 # NOTE: can't easily test the `data` arg to run, as select_sources
510 # reads in a random order every time.
511 self.assertEqual(run.call_args.args[2], self._butler.return_value)
514class TestCutoutPath(lsst.utils.tests.TestCase):
515 def test_normal_path(self):
516 """Can the path manager handles non-chunked paths?
517 """
518 manager = plotImageSubtractionCutouts.CutoutPath("some/root/path")
519 path = manager(id=12345678)
520 self.assertEqual(path, "some/root/path/images/12345678.png")
522 def test_chunking(self):
523 """Can the path manager handle ids chunked into 10,000 file
524 directories?
525 """
526 manager = plotImageSubtractionCutouts.CutoutPath("some/root/path", chunk_size=10000)
527 path = manager(id=12345678)
528 self.assertEqual(path, "some/root/path/images/12340000/12345678.png")
530 def test_chunk_sizes(self):
531 """Test valid and invalid values for the chunk_size parameter.
532 """
533 with self.assertRaisesRegex(RuntimeError, "chunk_size must be a power of 10"):
534 plotImageSubtractionCutouts.CutoutPath("some/root/path", chunk_size=123)
536 with self.assertRaisesRegex(RuntimeError, "chunk_size must be a power of 10"):
537 plotImageSubtractionCutouts.CutoutPath("some/root/path", chunk_size=12300)
539 # should not raise
540 plotImageSubtractionCutouts.CutoutPath("some/root/path", chunk_size=1000)
541 plotImageSubtractionCutouts.CutoutPath("some/root/path", chunk_size=1000000)
544class TestMemory(lsst.utils.tests.MemoryTestCase):
545 pass
548def setup_module(module):
549 lsst.utils.tests.init()
552if __name__ == "__main__": 552 ↛ 553line 552 didn't jump to line 553, because the condition on line 552 was never true
553 lsst.utils.tests.init()
554 unittest.main()