Coverage for tests/test_htcondor_service.py: 100%

312 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-09 10:31 +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 

35from unittest.mock import patch 

36 

37from htcondor2 import Collector, HTCondorException 

38 

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

40from lsst.ctrl.bps import BpsConfig, WmsStates 

41from lsst.ctrl.bps.htcondor import htcondor_service 

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

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

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

45from lsst.daf.butler import Config 

46 

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

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

49 

50LOCATE_SUCCESS = """[ 

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

52 MyType = "Scheduler"; 

53 Machine = "testmachine"; 

54 Name = "testmachine"; 

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

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

57 ] 

58""" 

59 

60PING_SUCCESS = """[ 

61 AuthCommand = 60011; 

62 AuthMethods = "FS_REMOTE"; 

63 Command = 60040; 

64 AuthorizationSucceeded = true; 

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

66 TriedAuthentication = true; 

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

68 MyRemoteUserName = "testuser@testmachine"; 

69 Authentication = "YES"; 

70 ] 

71""" 

72 

73 

74class HTCondorServiceTestCase(unittest.TestCase): 

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

76 

77 def setUp(self): 

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

79 self.service = htcondor_service.HTCondorService(config) 

80 

81 def tearDown(self): 

82 pass 

83 

84 def testDefaults(self): 

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

86 

87 def testDefaultsPath(self): 

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

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

90 

91 @patch("lsst.ctrl.bps.htcondor.htcondor_service.ping", return_value=PING_SUCCESS) 

92 @patch.object(Collector, "locate", return_value=LOCATE_SUCCESS) 

93 def testPingSuccess(self, mock_locate, mock_ping): 

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

95 self.assertEqual(status, 0) 

96 self.assertEqual(message, "") 

97 

98 def testPingFailure(self): 

99 with patch("lsst.ctrl.bps.htcondor.htcondor_service.Collector.locate") as locate_mock: 

100 locate_mock.side_effect = HTCondorException("Unable to locate local daemon.") 

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

102 self.assertEqual(status, 1) 

103 self.assertIn(message, "Could not locate Schedd service.") 

104 

105 @patch.object(Collector, "locate", return_value=LOCATE_SUCCESS) 

106 def testPingPermission(self, mock_locate): 

107 with patch("lsst.ctrl.bps.htcondor.htcondor_service.ping") as ping_mock: 

108 ping_mock.side_effect = HTCondorException("Failed to connect to schedd.") 

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

110 self.assertEqual(status, 1) 

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

112 

113 @patch("lsst.ctrl.bps.htcondor.htcondor_service._get_status_from_id") 

114 @patch("lsst.ctrl.bps.htcondor.htcondor_service._locate_schedds") 

115 @patch("lsst.ctrl.bps.htcondor.htcondor_service._wms_id_type") 

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

117 mock_type.return_value = htcondor_service.WmsIdType.LOCAL 

118 mock_locate.return_value = {} 

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

120 

121 fake_id = "100" 

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

123 

124 mock_type.assert_called_once_with(fake_id) 

125 mock_locate.assert_called_once_with(locate_all=False) 

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

127 

128 self.assertEqual(state, WmsStates.RUNNING) 

129 self.assertEqual(message, "") 

130 

131 @patch("lsst.ctrl.bps.htcondor.htcondor_service._get_status_from_id") 

132 @patch("lsst.ctrl.bps.htcondor.htcondor_service._locate_schedds") 

133 @patch("lsst.ctrl.bps.htcondor.htcondor_service._wms_id_type") 

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

135 mock_type.return_value = htcondor_service.WmsIdType.GLOBAL 

136 mock_locate.return_value = {} 

137 fake_message = "" 

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

139 

140 fake_id = "100" 

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

142 

143 mock_type.assert_called_once_with(fake_id) 

144 mock_locate.assert_called_once_with(locate_all=True) 

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

146 

147 self.assertEqual(state, WmsStates.RUNNING) 

148 self.assertEqual(message, fake_message) 

149 

