Hide keyboard shortcuts

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/>. 

21 

22"""Simple unit test for cmdLineFwk module. 

23""" 

24 

25import logging 

26from multiprocessing import Manager 

27import time 

28from types import SimpleNamespace 

29import unittest 

30 

31from lsst.ctrl.mpexec import MPGraphExecutor, MPGraphExecutorError, MPTimeoutError, QuantumExecutor 

32from lsst.ctrl.mpexec.execFixupDataId import ExecFixupDataId 

33 

34 

35logging.basicConfig(level=logging.DEBUG) 

36 

37_LOG = logging.getLogger(__name__) 

38 

39 

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() 

49 

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) 

56 

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] 

60 

61 

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() 

70 

71 

72class QuantumGraphMock: 

73 """Mock for quantum graph. 

74 """ 

75 def __init__(self, qdata): 

76 self.qdata = qdata 

77 

78 def traverse(self): 

79 return self.qdata 

80 

81 

82class TaskMockMP: 

83 """Simple mock class for task supporting multiprocessing. 

84 """ 

85 canMultiprocess = True 

86 

87 def runQuantum(self): 

88 _LOG.debug("TaskMockMP.runQuantum") 

89 pass 

90 

91 

92class TaskMockFail: 

93 """Simple mock class for task which fails. 

94 """ 

95 canMultiprocess = True 

96 

97 def runQuantum(self): 

98 _LOG.debug("TaskMockFail.runQuantum") 

99 raise ValueError("expected failure") 

100 

101 

102class TaskMockSleep: 

103 """Simple mock class for task which fails. 

104 """ 

105 canMultiprocess = True 

106 

107 def runQuantum(self): 

108 _LOG.debug("TaskMockSleep.runQuantum") 

109 time.sleep(3.) 

110 

111 

112class TaskMockNoMP: 

113 """Simple mock class for task not supporting multiprocessing. 

114 """ 

115 canMultiprocess = False 

116 

117 

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 

126 

127 def __str__(self): 

128 return f"TaskDefMock(taskName={self.taskName}, taskClass={self.taskClass.__name__})" 

129 

130 

131class MPGraphExecutorTestCase(unittest.TestCase): 

132 """A test case for MPGraphExecutor class 

133 """ 

134 

135 def test_mpexec_nomp(self): 

136 """Make simple graph and execute""" 

137 

138 taskDef = TaskDefMock() 

139 qgraph = QuantumGraphMock([ 

140 QuantumIterDataMock(index=i, taskDef=taskDef, detector=i) for i in range(3) 

141 ]) 

142 

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]) 

148 

149 def test_mpexec_mp(self): 

150 """Make simple graph and execute""" 

151 

152 taskDef = TaskDefMock() 

153 qgraph = QuantumGraphMock([ 

154 QuantumIterDataMock(index=i, taskDef=taskDef, detector=i) for i in range(3) 

155 ]) 

156 

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]) 

162 

163 def test_mpexec_nompsupport(self): 

164 """Try to run MP for task that has no MP support which should fail 

165 """ 

166 

167 taskDef = TaskDefMock(taskClass=TaskMockNoMP) 

168 qgraph = QuantumGraphMock([ 

169 QuantumIterDataMock(index=i, taskDef=taskDef, detector=i) for i in range(3) 

170 ]) 

171 

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) 

177 

178 def test_mpexec_fixup(self): 

179 """Make simple graph and execute, add dependencies by executing fixup code 

180 """ 

181 

182 taskDef = TaskDefMock() 

183 

184 for reverse in (False, True): 

185 

186 qgraph = QuantumGraphMock([ 

187 QuantumIterDataMock(index=i, taskDef=taskDef, detector=i) for i in range(3) 

188 ]) 

189 

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) 

195 

196 expected = [0, 1, 2] 

197 if reverse: 

198 expected = list(reversed(expected)) 

199 self.assertEqual(qexec.getDataIds("detector"), expected) 

200 

201 def test_mpexec_timeout(self): 

202 """Fail due to timeout""" 

203 

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 ]) 

211 

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) 

217 

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]) 

224 

225 def test_mpexec_failure(self): 

226 """Failure in one task should not stop other tasks""" 

227 

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 ]) 

235 

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]) 

241 

242 def test_mpexec_failure_dep(self): 

243 """Failure in one task should skip dependents""" 

244 

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) 

257 

258 qgraph = QuantumGraphMock(qdata) 

259 

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]) 

265 

266 def test_mpexec_failure_failfast(self): 

267 """Fast fail stops quickly. 

268 

269 Timing delay of task #3 should be sufficient to process 

270 failure and raise exception. 

271 """ 

272 

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) 

287 

288 qgraph = QuantumGraphMock(qdata) 

289 

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]) 

295 

296 

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

298 unittest.main()