Coverage for tests/test_htcondor_service.py: 100%

160 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-07-09 02:12 -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 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)