150 @patch("lsst.ctrl.bps.htcondor.htcondor_service._get_status_from_path") 

151 @patch("lsst.ctrl.bps.htcondor.htcondor_service._wms_id_type") 

152 def testGetStatusPath(self, mock_type, mock_status): 

153 fake_message = "fake message" 

154 mock_type.return_value = htcondor_service.WmsIdType.PATH 

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

156 

157 fake_id = "/fake/path" 

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

159 

160 mock_type.assert_called_once_with(fake_id) 

161 mock_status.assert_called_once_with(fake_id) 

162 

163 self.assertEqual(state, WmsStates.FAILED) 

164 self.assertEqual(message, fake_message) 

165 

166 @patch("lsst.ctrl.bps.htcondor.htcondor_service._wms_id_type") 

167 def testGetStatusUnknownType(self, mock_type): 

168 mock_type.return_value = htcondor_service.WmsIdType.UNKNOWN 

169 

170 fake_id = "100.0" 

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

172 

173 mock_type.assert_called_once_with(fake_id) 

174 

175 self.assertEqual(state, WmsStates.UNKNOWN) 

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

177 

178 @patch("lsst.ctrl.bps.htcondor.htcondor_workflow.HTCondorWorkflow.write") 

179 def testPrepare(self, mock_write): 

180 generic_workflow = make_3_label_workflow("test1", True) 

181 config = BpsConfig( 

182 { 

183 "bpsUseShared": True, 

184 "overwriteJobFiles": False, 

185 "memoryLimit": 491520, 

186 "profile": {}, 

187 "attrs": {}, 

188 "nodeset": "set1", 

189 } 

190 ) 

191 

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

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

194 mock_write.assert_called_once() 

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

196 

197 @patch("lsst.ctrl.bps.htcondor.htcondor_workflow.HTCondorWorkflow.write") 

198 def testPrepareProvision(self, mock_write): 

199 # Leaves testing provisioning code to test_provisioner.py. 

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

201 timestamp = "20260130T211713Z" 

202 generic_workflow = make_3_label_workflow("test1", True) 

203 config = BpsConfig( 

204 { 

205 "bpsUseShared": True, 

206 "overwriteJobFiles": False, 

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

208 "attrs": {}, 

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

210 "provisionResources": True, 

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

212 "bps_defined": {"timestamp": timestamp}, 

213 }, 

214 defaults=Config(HTC_DEFAULTS_URI), 

215 ) 

216 

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

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

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

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

221 

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

223 mock_write.assert_called_once() 

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

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

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

227 

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

229 self.assertTrue(prov_script.is_file()) 

230 script_contents = prov_script.read_text() 

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

232 

233 def testSubmitWithConfigPath(self): 

234 """Only testing value for wms_config_path being passed 

235 correctly to htc_create_submit_from_dag. Aborting submission 

236 after that call to skip rest of submit function. 

237 """ 

238 

239 def _fake_htc_create_submit_from_dag(filename, submit_options, wms_config_path): 

240 raise RuntimeError("Fake exception from mock") 

241 

242 dag_filename = "should_not_matter.dag" 

243 wms_config_path = "dagman.conf" 

244 submit_options = {"DAGMAN_MAX_JOBS_SUBMITTED": 30} 

245 attribs = {"bps_wms_config_path": wms_config_path} 

246 

247 workflow = HTCondorWorkflow("testSuccess") 

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

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

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

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

252 

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

254 workflow.submit_path = tmpdir 

255 with patch("lsst.ctrl.bps.htcondor.htcondor_service.htc_create_submit_from_dag") as create_mock: 

256 create_mock.side_effect = _fake_htc_create_submit_from_dag 

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

258 self.service.submit(workflow) 

259 create_mock.assert_called_once_with(dag_filename, submit_options, wms_config_path) 

260 

261 def testSubmitWithoutConfigPath(self): 

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

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

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

