Coverage for tests/test_htcondor_service.py: 100%
312 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-10 09:08 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-10 09:08 +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/>.
28"""Unit tests for the HTCondor WMS service class and related functions."""
30import logging
31import os
32import tempfile
33import unittest
34from pathlib import Path
35from unittest.mock import patch
37from htcondor2 import Collector, HTCondorException
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
47logger = logging.getLogger("lsst.ctrl.bps.htcondor")
48TESTDIR = os.path.abspath(os.path.dirname(__file__))
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"""
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"""
74class HTCondorServiceTestCase(unittest.TestCase):
75 """Test selected methods of the HTCondor WMS service class."""
77 def setUp(self):
78 config = BpsConfig({}, wms_service_class_fqn="lsst.ctrl.bps.htcondor.HTCondorService")
79 self.service = htcondor_service.HTCondorService(config)
81 def tearDown(self):
82 pass
84 def testDefaults(self):
85 self.assertEqual(self.service.defaults["memoryLimit"], 491520)
87 def testDefaultsPath(self):
88 self.assertEqual(self.service.defaults_uri, HTC_DEFAULTS_URI)
89 self.assertFalse(self.service.defaults_uri.isdir())
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, "")
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.")
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.")
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, "")
121 fake_id = "100"
122 state, message = self.service.get_status(fake_id)
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={})
128 self.assertEqual(state, WmsStates.RUNNING)
129 self.assertEqual(message, "")
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)
140 fake_id = "100"
141 state, message = self.service.get_status(fake_id, 2)
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={})
147 self.assertEqual(state, WmsStates.RUNNING)
148 self.assertEqual(message, fake_message)
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)
157 fake_id = "/fake/path"
158 state, message = self.service.get_status(fake_id)
160 mock_type.assert_called_once_with(fake_id)
161 mock_status.assert_called_once_with(fake_id)
163 self.assertEqual(state, WmsStates.FAILED)
164 self.assertEqual(message, fake_message)
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
170 fake_id = "100.0"
171 state, message = self.service.get_status(fake_id)
173 mock_type.assert_called_once_with(fake_id)
175 self.assertEqual(state, WmsStates.UNKNOWN)
176 self.assertEqual(message, "Invalid job id")
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 )
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
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 )
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"
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"])
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)
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 """
239 def _fake_htc_create_submit_from_dag(filename, submit_options, wms_config_path):
240 raise RuntimeError("Fake exception from mock")
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}
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)
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)
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 """
267 def _fake_htc_create_submit_from_dag(filename, submit_options, wms_config_path):
268 raise RuntimeError("Fake exception from mock")
270 dag_filename = "should_not_matter.dag"
271 wms_config_path = None
272 submit_options = {"DAGMAN_MAX_JOBS_SUBMITTED": 30}
273 attribs = {}
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)
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)
290class RestartTestCase(unittest.TestCase):
291 """Test HTCondorService.restart using mocked lssthtc functions."""
293 def setUp(self):
294 config = BpsConfig({}, wms_service_class_fqn="lsst.ctrl.bps.htcondor.HTCondorService")
295 self.service = htcondor_service.HTCondorService(config)
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)
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)
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)
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)
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)
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)
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")
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
440 run_id, run_name, message = self.service.restart(tmpdir)
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")
452class RunSubmissionChecksTestCase(unittest.TestCase):
453 """Test HTCondorService.run_submission_checks."""
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)
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())
465 def testBpsMakeCommandTrue(self):
466 service = self._make_service({"bpsMakeCommand": True})
467 self.assertIsNone(service.run_submission_checks())
469 def testMissingPayloadCommand(self):
470 service = self._make_service({"bpsMakeCommand": False})
471 with self.assertRaisesRegex(KeyError, "Missing 'payloadCommand'"):
472 service.run_submission_checks()
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())
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()
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())
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()
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())