Coverage for tests/test_appipe.py: 28%
88 statements
« prev ^ index » next coverage.py v6.4.1, created at 2022-06-07 02:31 -0700
« prev ^ index » next coverage.py v6.4.1, created at 2022-06-07 02:31 -0700
1#
2# This file is part of ap_pipe.
3#
4# Developed for the LSST Data Management System.
5# This product includes software developed by the LSST Project
6# (http://www.lsst.org).
7# See the COPYRIGHT file at the top-level directory of this distribution
8# for details of code ownership.
9#
10# This program is free software: you can redistribute it and/or modify
11# it under the terms of the GNU General Public License as published by
12# the Free Software Foundation, either version 3 of the License, or
13# (at your option) any later version.
14#
15# This program is distributed in the hope that it will be useful,
16# but WITHOUT ANY WARRANTY; without even the implied warranty of
17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18# GNU General Public License for more details.
19#
20# You should have received a copy of the GNU General Public License
21# along with this program. If not, see <http://www.gnu.org/licenses/>.
22#
24import contextlib
25import os
26import unittest
27from unittest.mock import patch, Mock, ANY
29import lsst.utils.tests
30import lsst.daf.persistence as dafPersist
31import lsst.pipe.base as pipeBase
33from lsst.ap.pipe import ApPipeTask
36class PipelineTestSuite(lsst.utils.tests.TestCase):
37 '''
38 A set of tests for the functions in ap_pipe.
39 '''
41 @classmethod
42 def _makeDefaultConfig(cls):
43 config = ApPipeTask.ConfigClass()
44 config.load(os.path.join(cls.datadir, "config", "apPipe.py"))
45 config.diaPipe.apdb.db_url = "sqlite://"
46 return config
48 @classmethod
49 def setUpClass(cls):
50 try:
51 cls.datadir = lsst.utils.getPackageDir("ap_pipe_testdata")
52 except LookupError:
53 raise unittest.SkipTest("ap_pipe_testdata not set up")
54 try:
55 lsst.utils.getPackageDir("obs_decam")
56 except LookupError:
57 raise unittest.SkipTest("obs_decam not set up; needed for ap_pipe_testdata")
59 def _setupObjPatch(self, *args, **kwargs):
60 """Create a patch in setUp that will be reverted once the test ends.
62 Parameters
63 ----------
64 *args
65 **kwargs
66 Inputs to `unittest.mock.patch.object`.
67 """
68 patcher = patch.object(*args, **kwargs)
69 patcher.start()
70 self.addCleanup(patcher.stop)
72 def setUp(self):
73 self.config = self._makeDefaultConfig()
74 self.butler = dafPersist.Butler(inputs={'root': self.datadir})
76 def makeMockDataRef(datasetType, level=None, dataId={}, **rest):
77 mockDataRef = Mock(dafPersist.ButlerDataRef)
78 mockDataRef.dataId = dict(dataId, **rest)
79 return mockDataRef
81 self._setupObjPatch(self.butler, "dataRef", side_effect=makeMockDataRef)
82 self.dataId = {"visit": 413635, "ccdnum": 42}
83 self.inputRef = self.butler.dataRef("raw", **self.dataId)
85 @contextlib.contextmanager
86 def mockPatchSubtasks(self, task):
87 """Make mocks for all the ap_pipe subtasks.
89 This is needed because the task itself cannot be a mock.
90 The task's subtasks do not exist until the task is created, so
91 this allows us to mock them instead.
93 Parameters
94 ----------
95 task : `lsst.ap.pipe.ApPipeTask`
96 The task whose subtasks will be mocked.
98 Yields
99 ------
100 subtasks : `lsst.pipe.base.Struct`
101 All mocks created by this context manager, including:
103 ``ccdProcessor``
104 ``differencer``
105 ``diaPipe``
106 a mock for the corresponding subtask. Mocks do not return any
107 particular value, but have mocked methods that can be queried
108 for calls by ApPipeTask
109 """
110 with patch.object(task, "ccdProcessor", autospec=True) as mockCcdProcessor, \
111 patch.object(task, "differencer", autospec=True) as mockDifferencer, \
112 patch.object(task, "transformDiaSrcCat", autospec=True) as mockTransform, \
113 patch.object(task, "diaPipe", autospec=True) as mockDiaPipe:
114 yield pipeBase.Struct(ccdProcessor=mockCcdProcessor,
115 differencer=mockDifferencer,
116 transformDiaSrcCat=mockTransform,
117 diaPipe=mockDiaPipe)
119 def testGenericRun(self):
120 """Test the normal workflow of each ap_pipe step.
121 """
122 task = ApPipeTask(self.butler, config=self.config)
123 with self.mockPatchSubtasks(task) as subtasks:
124 task.runDataRef(self.inputRef)
125 subtasks.ccdProcessor.runDataRef.assert_called_once()
126 subtasks.differencer.runDataRef.assert_called_once()
127 subtasks.diaPipe.run.assert_called_once()
129 def testReuseExistingOutput(self):
130 """Test reuse keyword to ApPipeTask.runDataRef.
131 """
132 task = ApPipeTask(self.butler, config=self.config)
134 self.checkReuseExistingOutput(task, ['ccdProcessor'])
135 self.checkReuseExistingOutput(task, ['ccdProcessor', 'differencer'])
136 self.checkReuseExistingOutput(task, ['ccdProcessor', 'differencer', 'diaPipe'])
138 def checkReuseExistingOutput(self, task, skippable):
139 """Check whether a task's subtasks are skipped when "reuse" is set.
141 Mock guarantees that all "has this been made" tests pass,
142 so skippable subtasks should actually be skipped.
143 """
144 # extremely brittle because it depends on internal code of runDataRef
145 # but it only needs to work until DM-21886, then we can unit-test the subtask
146 internalRef = self.inputRef.getButler().dataRef()
147 internalRef.put.reset_mock()
149 with self.mockPatchSubtasks(task) as subtasks:
150 struct = task.runDataRef(self.inputRef, reuse=skippable)
151 for subtaskName, runner in {
152 'ccdProcessor': subtasks.ccdProcessor.runDataRef,
153 'differencer': subtasks.differencer.runDataRef,
154 'diaPipe': subtasks.diaPipe.run,
155 }.items():
156 msg = "subtask = " + subtaskName
157 if subtaskName in skippable:
158 runner.assert_not_called()
159 self.assertIsNone(struct.getDict()[subtaskName], msg=msg)
160 else:
161 runner.assert_called_once()
162 self.assertIsNotNone(struct.getDict()[subtaskName], msg=msg)
164 if 'diaPipe' in skippable:
165 internalRef.put.assert_not_called()
166 else:
167 internalRef.put.assert_called_once_with(ANY, "apdb_marker")
169 def testCalexpRun(self):
170 """Test the calexp template workflow of each ap_pipe step.
171 """
172 calexpConfigFile = os.path.join(lsst.utils.getPackageDir('ap_pipe'),
173 'config', 'calexpTemplates.py')
174 calexpConfig = self._makeDefaultConfig()
175 calexpConfig.load(calexpConfigFile)
176 calexpConfig.differencer.doSelectSources = False # Workaround for DM-18394
178 task = ApPipeTask(self.butler, config=calexpConfig)
179 with self.mockPatchSubtasks(task) as subtasks:
180 # We use the same dataId here for both template and science
181 # in difference imaging. This is OK because everything is a mock
182 # and we aren't actually doing any image processing.
183 task.runDataRef(self.inputRef, templateIds=[self.dataId])
184 self.assertEqual(subtasks.ccdProcessor.runDataRef.call_count, 2)
185 subtasks.differencer.runDataRef.assert_called_once()
186 subtasks.diaPipe.run.assert_called_once()
189class MemoryTester(lsst.utils.tests.MemoryTestCase):
190 pass
193def setup_module(module):
194 lsst.utils.tests.init()
197if __name__ == "__main__": 197 ↛ 198line 197 didn't jump to line 198, because the condition on line 197 was never true
198 lsst.utils.tests.init()
199 unittest.main()