Coverage for tests / test_bpsconfig.py: 21%
267 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-22 09:00 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-22 09:00 +0000
1# This file is part of ctrl_bps.
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/>.
27import os
28import unittest
30import yaml
32from lsst.ctrl.bps import BPS_SEARCH_ORDER, BpsConfig
33from lsst.daf.butler import Config
35TESTDIR = os.path.abspath(os.path.dirname(__file__))
38class TestBpsConfigConstructor(unittest.TestCase):
39 """Test BpsConfig construction."""
41 def setUp(self):
42 self.filename = os.path.join(TESTDIR, "data/config.yaml")
43 with open(self.filename) as f:
44 self.dictionary = yaml.safe_load(f)
46 def tearDown(self):
47 pass
49 def testFromFilename(self):
50 """Test initialization from a file, but without defaults."""
51 config = BpsConfig(self.filename)
52 self.assertEqual(set(config), {"foo", "bar", "baz"} | set(BPS_SEARCH_ORDER))
53 self.assertEqual(set(config["foo"]), {"qux"})
54 self.assertEqual(set(config["bar"]), {"qux"})
55 self.assertEqual(set(config["baz"]), {"grault", "garply"})
56 self.assertEqual(config.search_order, BPS_SEARCH_ORDER)
58 def testFromDict(self):
59 """Test initialization from a dictionary."""
60 config = BpsConfig(self.dictionary)
61 self.assertEqual(set(config), {"foo", "bar", "baz"} | set(BPS_SEARCH_ORDER))
62 self.assertEqual(set(config["foo"]), {"qux"})
63 self.assertEqual(set(config["bar"]), {"qux"})
64 self.assertEqual(set(config["baz"]), {"grault", "garply"})
65 self.assertEqual(config.search_order, BPS_SEARCH_ORDER)
67 def testFromConfig(self):
68 """Test initialization from other Config object."""
69 config_daf = Config(self.dictionary)
70 config = BpsConfig(config_daf)
71 self.assertEqual(set(config), {"foo", "bar", "baz"} | set(BPS_SEARCH_ORDER))
72 self.assertEqual(set(config["foo"]), {"qux"})
73 self.assertEqual(set(config["bar"]), {"qux"})
74 self.assertEqual(set(config["baz"]), {"grault", "garply"})
75 self.assertEqual(config.search_order, BPS_SEARCH_ORDER)
77 def testFromBpsConfig(self):
78 """Test initialization from other BpsConfig object."""
79 config_ref = BpsConfig(self.dictionary)
80 config = BpsConfig(config_ref)
81 self.assertEqual(set(config), set(config_ref))
82 self.assertEqual(config.search_order, config_ref.search_order)
84 def testDefaultsInclusion(self):
85 """Test including defaults."""
86 config = BpsConfig(self.filename, defaults={"quux": 1})
87 self.assertEqual(set(config), {"foo", "bar", "baz", "quux"} | set(BPS_SEARCH_ORDER))
88 self.assertEqual(config["quux"], 1)
89 self.assertEqual(config.search_order, BPS_SEARCH_ORDER)
91 def testDefaultsOverride(self):
92 """Test overriding defaults."""
93 config = BpsConfig(self.filename, defaults={"foo": {"qux": 2}, "quux": 1})
94 self.assertEqual(set(config), {"foo", "bar", "baz", "quux"} | set(BPS_SEARCH_ORDER))
95 self.assertEqual(config["quux"], 1)
96 self.assertEqual(config["foo"], BpsConfig({"qux": 1}, search_order=[]))
97 self.assertEqual(config.search_order, BPS_SEARCH_ORDER)
99 def testWmsFromCmdline(self):
100 """Test initialization when WMS class specified at the cmdline."""
101 with unittest.mock.patch.dict(
102 os.environ, {"BPS_WMS_SERVICE_CLASS": "wms_test_utils.WmsServiceFromEnv"}
103 ):
104 filename = os.path.join(TESTDIR, "data/config_with_wms.yaml")
105 config = BpsConfig(
106 filename,
107 defaults={"wmsServiceClass": "wms_test_utils.WmsServiceFromDefaults"},
108 wms_service_class_fqn="wms_test_utils.WmsServiceFromCmdline",
109 )
110 self.assertEqual(set(config), {"corge", "wmsServiceClass"} | set(BPS_SEARCH_ORDER))
111 self.assertEqual(config["corge"], "cmdline")
113 def testWmsFromConfig(self):
114 """Test initialization when WMS class specified in the config."""
115 with unittest.mock.patch.dict(
116 os.environ, {"BPS_WMS_SERVICE_CLASS": "wms_test_utils.WmsServiceFromEnv"}
117 ):
118 filename = os.path.join(TESTDIR, "data/config_with_wms.yaml")
119 config = BpsConfig(
120 filename,
121 defaults={"wmsServiceClass": "wms_test_utils.WmsServiceFromDefaults"},
122 wms_service_class_fqn=None,
123 )
124 self.assertEqual(set(config), {"corge", "wmsServiceClass"} | set(BPS_SEARCH_ORDER))
125 self.assertEqual(config["corge"], "config")
126 self.assertEqual(config.search_order, BPS_SEARCH_ORDER)
128 def testWmsFromEnv(self):
129 """Test initialization with WMS class in the env."""
130 with unittest.mock.patch.dict(
131 os.environ, {"BPS_WMS_SERVICE_CLASS": "wms_test_utils.WmsServiceFromEnv"}
132 ):
133 config = BpsConfig(
134 {},
135 defaults={"wmsServiceClass": "wms_test_utils.WmsServiceFromDefaults"},
136 wms_service_class_fqn=None,
137 )
138 self.assertEqual(set(config), {"corge", "wmsServiceClass"} | set(BPS_SEARCH_ORDER))
139 self.assertEqual(config["corge"], "env")
140 self.assertEqual(config.search_order, BPS_SEARCH_ORDER)
142 def testWmsFromDefaults(self):
143 """Test initialization with a default WMS class."""
144 config = BpsConfig({}, defaults={"wmsServiceClass": "wms_test_utils.WmsServiceFromDefaults"})
145 self.assertEqual(set(config), {"corge", "wmsServiceClass"} | set(BPS_SEARCH_ORDER))
146 self.assertEqual(config["corge"], "defaults")
147 self.assertEqual(config.search_order, BPS_SEARCH_ORDER)
149 def testDefaultsExclusion(self):
150 config = BpsConfig(
151 self.filename, defaults=None, wms_service_class_fqn="wms_test_utils.WmsServiceFromCmdline"
152 )
153 self.assertEqual(set(config), {"foo", "bar", "baz"} | set(BPS_SEARCH_ORDER))
154 self.assertEqual(set(config["foo"]), {"qux"})
155 self.assertEqual(set(config["bar"]), {"qux"})
156 self.assertEqual(set(config["baz"]), {"grault", "garply"})
157 self.assertEqual(config.search_order, BPS_SEARCH_ORDER)
159 def testInvalidArg(self):
160 """Test if exception is raised for an argument of unsupported type."""
161 sequence = ["wibble", "wobble", "wubble", "flob"]
162 with self.assertRaises(ValueError):
163 BpsConfig(sequence)
166class TestBpsConfigGet(unittest.TestCase):
167 """Test retrieval of items from BpsConfig."""
169 def setUp(self):
170 self.config = BpsConfig({"foo": "bar"})
172 def tearDown(self):
173 pass
175 def testKeyExistsNoDefault(self):
176 """Test if the value is returned when the key is in the dictionary."""
177 self.assertEqual("bar", self.config.get("foo"))
179 def testKeyExistsDefaultProvided(self):
180 """Test if the value is returned when the key is in the dictionary."""
181 self.assertEqual("bar", self.config.get("foo", "qux"))
183 def testKeyMissingNoDefault(self):
184 """Test if the provided default is returned if the key is missing."""
185 self.assertEqual("", self.config.get("baz"))
187 def testKeyMissingDefaultProvided(self):
188 """Test if the custom default is returned if the key is missing."""
189 self.assertEqual("qux", self.config.get("baz", "qux"))
192class TestBpsConfigSearch(unittest.TestCase):
193 """Test searching of BpsConfig."""
195 def setUp(self):
196 filename = os.path.join(TESTDIR, "data/config.yaml")
197 self.config = BpsConfig(filename, search_order=["baz", "bar", "foo"], defaults={})
198 os.environ["GARPLY"] = "garply"
200 def tearDown(self):
201 del os.environ["GARPLY"]
203 def testSectionSearchOrder(self):
204 """Test if sections are searched in the prescribed order."""
205 key = "qux"
206 found, value = self.config.search(key)
207 self.assertEqual(found, True)
208 self.assertEqual(value, 2)
210 def testCurrentValues(self):
211 """Test if a current value overrides of the one in configuration."""
212 found, value = self.config.search("qux", opt={"curvals": {"qux": -3}})
213 self.assertEqual(found, True)
214 self.assertEqual(value, -3)
216 def testSearchobjValues(self):
217 """Test if a serachobj value overrides of the one in configuration."""
218 options = {"searchobj": {"qux": 4}}
219 found, value = self.config.search("qux", opt=options)
220 self.assertEqual(found, True)
221 self.assertEqual(value, 4)
223 def testSubsectionSearch(self):
224 options = {"curvals": {"curr_baz": "garply"}}
225 found, value = self.config.search("qux", opt=options)
226 self.assertEqual(found, True)
227 self.assertEqual(value, 3)
229 def testDefault(self):
230 """Test if a default value is properly set."""
231 found, value = self.config.search("plugh", opt={"default": 4})
232 self.assertEqual(found, True)
233 self.assertEqual(value, 4)
235 def testReplaceVars(self):
236 """Test replaceVars method."""
237 test_opt = {"default": 555}
238 orig_str = "<ENV:GARPLY>/waldo/{qux:03}/{notthere}"
239 value = self.config.replace_vars(orig_str, opt=test_opt)
240 self.assertEqual(value, "<ENV:GARPLY>/waldo/002/")
241 self.assertEqual(test_opt["default"], 555)
243 def testReplaceVarsSkipNames(self):
244 test_opt = {"default": 555, "skipNames": ["qgraphFile", "butlerConfig"]}
245 orig_str = "<ENV:GARPLY>/waldo/{qux:03} {qgraphFile} {butlerConfig}"
246 value = self.config.replace_vars(orig_str, opt=test_opt)
247 self.assertEqual(value, "<ENV:GARPLY>/waldo/002 {qgraphFile} {butlerConfig}")
248 self.assertEqual(test_opt["default"], 555)
250 def testVariables(self):
251 """Test combinations of expandEnvVars, replaceEnvVars,
252 and replaceVars.
253 """
254 test_opt = {"expandEnvVars": False, "replaceEnvVars": False, "replaceVars": False}
255 found, value = self.config.search("grault", opt=test_opt)
256 self.assertEqual(found, True)
257 self.assertEqual(value, "${GARPLY}/waldo/{qux:03}")
259 test_opt = {"expandEnvVars": False, "replaceEnvVars": False, "replaceVars": True}
260 found, value = self.config.search("grault", opt=test_opt)
261 self.assertEqual(found, True)
262 self.assertEqual(value, "${GARPLY}/waldo/002")
264 test_opt = {"expandEnvVars": False, "replaceEnvVars": True, "replaceVars": False}
265 found, value = self.config.search("grault", opt=test_opt)
266 self.assertEqual(found, True)
267 self.assertEqual(value, "<ENV:GARPLY>/waldo/{qux:03}")
269 test_opt = {"expandEnvVars": False, "replaceEnvVars": True, "replaceVars": True}
270 found, value = self.config.search("grault", opt=test_opt)
271 self.assertEqual(found, True)
272 self.assertEqual(value, "<ENV:GARPLY>/waldo/002")
274 test_opt = {"expandEnvVars": True, "replaceEnvVars": False, "replaceVars": False}
275 found, value = self.config.search("grault", opt=test_opt)
276 self.assertEqual(found, True)
277 self.assertEqual(value, "garply/waldo/{qux:03}")
279 test_opt = {"expandEnvVars": True, "replaceEnvVars": False, "replaceVars": True}
280 found, value = self.config.search("grault", opt=test_opt)
281 self.assertEqual(found, True)
282 self.assertEqual(value, "garply/waldo/002")
284 test_opt = {"expandEnvVars": True, "replaceEnvVars": True, "replaceVars": False}
285 found, value = self.config.search("grault", opt=test_opt)
286 self.assertEqual(found, True)
287 self.assertEqual(value, "garply/waldo/{qux:03}")
289 test_opt = {"expandEnvVars": True, "replaceEnvVars": True, "replaceVars": True}
290 found, value = self.config.search("grault", opt=test_opt)
291 self.assertEqual(found, True)
292 self.assertEqual(value, "garply/waldo/002")
294 def testRequired(self):
295 """Test if exception is raised if a required setting is missing."""
296 with self.assertRaises(KeyError):
297 self.config.search("fred", opt={"required": True})
299 def testS3Path(self):
300 """Test that double slashes aren't replaced with single slash."""
301 config = BpsConfig(self.config)
302 s3 = "s3://user1@rubin-place-users/butler-pipeline1-processing.yaml"
303 config["butlerConfig"] = s3
304 test_opt = {"expandEnvVars": True, "replaceEnvVars": True, "replaceVars": True}
305 found, value = config.search("butlerConfig", opt=test_opt)
306 self.assertEqual(found, True)
307 self.assertEqual(value, s3)
309 def testSubDirTemplate(self):
310 """Test that double slashes are replaced with single slash
311 in subDirTemplate.
312 """
313 config = BpsConfig(self.config)
315 # template doesn't end in slash, post-slashes value didn't end in slash
316 config["subDirTemplate"] = "{label}/{val1}/{val2}/{val3}/{val4}/{val5}"
317 test_opt = {
318 "expandEnvVars": True,
319 "replaceEnvVars": True,
320 "replaceVars": True,
321 "curvals": {"label": "label1", "val5": 12345},
322 }
323 found, value = config.search("subDirTemplate", opt=test_opt)
324 self.assertEqual(found, True)
325 self.assertEqual(value, "label1/12345")
327 # template doesn't end in slash, post-slashes value ended in slash
328 config["subDirTemplate"] = "{label}/{val1}/{val2}/{val3}/{val4}/{val5}"
329 test_opt = {
330 "expandEnvVars": True,
331 "replaceEnvVars": True,
332 "replaceVars": True,
333 "curvals": {"label": "label2", "val4": 12345},
334 }
335 found, value = config.search("subDirTemplate", opt=test_opt)
336 self.assertEqual(found, True)
337 self.assertEqual(value, "label2/12345")
339 # template ends in slash, post-slashes value didn't end in slash
340 config["subDirTemplate"] = "{label}/{val1}/{val2}/{val3}/{val4}/{val5}/"
341 test_opt = {
342 "expandEnvVars": True,
343 "replaceEnvVars": True,
344 "replaceVars": True,
345 "curvals": {"label": "label3", "val4": 12345},
346 }
347 found, value = config.search("subDirTemplate", opt=test_opt)
348 self.assertEqual(found, True)
349 self.assertEqual(value, "label3/12345/")
351 # template ends in slash, post-slashes value ended in slash
352 config["subDirTemplate"] = "{label}/{val1}/{val2}/{val3}/{val4}/{val5}/"
353 test_opt = {
354 "expandEnvVars": True,
355 "replaceEnvVars": True,
356 "replaceVars": True,
357 "curvals": {"label": "label4", "val2": 12345},
358 }
359 found, value = config.search("subDirTemplate", opt=test_opt)
360 self.assertEqual(found, True)
361 self.assertEqual(value, "label4/12345/")
363 def testOtherTemplate(self):
364 """Test that other subDirTemplate-like value doesn't have
365 double slashes replaced.
366 """
367 config = BpsConfig(self.config)
369 config["otherTemplate"] = "{label}/{val1}/{val2}/{val3}/{val4}/{val5}"
370 test_opt = {
371 "expandEnvVars": True,
372 "replaceEnvVars": True,
373 "replaceVars": True,
374 "curvals": {"label": "label1", "val4": 12345, "val5": ""},
375 }
376 found, value = config.search("otherTemplate", opt=test_opt)
377 self.assertEqual(found, True)
378 self.assertEqual(value, "label1////12345/")
381class TestBpsConfigGenerateConfig(unittest.TestCase):
382 """Test BpsConfig.generate_config and bpsEval methods."""
384 def setUp(self):
385 # Just to shorten string length in tests
386 self.test_prefix = "lsst.ctrl.bps.tests.config_test_utils"
388 filename = os.path.join(TESTDIR, "data/initialize_config.yaml")
389 self.config = BpsConfig(filename, BPS_SEARCH_ORDER, defaults={})
391 def testUnparsableValue(self):
392 config = BpsConfig(
393 {
394 "p1": 3,
395 "p3": 16,
396 },
397 BPS_SEARCH_ORDER,
398 )
399 # invalid function name
400 config["bpsGenerateConfig"] = "not a valid name({p1}, param3={p3})"
401 with self.assertRaisesRegex(ValueError, "Unparsable bpsGenerateConfig value='not a valid"):
402 config.generate_config()
404 def testMissingParen(self):
405 config = BpsConfig(
406 {
407 "p1": 3,
408 "p3": 16,
409 },
410 BPS_SEARCH_ORDER,
411 )
412 # invalid function name
413 config["bpsGenerateConfig"] = self.test_prefix + ".generate_config_1(1, param3=2"
414 with self.assertRaisesRegex(ValueError, "Unparsable bpsGenerateConfig value='"):
415 config.generate_config()
417 def testBadFunctionName(self):
418 config = BpsConfig(
419 {
420 "p1": 3,
421 "p3": 16,
422 },
423 BPS_SEARCH_ORDER,
424 )
425 # invalid function name
426 config["bpsGenerateConfig"] = self.test_prefix + ".notthere({p1}, param3={p3})"
427 with self.assertRaisesRegex(ImportError, "notthere"):
428 config.generate_config()
430 def testBadModuleName(self):
431 config = BpsConfig(
432 {
433 "p1": 3,
434 "p3": 16,
435 },
436 BPS_SEARCH_ORDER,
437 )
438 # invalid module name
439 config["bpsGenerateConfig"] = "lsst.ctrl.bps.notthere.generate_config(1, 2)"
440 with self.assertRaisesRegex(ImportError, "notthere"):
441 config.generate_config()
443 def testBadParamName(self):
444 config = BpsConfig(
445 {"p1": 3, "p3": 16, "bpsGenerateConfig": self.test_prefix + ".generate_config_1(1, param5=2)"},
446 BPS_SEARCH_ORDER,
447 )
448 with self.assertRaisesRegex(TypeError, "unexpected keyword argument 'param5'"):
449 config.generate_config()
451 def testExtraParam(self):
452 config = BpsConfig(
453 {
454 "p1": 3,
455 "p3": 16,
456 "bpsGenerateConfig": self.test_prefix + ".generate_config_1({p1}, 2, {p3}, 4)",
457 },
458 BPS_SEARCH_ORDER,
459 )
460 with self.assertRaisesRegex(TypeError, "positional arguments"):
461 config.generate_config()
463 def testWithSearchOrder(self):
464 # Check that bpsGenerateConfig is replaced in search sections
465 # (e.g., pipetask) And when replacing vars in subsections
466 # config ordering is used.
467 # Ditto for finalJob (which isn't a search section).
468 # Checking all in single function to ensure doesn't quit early.
469 self.config.generate_config()
471 filename = os.path.join(TESTDIR, "data/initialize_config_truth.yaml")
472 truth = BpsConfig(filename, BPS_SEARCH_ORDER, defaults={})
474 self.assertEqual(self.config, truth)
476 def testBpsEval(self):
477 """Test replacing bpsEval when need to import module."""
478 test_opt = {
479 "expandEnvVars": True,
480 "replaceEnvVars": True,
481 "replaceVars": True,
482 "curvals": {"curr_pipetask": "ptask1"},
483 }
484 found, value = self.config.search("genval1", opt=test_opt)
485 self.assertEqual(found, True)
486 self.assertEqual(value, "-c val1:0.1 -c val2:3")
488 def testBpsEvalBuiltin(self):
489 """Test replacing bpsEval with builtin function."""
490 test_opt = {
491 "expandEnvVars": True,
492 "replaceEnvVars": True,
493 "replaceVars": True,
494 "curvals": {"curr_pipetask": "ptask1"},
495 }
496 found, value = self.config.search("genval2", opt=test_opt)
497 self.assertEqual(found, True)
498 self.assertEqual(value, "-c val1:32")
500 def testBpsEvalInvalid(self):
501 """Test reporting not replacing bpsEval."""
502 test_opt = {
503 "expandEnvVars": True,
504 "replaceEnvVars": True,
505 "replaceVars": True,
506 "curvals": {"curr_pipetask": "ptask1"},
507 }
509 self.config["badkey1"] = "badval1 bpsEval('sum([1,2]') blah"
510 with self.assertRaisesRegex(ValueError, "Unparsable bpsEval in 'badval1 bpsEval"):
511 _ = self.config.search("badkey1", opt=test_opt)
514if __name__ == "__main__":
515 unittest.main()