265 """ 

266 

267 def _fake_htc_create_submit_from_dag(filename, submit_options, wms_config_path): 

268 raise RuntimeError("Fake exception from mock") 

269 

270 dag_filename = "should_not_matter.dag" 

271 wms_config_path = None 

272 submit_options = {"DAGMAN_MAX_JOBS_SUBMITTED": 30} 

273 attribs = {} 

274 

275 workflow = HTCondorWorkflow("testSuccess") 

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

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

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

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

280 

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

282 workflow.submit_path = tmpdir 

283 with patch("lsst.ctrl.bps.htcondor.htcondor_service.htc_create_submit_from_dag") as create_mock: 

284 create_mock.side_effect = _fake_htc_create_submit_from_dag 

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

286 self.service.submit(workflow) 

287 create_mock.assert_called_once_with(dag_filename, submit_options, wms_config_path) 

288 

289 

290class RestartTestCase(unittest.TestCase): 

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

292 

293 def setUp(self): 

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

295 self.service = htcondor_service.HTCondorService(config) 

296 

297 @patch("lsst.ctrl.bps.htcondor.htcondor_service._wms_id_to_dir") 

298 def testIdNotFound(self, mock_to_dir): 

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

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

301 self.assertIsNone(run_id) 

302 self.assertIsNone(run_name) 

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

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

305 

306 @patch("lsst.ctrl.bps.htcondor.htcondor_service._wms_id_to_dir") 

307 def testSubmitDirNotFound(self, mock_to_dir): 

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

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

310 self.assertIsNone(run_id) 

311 self.assertIsNone(run_name) 

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

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

314 

315 @patch("lsst.ctrl.bps.htcondor.htcondor_service._wms_id_to_dir") 

316 def testNoRescueDag(self, mock_to_dir): 

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

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

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

320 self.assertIsNone(run_id) 

321 self.assertIsNone(run_name) 

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

323 

324 @patch("lsst.ctrl.bps.htcondor.htcondor_service.condor_q") 

325 @patch("lsst.ctrl.bps.htcondor.htcondor_service._wms_id_to_dir") 

326 def testAlreadyInQueue(self, mock_to_dir, mock_condor_q): 

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

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

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

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

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

332 self.assertIsNone(run_id) 

333 self.assertIsNone(run_name) 

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

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

336 

337 @patch("lsst.ctrl.bps.htcondor.htcondor_service.read_dag_status") 

338 @patch("lsst.ctrl.bps.htcondor.htcondor_service.condor_q") 

339 @patch("lsst.ctrl.bps.htcondor.htcondor_service._wms_id_to_dir") 

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

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

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

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

344 mock_condor_q.return_value = {} 

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

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

347 self.assertIsNone(run_id) 

348 self.assertIsNone(run_name) 

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

350 

351 @patch("lsst.ctrl.bps.htcondor.htcondor_service.htc_backup_files") 

352 @patch("lsst.ctrl.bps.htcondor.htcondor_service.read_dag_info") 

353 @patch("lsst.ctrl.bps.htcondor.htcondor_service.read_dag_status") 

354 @patch("lsst.ctrl.bps.htcondor.htcondor_service.condor_q") 

355 @patch("lsst.ctrl.bps.htcondor.htcondor_service._wms_id_to_dir") 

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

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

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

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

360 mock_condor_q.return_value = {} 

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

362 mock_read_info.return_value = ( 

363 "info.json", 

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

365 ) 

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

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

368 self.assertIsNone(run_id) 

369 self.assertIsNone(run_name) 

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

371 

372 @patch("lsst.ctrl.bps.htcondor.htcondor_service.htc_submit_dag") 

373 @patch("lsst.ctrl.bps.htcondor.htcondor_service.htc_create_submit_from_file") 

374 @patch("lsst.ctrl.bps.htcondor.htcondor_service.htc_backup_files") 

375 @patch("lsst.ctrl.bps.htcondor.htcondor_service.read_dag_info") 

376 @patch("lsst.ctrl.bps.htcondor.htcondor_service.read_dag_status") 

377 @patch("lsst.ctrl.bps.htcondor.htcondor_service.condor_q") 

378 @patch("lsst.ctrl.bps.htcondor.htcondor_service._wms_id_to_dir") 

379 def testSubmitInfoUnavailable( 

380 self, 

381 mock_to_dir, 

382 mock_condor_q, 

383 mock_status, 

384 mock_read_info, 

385 mock_backup, 

386 mock_create, 

387 mock_submit, 

388 ): 

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

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

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

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

393 mock_condor_q.return_value = {} 

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

395 mock_read_info.return_value = ( 

396 "info.json", 

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

398 ) 

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

400 mock_submit.return_value = {} 

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

402 self.assertIsNone(run_id) 

403 self.assertIsNone(run_name) 

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

405 

406 @patch("lsst.ctrl.bps.htcondor.htcondor_service.write_dag_info") 

407 @patch("lsst.ctrl.bps.htcondor.htcondor_service.htc_submit_dag") 

408 @patch("lsst.ctrl.bps.htcondor.htcondor_service.htc_create_submit_from_file") 

409 @patch("lsst.ctrl.bps.htcondor.htcondor_service.htc_backup_files") 

410 @patch("lsst.ctrl.bps.htcondor.htcondor_service.read_dag_info") 

411 @patch("lsst.ctrl.bps.htcondor.htcondor_service.read_dag_status") 

412 @patch("lsst.ctrl.bps.htcondor.htcondor_service.condor_q") 

413 @patch("lsst.ctrl.bps.htcondor.htcondor_service._wms_id_to_dir") 

414 def testSuccess( 

415 self, 

416 mock_to_dir, 

417 mock_condor_q, 

418 mock_status, 

419 mock_read_info, 

420 mock_backup, 

421 mock_create, 

422 mock_submit, 

423 mock_write_info, 

424 ): 

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

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

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

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

429 mock_condor_q.return_value = {} 

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

431 info_filename = "info.json" 

432 mock_read_info.return_value = ( 

433 info_filename, 

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

435 ) 

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

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

438 mock_submit.return_value = schedd_dag_info 

439 

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

441 

442 self.assertEqual(run_id, "2.0") 

443 self.assertEqual(run_name, "myrun") 

444 self.assertEqual(message, "") 

445 mock_write_info.assert_called_once_with(info_filename, schedd_dag_info) 

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

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

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

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

450 

451 

452class RunSubmissionChecksTestCase(unittest.TestCase): 

453 """Test HTCondorService.run_submission_checks.""" 

454 

455 @staticmethod 

456 def _make_service(config_dict): 

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

458 return htcondor_service.HTCondorService(config) 

459 

460 def testBpsMakeCommandMissing(self): 

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

462 service = self._make_service({}) 

463 self.assertIsNone(service.run_submission_checks()) 

464 

465 def testBpsMakeCommandTrue(self): 

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

467 self.assertIsNone(service.run_submission_checks()) 

468 

469 def testMissingPayloadCommand(self): 

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

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

472 service.run_submission_checks() 

473 

474 def testPayloadCommandWithoutSetupEnv(self): 

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

476 # remaining checks are skipped. 

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

478 self.assertIsNone(service.run_submission_checks()) 

479 

480 def testMissingSetupEnv(self): 

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

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

483 service.run_submission_checks() 

484 

485 def testSetupEnvWithoutLsstVersion(self): 

486 service = self._make_service( 

487 { 

488 "bpsMakeCommand": False, 

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

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

491 } 

492 ) 

493 self.assertIsNone(service.run_submission_checks()) 

494 

495 def testMissingLsstVersion(self): 

496 service = self._make_service( 

497 { 

498 "bpsMakeCommand": False, 

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

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

501 } 

502 ) 

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

504 service.run_submission_checks() 

505 

506 def testAllPresent(self): 

507 service = self._make_service( 

508 { 

509 "bpsMakeCommand": False, 

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

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

512 "lsstVersion": "w_2026_01", 

513 } 

514 ) 

515 self.assertIsNone(service.run_submission_checks())