Coverage for tests/test_handlers.py: 23%

199 statements  

« prev     ^ index     » next       coverage.py v7.5.1, created at 2024-05-11 03:48 -0700

1# This file is part of ctrl_bps_htcondor. 

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 software is dual licensed under the GNU General Public License and also 

10# under a 3-clause BSD license. Recipients may choose which of these licenses 

11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt, 

12# respectively. If you choose the GPL option then the following text applies 

13# (but note that there is still no warranty even if you opt for BSD instead): 

14# 

15# This program is free software: you can redistribute it and/or modify 

16# it under the terms of the GNU General Public License as published by 

17# the Free Software Foundation, either version 3 of the License, or 

18# (at your option) any later version. 

19# 

20# This program is distributed in the hope that it will be useful, 

21# but WITHOUT ANY WARRANTY; without even the implied warranty of 

22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

23# GNU General Public License for more details. 

24# 

25# You should have received a copy of the GNU General Public License 

26# along with this program. If not, see <https://www.gnu.org/licenses/>. 

27 

28"""Unit tests for job ClassAd handlers.""" 

29 

30import logging 

31import unittest 

32from typing import Any 

33 

34from lsst.ctrl.bps.htcondor.handlers import ( 

35 Chain, 

36 Handler, 

37 JobCompletedWithExecTicketHandler, 

38 JobCompletedWithoutExecTicketHandler, 

39 JobHeldByOtherHandler, 

40 JobHeldBySignalHandler, 

41 JobHeldByUserHandler, 

42) 

43 

44logger = logging.getLogger("lsst.ctrl.bps.htcondor") 

45 

46 

47class DummyHandler(Handler): 

48 """A concrete handler that does nothing.""" 

49 

50 def handle(self, ad: dict[str, Any]) -> dict[str, Any]: 

51 pass 

52 

53 

54class RaisingHandler(Handler): 

55 """A concrete handler that raises KeyError exception.""" 

56 

57 def handle(self, ad: dict[str, Any]) -> dict[str, Any]: 

58 raise KeyError("foo") 

59 

60 

61class ChainTestCase(unittest.TestCase): 

62 """Test the Chain class.""" 

63 

64 def setUp(self): 

65 pass 

66 

67 def tearDown(self): 

68 pass 

69 

70 def testDefaultInitialization(self): 

71 chain = Chain() 

72 self.assertEqual(len(chain), 0) 

73 

74 def testCustomInitialization(self): 

75 handler = DummyHandler() 

76 chain = Chain(handlers=[handler]) 

77 self.assertEqual(len(chain), 1) 

78 self.assertIs(chain[0], handler) 

79 

80 def testAppendingHandler(self): 

81 first = DummyHandler() 

82 second = DummyHandler() 

83 chain = Chain(handlers=[first]) 

84 chain.append(second) 

85 self.assertEqual(len(chain), 2) 

86 self.assertIs(chain[0], first) 

87 self.assertIs(chain[1], second) 

88 

89 def testAppendingNonHandler(self): 

90 handler = "foo" 

91 chain = Chain() 

92 with self.assertRaises(TypeError): 

93 chain.append(handler) 

94 

95 

96class JobCompletedWithExecTicketHandlerTestCase(unittest.TestCase): 

97 """Test the handler for a completed job with the ticket of execution.""" 

98 

99 def setUp(self): 

100 self.ad = {"ClusterId": 1, "ProcId": 0, "MyType": "JobTerminatedEvent"} 

101 self.handler = JobCompletedWithExecTicketHandler() 

102 

103 def tearDown(self): 

104 pass 

105 

106 def testNormalTermination(self): 

107 ad = self.ad | {"ToE": {"ExitBySignal": False, "ExitCode": 0}} 

108 result = self.handler.handle(ad) 

109 self.assertIsNotNone(result) 

110 self.assertIn("ExitBySignal", result) 

111 self.assertFalse(result["ExitBySignal"]) 

112 self.assertIn("ExitCode", result) 

113 self.assertEqual(result["ExitCode"], 0) 

114 

115 def testAbnormalTermination(self): 

116 ad = self.ad | {"ToE": {"ExitBySignal": True, "ExitSignal": 9}} 

117 result = self.handler.handle(ad) 

118 self.assertIsNotNone(result) 

