Coverage for tests/test_executors.py : 27%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# This file is part of ctrl_mpexec.
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/>.
22"""Simple unit test for cmdLineFwk module.
23"""
25import logging
26from multiprocessing import Manager
27import time
28from types import SimpleNamespace
29import unittest
31from lsst.ctrl.mpexec import MPGraphExecutor, MPGraphExecutorError, MPTimeoutError, QuantumExecutor
32from lsst.ctrl.mpexec.execFixupDataId import ExecFixupDataId
35logging.basicConfig(level=logging.DEBUG)
37_LOG = logging.getLogger(__name__)
40class QuantumExecutorMock(QuantumExecutor):
41 """Mock class for QuantumExecutor
42 """
43 def __init__(self, mp=False):
44 self.quanta = []
45 if mp:
46 # in multiprocess mode use shared list
47 manager = Manager()
48 self.quanta = manager.list()
50 def execute(self, taskDef, quantum, butler):
51 _LOG.debug("QuantumExecutorMock.execute: taskDef=%s dataId=%s", taskDef, quantum.dataId)
52 if taskDef.taskClass:
53 # only works for TaskMockMP class below
54 taskDef.taskClass().runQuantum()
55 self.quanta.append(quantum)
57 def getDataIds(self, field):
58 """Returns values for dataId field for each visited quanta"""
59 return [quantum.dataId[field] for quantum in self.quanta]
62class QuantumIterDataMock:
63 """Simple class to mock QuantumIterData.
64 """
65 def __init__(self, index, taskDef, **dataId):
66 self.index = index
67 self.taskDef = taskDef
68 self.quantum = SimpleNamespace(dataId=dataId)
69 self.dependencies = set()
72class QuantumGraphMock:
73 """Mock for quantum graph.
74 """
75 def __init__(self, qdata):
76 self.qdata = qdata
78 def traverse(self):
79 return self.qdata
82class TaskMockMP:
83 """Simple mock class for task supporting multiprocessing.
84 """
85 canMultiprocess = True
87 def runQuantum(self):
88 _LOG.debug("TaskMockMP.runQuantum")
89 pass
92class TaskMockFail:
93 """Simple mock class for task which fails.
94 """
95 canMultiprocess = True
97 def runQuantum(self):
98 _LOG.debug("TaskMockFail.runQuantum")
99 raise ValueError("expected failure")
102class TaskMockSleep:
103 """Simple mock class for task which fails.
104 """
105 canMultiprocess = True
107 def runQuantum(self):
108 _LOG.debug("TaskMockSleep.runQuantum")
109 time.sleep(3.)
112class TaskMockNoMP:
113 """Simple mock class for task not supporting multiprocessing.
114 """
115 canMultiprocess = False
118class TaskDefMock:
119 """Simple mock class for task definition in a pipeline.
120 """
121 def __init__(self, taskName="Task", config=None, taskClass=TaskMockMP, label="task1"):
122 self.taskName = taskName
123 self.config = config
124 self.taskClass = taskClass
125 self.label = label
127 def __str__(self):
128 return f"TaskDefMock(taskName={self.taskName}, taskClass={self.taskClass.__name__})"
131class MPGraphExecutorTestCase(unittest.TestCase):
132 """A test case for MPGraphExecutor class
133 """
135 def test_mpexec_nomp(self):
136 """Make simple graph and execute"""
138 taskDef = TaskDefMock()
139 qgraph = QuantumGraphMock([
140 QuantumIterDataMock(index=i, taskDef=taskDef, detector=i) for i in range(3)
141 ])
143 # run in single-process mode
144 qexec = QuantumExecutorMock()
145 mpexec = MPGraphExecutor(numProc=1, timeout=100, quantumExecutor=qexec)
146 mpexec.execute(qgraph, butler=None)
147 self.assertEqual(qexec.getDataIds("detector"), [0, 1, 2])
149 def test_mpexec_mp(self):
150 """Make simple graph and execute"""
152 taskDef = TaskDefMock()
153 qgraph = QuantumGraphMock([
154 QuantumIterDataMock(index=i, taskDef=taskDef, detector=i) for i in range(3)
155 ])
157 # run in multi-process mode, the order of results is not defined
158 qexec = QuantumExecutorMock(mp=True)
159 mpexec = MPGraphExecutor(numProc=3, timeout=100, quantumExecutor=qexec)
160 mpexec.execute(qgraph, butler=None)
161 self.assertCountEqual(qexec.getDataIds("detector"), [0, 1, 2])
163 def test_mpexec_nompsupport(self):
164 """Try to run MP for task that has no MP support which should fail
165 """
167 taskDef = TaskDefMock(taskClass=TaskMockNoMP)
168 qgraph = QuantumGraphMock([
169 QuantumIterDataMock(index=i, taskDef=taskDef, detector=i) for i in range(3)
170 ])
172 # run in multi-process mode
173 qexec = QuantumExecutorMock()
174 mpexec = MPGraphExecutor(numProc=3, timeout=100, quantumExecutor=qexec)
175 with self.assertRaises(MPGraphExecutorError):
176 mpexec.execute(qgraph, butler=None)
178 def test_mpexec_fixup(self):
179 """Make simple graph and execute, add dependencies by executing fixup code
180 """
182 taskDef = TaskDefMock()
184 for reverse in (False, True):
186 qgraph = QuantumGraphMock([
187 QuantumIterDataMock(index=i, taskDef=taskDef, detector=i) for i in range(3)
188 ])
190 qexec = QuantumExecutorMock()
191 fixup = ExecFixupDataId("task1", "detector", reverse=reverse)
192 mpexec = MPGraphExecutor(numProc=1, timeout=100, quantumExecutor=qexec,
193 executionGraphFixup=fixup)
194 mpexec.execute(qgraph, butler=None)
196 expected = [0, 1, 2]
197 if reverse:
198 expected = list(reversed(expected))
199 self.assertEqual(qexec.getDataIds("detector"), expected)
201 def test_mpexec_timeout(self):
202 """Fail due to timeout"""
204 taskDef = TaskDefMock()
205 taskDefSleep = TaskDefMock(taskClass=TaskMockSleep)
206 qgraph = QuantumGraphMock([
207 QuantumIterDataMock(index=0, taskDef=taskDef, detector=0),
208 QuantumIterDataMock(index=1, taskDef=taskDefSleep, detector=1),
209 QuantumIterDataMock(index=2, taskDef=taskDef, detector=2),
210 ])
212 # with failFast we'll get immediate MPTimeoutError
213 qexec = QuantumExecutorMock(mp=True)
214 mpexec = MPGraphExecutor(numProc=3, timeout=1, quantumExecutor=qexec, failFast=True)
215 with self.assertRaises(MPTimeoutError):
216 mpexec.execute(qgraph, butler=None)
218 # with failFast=False exception happens after last task finishes
219 qexec = QuantumExecutorMock(mp=True)
220 mpexec = MPGraphExecutor(numProc=3, timeout=1, quantumExecutor=qexec, failFast=False)
221 with self.assertRaises(MPTimeoutError):
222 mpexec.execute(qgraph, butler=None)
223 self.assertCountEqual(qexec.getDataIds("detector"), [0, 2])
225 def test_mpexec_failure(self):
226 """Failure in one task should not stop other tasks"""
228 taskDef = TaskDefMock()
229 taskDefFail = TaskDefMock(taskClass=TaskMockFail)
230 qgraph = QuantumGraphMock([
231 QuantumIterDataMock(index=0, taskDef=taskDef, detector=0),
232 QuantumIterDataMock(index=1, taskDef=taskDefFail, detector=1),
233 QuantumIterDataMock(index=2, taskDef=taskDef, detector=2),
234 ])
236 qexec = QuantumExecutorMock(mp=True)
237 mpexec = MPGraphExecutor(numProc=3, timeout=100, quantumExecutor=qexec)
238 with self.assertRaises(MPGraphExecutorError):
239 mpexec.execute(qgraph, butler=None)
240 self.assertCountEqual(qexec.getDataIds("detector"), [0, 2])
242 def test_mpexec_failure_dep(self):
243 """Failure in one task should skip dependents"""
245 taskDef = TaskDefMock()
246 taskDefFail = TaskDefMock(taskClass=TaskMockFail)
247 qdata = [
248 QuantumIterDataMock(index=0, taskDef=taskDef, detector=0),
249 QuantumIterDataMock(index=1, taskDef=taskDefFail, detector=1),
250 QuantumIterDataMock(index=2, taskDef=taskDef, detector=2),
251 QuantumIterDataMock(index=3, taskDef=taskDef, detector=3),
252 QuantumIterDataMock(index=4, taskDef=taskDef, detector=4),
253 ]
254 qdata[2].dependencies.add(1)
255 qdata[4].dependencies.add(3)
256 qdata[4].dependencies.add(2)
258 qgraph = QuantumGraphMock(qdata)
260 qexec = QuantumExecutorMock(mp=True)
261 mpexec = MPGraphExecutor(numProc=3, timeout=100, quantumExecutor=qexec)
262 with self.assertRaises(MPGraphExecutorError):
263 mpexec.execute(qgraph, butler=None)
264 self.assertCountEqual(qexec.getDataIds("detector"), [0, 3])
266 def test_mpexec_failure_failfast(self):
267 """Fast fail stops quickly.
269 Timing delay of task #3 should be sufficient to process
270 failure and raise exception.
271 """
273 taskDef = TaskDefMock()
274 taskDefFail = TaskDefMock(taskClass=TaskMockFail)
275 taskDefSleep = TaskDefMock(taskClass=TaskMockSleep)
276 qdata = [
277 QuantumIterDataMock(index=0, taskDef=taskDef, detector=0),
278 QuantumIterDataMock(index=1, taskDef=taskDefFail, detector=1),
279 QuantumIterDataMock(index=2, taskDef=taskDef, detector=2),
280 QuantumIterDataMock(index=3, taskDef=taskDefSleep, detector=3),
281 QuantumIterDataMock(index=4, taskDef=taskDef, detector=4),
282 ]
283 qdata[1].dependencies.add(0)
284 qdata[2].dependencies.add(1)
285 qdata[4].dependencies.add(3)
286 qdata[4].dependencies.add(2)
288 qgraph = QuantumGraphMock(qdata)
290 qexec = QuantumExecutorMock(mp=True)
291 mpexec = MPGraphExecutor(numProc=3, timeout=100, quantumExecutor=qexec, failFast=True)
292 with self.assertRaises(MPGraphExecutorError):
293 mpexec.execute(qgraph, butler=None)
294 self.assertCountEqual(qexec.getDataIds("detector"), [0])
297if __name__ == "__main__": 297 ↛ 298line 297 didn't jump to line 298, because the condition on line 297 was never true
298 unittest.main()