Coverage for tests/test_htcondor_service.py: 100%

311 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-09-15 09:04 +0000

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 the HTCondor WMS service class and related functions.""" 

29 

30import logging 

31import os 

32import tempfile 

33import unittest 

34from pathlib import Path 

35 

36import htcondor 

37 

38import lsst.ctrl.bps.htcondor.lssthtc as lssthtc 

39from lsst.ctrl.bps import BpsConfig, WmsStates 

40from lsst.ctrl.bps.htcondor import htcondor_service 

41from lsst.ctrl.bps.htcondor.htcondor_config import HTC_DEFAULTS_URI 

42from lsst.ctrl.bps.htcondor.htcondor_workflow import HTCondorWorkflow 

43from lsst.ctrl.bps.tests.gw_test_utils import make_3_label_workflow 

44from lsst.daf.butler import Config 

45 

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

47TESTDIR = os.path.abspath(os.path.dirname(__file__)) 

48 

49LOCATE_SUCCESS = """[ 

50 CondorPlatform = "$CondorPlatform: X86_64-CentOS_7.9 $"; 

51 MyType = "Scheduler"; 

52 Machine = "testmachine"; 

53 Name = "testmachine"; 

54 CondorVersion = "$CondorVersion: 23.0.3 2024-04-04 $"; 

55 MyAddress = "<127.0.0.1:9618?addrs=127.0.0.1-9618+snip>" 

56 ] 

57""" 

58 

59PING_SUCCESS = """[ 

60 AuthCommand = 60011; 

61 AuthMethods = "FS_REMOTE"; 

62 Command = 60040; 

63 AuthorizationSucceeded = true; 

64 ValidCommands = "60002,60003,60011,60014,60045,60046,60047,60048,60049,60050,60052,523"; 

65 TriedAuthentication = true; 

66 RemoteVersion = "$CondorVersion: 10.9.0 2023-09-28 BuildID: 678228 PackageID: 10.9.0-1 $"; 

67 MyRemoteUserName = "testuser@testmachine"; 

68 Authentication = "YES"; 

69 ] 