119 self.assertIn("ExitBySignal", result) 

120 self.assertTrue(result["ExitBySignal"]) 

121 self.assertIn("ExitSignal", result) 

122 self.assertEqual(result["ExitSignal"], 9) 

123 

124 def testNotHandlingMissingExecTicket(self): 

125 with self.assertLogs(logger=logger, level="DEBUG") as cm: 

126 result = self.handler.handle(self.ad) 

127 self.assertIsNone(result) 

128 self.assertIn("ticket of execution", cm.output[0]) 

129 self.assertIn("missing", cm.output[0]) 

130 

131 def testNotHandlingJobNotCompleted(self): 

132 ad = self.ad | {"MyType": "foo"} 

133 with self.assertLogs(logger=logger, level="DEBUG") as cm: 

134 result = self.handler.handle(ad) 

135 self.assertIsNone(result) 

136 self.assertIn("job not completed", cm.output[0]) 

137 

138 

139class JobCompletedWithoutExecTicketHandlerTestCase(unittest.TestCase): 

140 """Test the handler for a completed job w/o the ticket of execution.""" 

141 

142 def setUp(self): 

143 self.ad = {"ClusterId": 1, "ProcId": 0, "MyType": "JobTerminatedEvent"} 

144 self.handler = JobCompletedWithoutExecTicketHandler() 

145 

146 def tearDown(self): 

147 pass 

148 

149 def testNormalTermination(self): 

150 ad = self.ad | {"TerminatedNormally": True, "ReturnValue": 0} 

151 result = self.handler.handle(ad) 

152 self.assertIsNotNone(result) 

153 self.assertIn("ExitBySignal", result) 

154 self.assertFalse(result["ExitBySignal"]) 

155 self.assertIn("ExitCode", result) 

156 self.assertEqual(result["ExitCode"], 0) 

157 

158 def testAbnormalTermination(self): 

159 ad = self.ad | {"TerminatedNormally": False, "TerminatedBySignal": 9} 

160 result = self.handler.handle(ad) 

161 self.assertIsNotNone(result) 

162 self.assertIn("ExitBySignal", result) 

163 self.assertTrue(result["ExitBySignal"]) 

164 self.assertIn("ExitSignal", result) 

165 self.assertEqual(result["ExitSignal"], 9) 

166 

167 def testNotHandlingExecTicketExists(self): 

168 ad = self.ad | {"ToE": {"ExitBySignal": False, "ExitCode": 0}} 

169 with self.assertLogs(logger=logger, level="DEBUG") as cm: 

170 result = self.handler.handle(ad) 

171 self.assertIsNone(result) 

172 self.assertIn("ticket of execution", cm.output[0]) 

173 self.assertIn("found", cm.output[0]) 

174 

175 def testNotHandlingJobNotCompleted(self): 

176 ad = self.ad | {"MyType": "foo"} 

177 with self.assertLogs(logger=logger, level="DEBUG") as cm: 

178 result = self.handler.handle(ad) 

179 self.assertIsNone(result) 

180 self.assertIn("job not completed", cm.output[0]) 

181 

182 

183class JobHeldOtherTestCase(unittest.TestCase): 

184 """Test the handler for a held job.""" 

185 

186 def setUp(self): 

187 self.ad = {"ClusterId": 1, "ProcId": 0, "MyType": "JobHeldEvent"} 

188 self.handler = JobHeldByOtherHandler() 

189 

190 def tearDown(self): 

191 pass 

192 

193 def testHeld(self): 

194 ad = self.ad | {"HoldReasonCode": 42} 

195 result = self.handler.handle(ad) 

196 self.assertIsNotNone(result) 

197 self.assertIn("ExitBySignal", result) 

198 self.assertFalse(result["ExitBySignal"]) 

199 self.assertIn("ExitCode", result) 

200 self.assertEqual(result["ExitCode"], 42) 

201 

202 def testHeldBySignal(self): 

203 ad = self.ad | {"HoldReasonCode": 3} 

204 with self.assertLogs(logger=logger, level="DEBUG") as cm: 

205 result = self.handler.handle(ad) 

206 self.assertIsNone(result) 

207 self.assertIn("invalid hold reason code", cm.output[0]) 

208 self.assertIn("HoldReasonCode = 3", cm.output[0]) 

209 

210 def testHeldByUser(self): 