70""" 

71 

72 

73class HTCondorServiceTestCase(unittest.TestCase): 

74 """Test selected methods of the HTCondor WMS service class.""" 

75 

76 def setUp(self): 

77 config = BpsConfig({}, wms_service_class_fqn="lsst.ctrl.bps.htcondor.HTCondorService") 

78 self.service = htcondor_service.HTCondorService(config) 

79 

80 def tearDown(self): 

81 pass 

82 

83 def testDefaults(self): 

84 self.assertEqual(self.service.defaults["memoryLimit"], 491520) 

85 

86 def testDefaultsPath(self): 

87 self.assertEqual(self.service.defaults_uri, HTC_DEFAULTS_URI) 

88 self.assertFalse(self.service.defaults_uri.isdir()) 

89 

90 @unittest.mock.patch.object(htcondor.SecMan, "ping", return_value=PING_SUCCESS) 

91 @unittest.mock.patch.object(htcondor.Collector, "locate", return_value=LOCATE_SUCCESS) 

92 def testPingSuccess(self, mock_locate, mock_ping): 

93 status, message = self.service.ping(None) 

94 self.assertEqual(status, 0) 

95 self.assertEqual(message, "") 

96 

97 def testPingFailure(self): 

98 with unittest.mock.patch("htcondor.Collector.locate") as locate_mock: 

99 locate_mock.side_effect = htcondor.HTCondorLocateError() 

100 status, message = self.service.ping(None) 

101 self.assertEqual(status, 1) 

102 self.assertEqual(message, "Could not locate Schedd service.") 

103 

104 @unittest.mock.patch.object(htcondor.Collector, "locate", return_value=LOCATE_SUCCESS) 

105 def testPingPermission(self, mock_locate): 

106 with unittest.mock.patch("htcondor.SecMan.ping") as ping_mock: 

107 ping_mock.side_effect = htcondor.HTCondorIOError() 

108 status, message = self.service.ping(None) 

109 self.assertEqual(status, 1) 

110 self.assertEqual(message, "Permission problem with Schedd service.") 

111 

112 @unittest.mock.patch("lsst.ctrl.bps.htcondor.htcondor_service._get_status_from_id") 

113 @unittest.mock.patch("lsst.ctrl.bps.htcondor.htcondor_service._locate_schedds") 

114 @unittest.mock.patch("lsst.ctrl.bps.htcondor.htcondor_service._wms_id_type") 

115 def testGetStatusLocal(self, mock_type, mock_locate, mock_status): 

116 mock_type.return_value = htcondor_service.WmsIdType.LOCAL 

117 mock_locate.return_value = {} 

118 mock_status.return_value = (WmsStates.RUNNING, "") 

119 

120 fake_id = "100" 

121 state, message = self.service.get_status(fake_id) 

122 

123 mock_type.assert_called_once_with(fake_id) 

124 mock_locate.assert_called_once_with(locate_all=False) 

125 mock_status.assert_called_once_with(fake_id, 1, schedds={}) 

126 

127 self.assertEqual(state, WmsStates.RUNNING) 

128 self.assertEqual(message, "") 

129 

130 @unittest.mock.patch("lsst.ctrl.bps.htcondor.htcondor_service._get_status_from_id") 

131 @unittest.mock.patch("lsst.ctrl.bps.htcondor.htcondor_service._locate_schedds") 

132 @unittest.mock.patch("lsst.ctrl.bps.htcondor.htcondor_service._wms_id_type") 

133 def testGetStatusGlobal(self, mock_type, mock_locate, mock_status): 

134 mock_type.return_value = htcondor_service.WmsIdType.GLOBAL 

135 mock_locate.return_value = {} 

136 fake_message = "" 

137 mock_status.return_value = (WmsStates.RUNNING, fake_message) 

138 

139 fake_id = "100" 

140 state, message = self.service.get_status(fake_id, 2) 

141 

142 mock_type.assert_called_once_with(fake_id) 

143 mock_locate.assert_called_once_with(locate_all=True) 

144 mock_status.assert_called_once_with(fake_id, 2, schedds={}) 

145 

146 self.assertEqual(state, WmsStates.RUNNING) 

147 self.assertEqual(message, fake_message) 

148 

149 @unittest.mock.patch("lsst.ctrl.bps.htcondor.htcondor_service._get_status_from_path") 

150 @unittest.mock.patch("lsst.ctrl.bps.htcondor.htcondor_service._wms_id_type") 

151 def testGetStatusPath(self, mock_type, mock_status): 

152 fake_message = "fake message" 

153 mock_type.return_value = htcondor_service.WmsIdType.PATH 

154 mock_status.return_value = (WmsStates.FAILED, fake_message) 

155 

156 fake_id = "/fake/path" 

157 state, message = self.service.get_status(fake_id) 

158 

159 mock_type.assert_called_once_with(fake_id) 

160 mock_status.assert_called_once_with(fake_id) 

161 

162 self.assertEqual(state, WmsStates.FAILED) 

163 self.assertEqual(message, fake_message) 

164 

165 @unittest.mock.patch("lsst.ctrl.bps.htcondor.htcondor_service._wms_id_type") 

166 def testGetStatusUnknownType(self, mock_type): 

167 mock_type.return_value = htcondor_service.WmsIdType.UNKNOWN 

168 

169 fake_id = "100.0" 

170 state, message = self.service.get_status(fake_id) 

171 

172 mock_type.assert_called_once_with(fake_id) 

173 

174 self.assertEqual(state, WmsStates.UNKNOWN) 

175 self.assertEqual(message, "Invalid job id") 

176 

177 @unittest.mock.patch("lsst.ctrl.bps.htcondor.htcondor_workflow.HTCondorWorkflow.write") 

178 def testPrepare(self, mock_write): 

179 generic_workflow = make_3_label_workflow("test1", True) 

180 config = BpsConfig( 

181 { 

182 "bpsUseShared": True, 

183 "overwriteJobFiles": False, 

184 "memoryLimit": 491520, 

185 "profile": {}, 

186 "attrs": {}, 

187 "nodeset": "set1", 

188 } 

189 ) 

190 

191 with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir: 

192 htc_workflow = self.service.prepare(config, generic_workflow, tmpdir) 

193 mock_write.assert_called_once() 

194 self.assertEqual(len(htc_workflow.dag), 19) # 3 visit * 2 detectors * 3 labels + init 

195 

196 @unittest.mock.patch("lsst.ctrl.bps.htcondor.htcondor_workflow.HTCondorWorkflow.write") 

197 def testPrepareProvision(self, mock_write): 

198 # Leaves testing provisioning code to test_provisioner.py. 

199 # Just checking HTCondorService.prepare bits (like nodeset). 

200 timestamp = "20260130T211713Z" 

201 generic_workflow = make_3_label_workflow("test1", True) 

202 config = BpsConfig( 

203 { 

204 "bpsUseShared": True, 

205 "overwriteJobFiles": False, 

206 "profile": {"requirements": "dummy_val == 3"}, 

207 "attrs": {}, 

208 "nodeset": "set1", # this shouldn't be used with auto-provisioning 

209 "provisionResources": True, 

210 "provisioning": {"provisioningMaxWallTime": 1200}, 

211 "bps_defined": {"timestamp": timestamp}, 

212 }, 

213 defaults=Config(HTC_DEFAULTS_URI), 

214 ) 

215 

216 with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir: 

217 prov_config = Path(f"{tmpdir}/condor-info.py") 

218 config[".provisioning.provisioningScriptConfigPath"] = str(prov_config) 

219 config[".provisioning.provisioningScriptConfig"] = "foo" 

220 

221 htc_workflow = self.service.prepare(config, generic_workflow, tmpdir) 

222 mock_write.assert_called_once() 

223 self.assertEqual(config[".bps_defined.nodeset"], timestamp) 

224 self.assertEqual(len(htc_workflow.dag), 19) # 3 visit * 2 dets * 3 labels + init 

225 self.assertIsNotNone(htc_workflow.dag.graph["service_job"]) 

226 

227 prov_script = Path(tmpdir) / "provisioningJob.bash" 

228 self.assertTrue(prov_script.is_file()) 

229 script_contents = prov_script.read_text() 

230 self.assertIn(f"--nodeset '{timestamp}'", script_contents) 

231 

232 def testSubmitWithConfigPath(self): 

233 """Only testing value for wms_config_path being passed 

234 correctly to htc_create_submit_from_dag. Aborting submission 

235 after that call to skip rest of submit function. 

236 """ 

237 

238 def _fake_htc_create_submit_from_dag(filename, submit_options, wms_config_path): 

239 raise RuntimeError("Fake exception from mock") 

240 

241 dag_filename = "should_not_matter.dag" 

242 wms_config_path = "dagman.conf" 

243 submit_options = {"DAGMAN_MAX_JOBS_SUBMITTED": 30} 

244 attribs = {"bps_wms_config_path": wms_config_path} 

245 

246 workflow = HTCondorWorkflow("testSuccess") 

247 workflow.dag = lssthtc.HTCDag("testSuccess") 

248 workflow.dag.graph["dag_filename"] = dag_filename 

249 workflow.dag.graph["attr"] = dict(attribs) 

250 workflow.dag.graph["submit_options"] = dict(submit_options) 

251 

252 with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir: 

253 workflow.submit_path = tmpdir 

254 with unittest.mock.patch( 

255 "lsst.ctrl.bps.htcondor.htcondor_service.htc_create_submit_from_dag" 

256 ) as create_mock: 

257 create_mock.side_effect = _fake_htc_create_submit_from_dag 

258 with self.assertRaisesRegex(RuntimeError, "Fake exception from mock"): 

259 self.service.submit(workflow) 

260 create_mock.assert_called_once_with(dag_filename, submit_options, wms_config_path) 

261 

262 def testSubmitWithoutConfigPath(self): 

263 """Only testing that values are being passed correctly to 

264 htc_create_submit_from_dag when there isn't a wms config path. 

265 Aborting submission after that call to skip rest of submit function. 