211 ad = self.ad | {"HoldReasonCode": 1} 

212 with self.assertLogs(logger=logger, level="DEBUG") as cm: 

213 result = self.handler.handle(ad) 

214 self.assertIsNone(result) 

215 self.assertIn("invalid hold reason code", cm.output[0]) 

216 self.assertIn("HoldReasonCode = 1", cm.output[0]) 

217 

218 def testNotHandlingJobNotHeld(self): 

219 ad = self.ad | {"MyType": "foo"} 

220 with self.assertLogs(logger=logger, level="DEBUG") as cm: 

221 result = self.handler.handle(ad) 

222 self.assertIsNone(result) 

223 self.assertIn("job not held", cm.output[0]) 

224 

225 

226class JobHeldBySignalHandlerTestCase(unittest.TestCase): 

227 """Test the handler for a job held by a signal.""" 

228 

229 def setUp(self): 

230 self.ad = {"ClusterId": 1, "ProcId": 0, "MyType": "JobHeldEvent"} 

231 self.handler = JobHeldBySignalHandler() 

232 

233 def tearDown(self): 

234 pass 

235 

236 def testSignalAvailable(self): 

237 ad = self.ad | {"HoldReasonCode": 3, "HoldReason": "Job raised a signal 9."} 

238 result = self.handler.handle(ad) 

239 self.assertIsNotNone(ad) 

240 self.assertIn("ExitBySignal", result) 

241 self.assertTrue(result["ExitBySignal"]) 

242 self.assertIn("ExitSignal", result) 

243 self.assertEqual(int(result["ExitSignal"]), 9) 

244 

245 def testSignalNotAvailable(self): 

246 ad = self.ad | {"HoldReasonCode": 3, "HoldReason": "foo"} 

247 with self.assertLogs(logger=logger, level="DEBUG") as cm: 

248 result = self.handler.handle(ad) 

249 self.assertIsNone(result) 

250 self.assertIn("signal not found", cm.output[0]) 

251 

252 def testNotHandlingInvalidHoldReasonCode(self): 

253 ad = self.ad | {"HoldReasonCode": 1, "HoldReason": "via condor_hold (by user foo)"} 

254 with self.assertLogs(logger=logger, level="DEBUG") as cm: 

255 result = self.handler.handle(ad) 

256 self.assertIsNone(result) 

257 self.assertIn("not held by a signal", cm.output[0]) 

258 

259 def testNotHandlingJobNotHeld(self): 

260 ad = self.ad | {"MyType": "foo"} 

261 with self.assertLogs(logger=logger, level="DEBUG") as cm: 

262 result = self.handler.handle(ad) 

263 self.assertIsNone(result) 

264 self.assertIn("job not held", cm.output[0]) 

265 

266 

267class JobHeldByUserHandlerTestCase(unittest.TestCase): 

268 """Test the handler for a job held by the user.""" 

269 

270 def setUp(self): 

271 self.ad = {"ClusterId": 1, "ProcId": 0, "MyType": "JobHeldEvent"} 

272 self.handler = JobHeldByUserHandler() 

273 

274 def tearDown(self): 

275 pass 

276 

277 def testHandling(self): 

278 ad = self.ad | {"HoldReasonCode": 1} 

279 result = self.handler.handle(ad) 

280 self.assertIsNotNone(result) 

281 self.assertIn("ExitBySignal", result) 

282 self.assertFalse(result["ExitBySignal"]) 

283 self.assertIn("ExitCode", result) 

284 self.assertEqual(result["ExitCode"], 0) 

285 

286 def testNotHandlingInvalidHoldReaconCode(self): 

287 ad = self.ad | {"HoldReasonCode": 3, "HoldReason": "Job raised a signal 9."} 

288 with self.assertLogs(logger=logger, level="DEBUG") as cm: 

289 result = self.handler.handle(ad) 

290 self.assertIsNone(result) 

291 self.assertIn("not held by the user", cm.output[0]) 

292 

293 def testNotHandlingJobNotHeld(self): 

294 ad = self.ad | {"MyType": "foo"} 

295 with self.assertLogs(logger=logger, level="DEBUG") as cm: 

296 result = self.handler.handle(ad) 

297 self.assertIsNone(result) 

298 self.assertIn("job not held", cm.output[0])