266 """ 

267 

268 def _fake_htc_create_submit_from_dag(filename, submit_options, wms_config_path): 

269 raise RuntimeError("Fake exception from mock") 

270 

271 dag_filename = "should_not_matter.dag" 

272 wms_config_path = None 

273 submit_options = {"DAGMAN_MAX_JOBS_SUBMITTED": 30} 

274 attribs = {} 

275 

276 workflow = HTCondorWorkflow("testSuccess") 

277 workflow.dag = lssthtc.HTCDag("testSuccess") 

278 workflow.dag.graph["dag_filename"] = dag_filename 

279 workflow.dag.graph["attr"] = dict(attribs) 

280 workflow.dag.graph["submit_options"] = dict(submit_options) 

281 

282 with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir: 

283 workflow.submit_path = tmpdir 

284 with unittest.mock.patch( 

285 "lsst.ctrl.bps.htcondor.htcondor_service.htc_create_submit_from_dag" 

286 ) as create_mock: 

287 create_mock.side_effect = _fake_htc_create_submit_from_dag 

288 with self.assertRaisesRegex(RuntimeError, "Fake exception from mock"): 

289 self.service.submit(workflow) 

290 create_mock.assert_called_once_with(dag_filename, submit_options, wms_config_path) 

291 

292 

293class RestartTestCase(unittest.TestCase): 

294 """Test HTCondorService.restart using mocked lssthtc functions.""" 

295 

296 def setUp(self): 

297 config = BpsConfig({}, wms_service_class_fqn="lsst.ctrl.bps.htcondor.HTCondorService") 

298 self.service = htcondor_service.HTCondorService(config) 

299 

300 @unittest.mock.patch("lsst.ctrl.bps.htcondor.htcondor_service._wms_id_to_dir") 

301 def testIdNotFound(self, mock_to_dir): 

302 mock_to_dir.return_value = (None, htcondor_service.WmsIdType.UNKNOWN) 

303 run_id, run_name, message = self.service.restart("bad_id") 

304 self.assertIsNone(run_id) 

305 self.assertIsNone(run_name) 

306 self.assertIn("not found", message) 

307 self.assertIn("submit directory", message) 

308 

309 @unittest.mock.patch("lsst.ctrl.bps.htcondor.htcondor_service._wms_id_to_dir") 

310 def testSubmitDirNotFound(self, mock_to_dir): 

311 mock_to_dir.return_value = (Path("/does/not/exist"), htcondor_service.WmsIdType.LOCAL) 

312 run_id, run_name, message = self.service.restart("100.0") 

313 self.assertIsNone(run_id) 

314 self.assertIsNone(run_name) 

315 self.assertIn("submit directory", message) 

316 self.assertIn("not found", message) 

317 

318 @unittest.mock.patch("lsst.ctrl.bps.htcondor.htcondor_service._wms_id_to_dir") 

319 def testNoRescueDag(self, mock_to_dir): 

320 with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir: 

321 mock_to_dir.return_value = (Path(tmpdir), htcondor_service.WmsIdType.PATH) 

322 run_id, run_name, message = self.service.restart(tmpdir) 

323 self.assertIsNone(run_id) 

324 self.assertIsNone(run_name) 

325 self.assertIn("rescue DAG", message) 

326 

327 @unittest.mock.patch("lsst.ctrl.bps.htcondor.htcondor_service.condor_q") 

328 @unittest.mock.patch("lsst.ctrl.bps.htcondor.htcondor_service._wms_id_to_dir") 

329 def testAlreadyInQueue(self, mock_to_dir, mock_condor_q): 

330 with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir: 

331 (Path(tmpdir) / "test.dag.rescue001").touch() 

332 mock_to_dir.return_value = (Path(tmpdir), htcondor_service.WmsIdType.PATH) 

333 mock_condor_q.return_value = {"schedd": {"1.0": {"GlobalJobId": "schedd#1.0#123"}}} 

334 run_id, run_name, message = self.service.restart(tmpdir) 

335 self.assertIsNone(run_id) 

336 self.assertIsNone(run_name) 

337 self.assertIn("already in the job queue", message) 

338 self.assertIn("schedd#1.0#123", message) 

339 

340 @unittest.mock.patch("lsst.ctrl.bps.htcondor.htcondor_service.read_dag_status") 

341 @unittest.mock.patch("lsst.ctrl.bps.htcondor.htcondor_service.condor_q") 

342 @unittest.mock.patch("lsst.ctrl.bps.htcondor.htcondor_service._wms_id_to_dir") 

343 def testAllJobsFinished(self, mock_to_dir, mock_condor_q, mock_status): 

344 with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir: 

345 (Path(tmpdir) / "test.dag.rescue001").touch() 

346 mock_to_dir.return_value = (Path(tmpdir), htcondor_service.WmsIdType.PATH) 

347 mock_condor_q.return_value = {} 

348 mock_status.return_value = {"NodesTotal": 5, "NodesDone": 5} 

349 run_id, run_name, message = self.service.restart(tmpdir) 

350 self.assertIsNone(run_id) 

351 self.assertIsNone(run_name) 

352 self.assertIn("finished successfully", message) 

353 

354 @unittest.mock.patch("lsst.ctrl.bps.htcondor.htcondor_service.htc_backup_files") 

355 @unittest.mock.patch("lsst.ctrl.bps.htcondor.htcondor_service.read_dag_info") 

356 @unittest.mock.patch("lsst.ctrl.bps.htcondor.htcondor_service.read_dag_status") 

357 @unittest.mock.patch("lsst.ctrl.bps.htcondor.htcondor_service.condor_q") 

358 @unittest.mock.patch("lsst.ctrl.bps.htcondor.htcondor_service._wms_id_to_dir") 

359 def testNoCondorSub(self, mock_to_dir, mock_condor_q, mock_status, mock_read_info, mock_backup): 

360 with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir: 

361 (Path(tmpdir) / "test.dag.rescue001").touch() 

362 mock_to_dir.return_value = (Path(tmpdir), htcondor_service.WmsIdType.PATH) 

363 mock_condor_q.return_value = {} 

364 mock_status.return_value = {"NodesTotal": 5, "NodesDone": 3} 

365 mock_read_info.return_value = ( 

366 "info.json", 

367 {"schedd": {"1.0": {"bps_job_summary": "sum", "bps_run_quanta": "quanta"}}}, 

368 ) 

369 mock_backup.return_value = Path(tmpdir) / "test.dag.rescue001" 

370 run_id, run_name, message = self.service.restart(tmpdir) 

371 self.assertIsNone(run_id) 

372 self.assertIsNone(run_name) 

373 self.assertIn("submit description file not found", message) 

374 

375 @unittest.mock.patch("lsst.ctrl.bps.htcondor.htcondor_service.htc_submit_dag") 

376 @unittest.mock.patch("lsst.ctrl.bps.htcondor.htcondor_service.htc_create_submit_from_file") 

377 @unittest.mock.patch("lsst.ctrl.bps.htcondor.htcondor_service.htc_backup_files") 

378 @unittest.mock.patch("lsst.ctrl.bps.htcondor.htcondor_service.read_dag_info") 

379 @unittest.mock.patch("lsst.ctrl.bps.htcondor.htcondor_service.read_dag_status") 

380 @unittest.mock.patch("lsst.ctrl.bps.htcondor.htcondor_service.condor_q") 

381 @unittest.mock.patch("lsst.ctrl.bps.htcondor.htcondor_service._wms_id_to_dir") 

382 def testSubmitInfoUnavailable( 

383 self, 

384 mock_to_dir, 

385 mock_condor_q, 

386 mock_status, 

387 mock_read_info, 

388 mock_backup, 

389 mock_create, 

390 mock_submit, 

391 ): 

392 with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir: 

393 (Path(tmpdir) / "test.dag.rescue001").touch() 

394 (Path(tmpdir) / "test.dag.condor.sub").touch() 

395 mock_to_dir.return_value = (Path(tmpdir), htcondor_service.WmsIdType.PATH) 

396 mock_condor_q.return_value = {} 

397 mock_status.return_value = {"NodesTotal": 5, "NodesDone": 3} 

398 mock_read_info.return_value = ( 

399 "info.json", 

400 {"schedd": {"1.0": {"bps_job_summary": "sum", "bps_run_quanta": "quanta"}}}, 

401 ) 

402 mock_backup.return_value = Path(tmpdir) / "test.dag.rescue001" 

403 mock_submit.return_value = {} 

404 run_id, run_name, message = self.service.restart(tmpdir) 

405 self.assertIsNone(run_id) 

406 self.assertIsNone(run_name) 

407 self.assertEqual(message, "DAGMan job information unavailable") 

408 

409 @unittest.mock.patch("lsst.ctrl.bps.htcondor.htcondor_service.write_dag_info") 

410 @unittest.mock.patch("lsst.ctrl.bps.htcondor.htcondor_service.htc_submit_dag") 

411 @unittest.mock.patch("lsst.ctrl.bps.htcondor.htcondor_service.htc_create_submit_from_file") 

412 @unittest.mock.patch("lsst.ctrl.bps.htcondor.htcondor_service.htc_backup_files") 

413 @unittest.mock.patch("lsst.ctrl.bps.htcondor.htcondor_service.read_dag_info") 

414 @unittest.mock.patch("lsst.ctrl.bps.htcondor.htcondor_service.read_dag_status") 

415 @unittest.mock.patch("lsst.ctrl.bps.htcondor.htcondor_service.condor_q") 

416 @unittest.mock.patch("lsst.ctrl.bps.htcondor.htcondor_service._wms_id_to_dir") 

417 def testSuccess( 

418 self, 

419 mock_to_dir, 

420 mock_condor_q, 

421 mock_status, 

422 mock_read_info, 

423 mock_backup, 

424 mock_create, 

425 mock_submit, 

426 mock_write_info, 

427 ): 

428 with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir: 

429 (Path(tmpdir) / "test.dag.rescue001").touch() 

430 (Path(tmpdir) / "test.dag.condor.sub").touch() 

431 mock_to_dir.return_value = (Path(tmpdir), htcondor_service.WmsIdType.PATH) 

432 mock_condor_q.return_value = {} 

433 mock_status.return_value = {"NodesTotal": 5, "NodesDone": 3} 

434 info_filename = "info.json" 

435 mock_read_info.return_value = ( 

436 info_filename, 

437 {"schedd": {"1.0": {"bps_job_summary": "sum", "bps_run_quanta": "quanta"}}}, 

438 ) 

439 mock_backup.return_value = Path(tmpdir) / "test.dag.rescue001" 

440 schedd_dag_info = {"schedd": {"2.0": {"ClusterId": 2, "ProcId": 0, "bps_run": "myrun"}}} 

441 mock_submit.return_value = schedd_dag_info 

442 

443 run_id, run_name, message = self.service.restart(tmpdir) 

444 

445 self.assertEqual(run_id, "2.0") 

446 self.assertEqual(run_name, "myrun") 

447 self.assertEqual(message, "") 

448 mock_write_info.assert_called_once_with(info_filename, schedd_dag_info) 

449 # Summaries from the previous run should be carried forward. 

450 dag_ad = schedd_dag_info["schedd"]["2.0"] 

451 self.assertEqual(dag_ad["bps_job_summary"], "sum") 

452 self.assertEqual(dag_ad["bps_run_quanta"], "quanta") 

453 

454 

455class RunSubmissionChecksTestCase(unittest.TestCase): 

456 """Test HTCondorService.run_submission_checks.""" 

457 

458 @staticmethod 

459 def _make_service(config_dict): 

460 config = BpsConfig(config_dict, wms_service_class_fqn="lsst.ctrl.bps.htcondor.HTCondorService") 

461 return htcondor_service.HTCondorService(config) 

462 

463 def testBpsMakeCommandMissing(self): 

464 # bpsMakeCommand absent defaults to True, so no checks are performed. 

465 service = self._make_service({}) 

466 self.assertIsNone(service.run_submission_checks()) 

467 

468 def testBpsMakeCommandTrue(self): 

469 service = self._make_service({"bpsMakeCommand": True}) 

470 self.assertIsNone(service.run_submission_checks()) 

471 

472 def testMissingPayloadCommand(self): 

473 service = self._make_service({"bpsMakeCommand": False}) 

474 with self.assertRaisesRegex(KeyError, "Missing 'payloadCommand'"): 

475 service.run_submission_checks() 

476 

477 def testPayloadCommandWithoutSetupEnv(self): 

478 # payloadCommand present but does not reference setupEnv, so the 

479 # remaining checks are skipped. 

480 service = self._make_service({"bpsMakeCommand": False, "payloadCommand": "run_thing --flag"}) 

481 self.assertIsNone(service.run_submission_checks()) 

482 

483 def testMissingSetupEnv(self): 

484 service = self._make_service({"bpsMakeCommand": False, "payloadCommand": "run_thing {setupEnv}"}) 

485 with self.assertRaisesRegex(KeyError, "Missing 'setupEnv'"): 

486 service.run_submission_checks() 

487 

488 def testSetupEnvWithoutLsstVersion(self): 

489 service = self._make_service( 

490 { 

491 "bpsMakeCommand": False, 

492 "payloadCommand": "run_thing {setupEnv}", 

493 "setupEnv": "source /opt/lsst/setup.sh", 

494 } 

495 ) 

496 self.assertIsNone(service.run_submission_checks()) 

497 

498 def testMissingLsstVersion(self): 

499 service = self._make_service( 

500 { 

501 "bpsMakeCommand": False, 

502 "payloadCommand": "run_thing {setupEnv}", 

503 "setupEnv": "setup lsst_distrib -t {lsstVersion}", 

504 } 

505 ) 

506 with self.assertRaisesRegex(KeyError, "Missing 'lsstVersion'"): 

507 service.run_submission_checks() 

508 

509 def testAllPresent(self): 

510 service = self._make_service( 

511 { 

512 "bpsMakeCommand": False, 

513 "payloadCommand": "run_thing {setupEnv}", 

514 "setupEnv": "setup lsst_distrib -t {lsstVersion}", 

515 "lsstVersion": "w_2026_01", 

516 } 

517 ) 

518 self.assertIsNone(service.run_submission_checks())