Coverage for tests/test_pipelineIR.py: 15%
177 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-10-11 09:32 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2023-10-11 09:32 +0000
1# This file is part of pipe_base.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (http://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 <http://www.gnu.org/licenses/>.
28import os
29import tempfile
30import textwrap
31import unittest
33import lsst.utils.tests
34from lsst.pipe.base.pipelineIR import ConfigIR, PipelineIR
36# Find where the test pipelines exist and store it in an environment variable.
37os.environ["TESTDIR"] = os.path.dirname(__file__)
40class ConfigIRTestCase(unittest.TestCase):
41 """A test case for ConfigIR Objects
43 ConfigIR contains a method that is not exercised by the PipelineIR task,
44 so it should be tested here
45 """
47 def setUp(self):
48 pass
50 def tearDown(self):
51 pass
53 def testMergeConfig(self):
54 # Create some configs to merge
55 config1 = ConfigIR(
56 python="config.foo=6", dataId={"visit": 7}, file=["test1.py"], rest={"a": 1, "b": 2}
57 )
58 config2 = ConfigIR(python=None, dataId=None, file=["test2.py"], rest={"c": 1, "d": 2})
59 config3 = ConfigIR(python="config.bar=7", dataId=None, file=["test3.py"], rest={"c": 1, "d": 2})
60 config4 = ConfigIR(python=None, dataId=None, file=["test4.py"], rest={"c": 3, "e": 4})
61 config5 = ConfigIR(rest={"f": 5, "g": 6})
62 config6 = ConfigIR(rest={"h": 7, "i": 8})
63 config7 = ConfigIR(rest={"h": 9})
65 # Merge configs with different dataIds, this should yield two elements
66 self.assertEqual(list(config1.maybe_merge(config2)), [config1, config2])
68 # Merge configs with python blocks defined, this should yield two
69 # elements
70 self.assertEqual(list(config1.maybe_merge(config3)), [config1, config3])
72 # Merge configs with file defined, this should yield two elements
73 self.assertEqual(list(config2.maybe_merge(config4)), [config2, config4])
75 # merge config2 into config1
76 merge_result = list(config5.maybe_merge(config6))
77 self.assertEqual(len(merge_result), 1)
78 self.assertEqual(config5.rest, {"f": 5, "g": 6, "h": 7, "i": 8})
80 # Cant merge configs with shared keys
81 self.assertEqual(list(config6.maybe_merge(config7)), [config6, config7])
84class PipelineIRTestCase(unittest.TestCase):
85 """A test case for PipelineIR objects"""
87 def setUp(self):
88 pass
90 def tearDown(self):
91 pass
93 def testPipelineIRInitChecks(self):
94 # Missing description
95 pipeline_str = """
96 tasks:
97 a: module.A
98 """
99 with self.assertRaises(ValueError):
100 PipelineIR.from_string(pipeline_str)
102 # Missing tasks
103 pipeline_str = """
104 description: Test Pipeline
105 """
106 with self.assertRaises(ValueError):
107 PipelineIR.from_string(pipeline_str)
109 # This should raise a FileNotFoundError, as there are imported defined
110 # so the __init__ method should pass but the imported file does not
111 # exist
112 pipeline_str = textwrap.dedent(
113 """
114 description: Test Pipeline
115 imports: /dummy_pipeline.yaml
116 """
117 )
119 with self.assertRaises(FileNotFoundError):
120 PipelineIR.from_string(pipeline_str)
122 def testTaskParsing(self):
123 # Should be able to parse a task defined both ways
124 pipeline_str = textwrap.dedent(
125 """
126 description: Test Pipeline
127 tasks:
128 modA: test.modA
129 modB:
130 class: test.modB
131 """
132 )
134 pipeline = PipelineIR.from_string(pipeline_str)
135 self.assertEqual(list(pipeline.tasks.keys()), ["modA", "modB"])
136 self.assertEqual([t.klass for t in pipeline.tasks.values()], ["test.modA", "test.modB"])
138 def testImportParsing(self):
139 # This should raise, as the two pipelines, both define the same label
140 pipeline_str = textwrap.dedent(
141 """
142 description: Test Pipeline
143 imports:
144 - $TESTDIR/testPipeline1.yaml
145 - $TESTDIR/testPipeline2.yaml
146 """
147 )
148 # "modA" is the duplicated label, and it should appear in the error.
149 with self.assertRaisesRegex(ValueError, "modA"):
150 PipelineIR.from_string(pipeline_str)
152 # This should pass, as the conflicting task is excluded
153 pipeline_str = textwrap.dedent(
154 """
155 description: Test Pipeline
156 imports:
157 - location: $TESTDIR/testPipeline1.yaml
158 exclude: modA
159 - $TESTDIR/testPipeline2.yaml
160 """
161 )
162 pipeline = PipelineIR.from_string(pipeline_str)
163 self.assertEqual(set(pipeline.tasks.keys()), {"modA", "modB"})
165 # This should pass, as the conflicting task is no in includes
166 pipeline_str = textwrap.dedent(
167 """
168 description: Test Pipeline
169 imports:
170 - location: $TESTDIR/testPipeline1.yaml
171 include: modB
172 - $TESTDIR/testPipeline2.yaml
173 """
174 )
176 pipeline = PipelineIR.from_string(pipeline_str)
177 self.assertEqual(set(pipeline.tasks.keys()), {"modA", "modB"})
179 # Test that you cant include and exclude a task
180 pipeline_str = textwrap.dedent(
181 """
182 description: Test Pipeline
183 imports:
184 - location: $TESTDIR/testPipeline1.yaml
185 exclude: modA
186 include: modB
187 - $TESTDIR/testPipeline2.yaml
188 """
189 )
191 with self.assertRaises(ValueError):
192 PipelineIR.from_string(pipeline_str)
194 # Test that contracts are imported
195 pipeline_str = textwrap.dedent(
196 """
197 description: Test Pipeline
198 imports:
199 - $TESTDIR/testPipeline1.yaml
200 """
201 )
203 pipeline = PipelineIR.from_string(pipeline_str)
204 self.assertEqual(pipeline.contracts[0].contract, "modA.b == modA.c")
206 # Test that contracts are not imported
207 pipeline_str = textwrap.dedent(
208 """
209 description: Test Pipeline
210 imports:
211 - location: $TESTDIR/testPipeline1.yaml
212 importContracts: False
213 """
214 )
216 pipeline = PipelineIR.from_string(pipeline_str)
217 self.assertEqual(pipeline.contracts, [])
219 # Test that configs are imported when defining the same task again
220 # with the same label
221 pipeline_str = textwrap.dedent(
222 """
223 description: Test Pipeline
224 imports:
225 - $TESTDIR/testPipeline2.yaml
226 tasks:
227 modA:
228 class: "test.moduleA"
229 config:
230 value2: 2
231 """
232 )
233 pipeline = PipelineIR.from_string(pipeline_str)
234 self.assertEqual(pipeline.tasks["modA"].config[0].rest, {"value1": 1, "value2": 2})
236 # Test that configs are not imported when redefining the task
237 # associated with a label
238 pipeline_str = textwrap.dedent(
239 """
240 description: Test Pipeline
241 imports:
242 - $TESTDIR/testPipeline2.yaml
243 tasks:
244 modA:
245 class: "test.moduleAReplace"
246 config:
247 value2: 2
248 """
249 )
250 pipeline = PipelineIR.from_string(pipeline_str)
251 self.assertEqual(pipeline.tasks["modA"].config[0].rest, {"value2": 2})
253 # Test that named subsets are imported
254 pipeline_str = textwrap.dedent(
255 """
256 description: Test Pipeline
257 imports:
258 - $TESTDIR/testPipeline2.yaml
259 """
260 )
261 pipeline = PipelineIR.from_string(pipeline_str)
262 self.assertEqual(pipeline.labeled_subsets.keys(), {"modSubset"})
263 self.assertEqual(pipeline.labeled_subsets["modSubset"].subset, {"modA"})
265 # Test that imported and redeclaring a named subset works
266 pipeline_str = textwrap.dedent(
267 """
268 description: Test Pipeline
269 imports:
270 - $TESTDIR/testPipeline2.yaml
271 tasks:
272 modE: "test.moduleE"
273 subsets:
274 modSubset:
275 - modE
276 """
277 )
278 pipeline = PipelineIR.from_string(pipeline_str)
279 self.assertEqual(pipeline.labeled_subsets.keys(), {"modSubset"})
280 self.assertEqual(pipeline.labeled_subsets["modSubset"].subset, {"modE"})
282 # Test that imported from two pipelines that both declare a named
283 # subset with the same name fails
284 pipeline_str = textwrap.dedent(
285 """
286 description: Test Pipeline
287 imports:
288 - $TESTDIR/testPipeline2.yaml
289 - $TESTDIR/testPipeline3.yaml
290 """
291 )
292 with self.assertRaises(ValueError):
293 PipelineIR.from_string(pipeline_str)
295 # Test that imported a named subset that duplicates a label declared
296 # in this pipeline fails
297 pipeline_str = textwrap.dedent(
298 """
299 description: Test Pipeline
300 imports:
301 - $TESTDIR/testPipeline2.yaml
302 tasks:
303 modSubset: "test.moduleE"
304 """
305 )
306 with self.assertRaises(ValueError):
307 PipelineIR.from_string(pipeline_str)
309 # Test that imported fails if a named subset and task label conflict
310 pipeline_str = textwrap.dedent(
311 """
312 description: Test Pipeline
313 imports:
314 - $TESTDIR/testPipeline2.yaml
315 - $TESTDIR/testPipeline4.yaml
316 """
317 )
318 with self.assertRaises(ValueError):
319 PipelineIR.from_string(pipeline_str)
321 def testReadParameters(self):
322 # verify that parameters section are read in from a pipeline
323 pipeline_str = textwrap.dedent(
324 """
325 description: Test Pipeline
326 parameters:
327 value1: A
328 value2: B
329 tasks:
330 modA: ModuleA
331 """
332 )
333 pipeline = PipelineIR.from_string(pipeline_str)
334 self.assertEqual(pipeline.parameters.mapping, {"value1": "A", "value2": "B"})
336 def testTaskParameterLabel(self):
337 # verify that "parameters" cannot be used as a task label
338 pipeline_str = textwrap.dedent(
339 """
340 description: Test Pipeline
341 tasks:
342 parameters: modA
343 """
344 )
345 with self.assertRaises(ValueError):
346 PipelineIR.from_string(pipeline_str)
348 def testParameterImporting(self):
349 # verify that importing parameters happens correctly
350 pipeline_str = textwrap.dedent(
351 """
352 description: Test Pipeline
353 imports:
354 - $TESTDIR/testPipeline1.yaml
355 - location: $TESTDIR/testPipeline2.yaml
356 exclude:
357 - modA
359 parameters:
360 value4: valued
361 """
362 )
363 pipeline = PipelineIR.from_string(pipeline_str)
364 self.assertEqual(
365 pipeline.parameters.mapping,
366 {"value4": "valued", "value1": "valueNew", "value2": "valueB", "value3": "valueC"},
367 )
369 def testImportingInstrument(self):
370 # verify an instrument is imported, or ignored, (Or otherwise modified
371 # for potential future use)
372 pipeline_str = textwrap.dedent(
373 """
374 description: Test Pipeline
375 imports:
376 - $TESTDIR/testPipeline1.yaml
377 """
378 )
379 pipeline = PipelineIR.from_string(pipeline_str)
380 self.assertEqual(pipeline.instrument, "test.instrument")
382 # verify that an imported pipeline can have its instrument set to None
383 pipeline_str = textwrap.dedent(
384 """
385 description: Test Pipeline
386 imports:
387 - location: $TESTDIR/testPipeline1.yaml
388 instrument: None
389 """
390 )
391 pipeline = PipelineIR.from_string(pipeline_str)
392 self.assertEqual(pipeline.instrument, None)
394 # verify that an imported pipeline can have its instrument modified
395 pipeline_str = textwrap.dedent(
396 """
397 description: Test Pipeline
398 imports:
399 - location: $TESTDIR/testPipeline1.yaml
400 instrument: new.instrument
401 """
402 )
403 pipeline = PipelineIR.from_string(pipeline_str)
404 self.assertEqual(pipeline.instrument, "new.instrument")
406 # Test that multiple instruments can't be defined,
407 # and that the error message tells you what instruments were found.
408 pipeline_str = textwrap.dedent(
409 """
410 description: Test Pipeline
411 instrument: new.instrument
412 imports:
413 - location: $TESTDIR/testPipeline1.yaml
414 """
415 )
416 with self.assertRaisesRegex(ValueError, "new.instrument .* test.instrument."):
417 PipelineIR.from_string(pipeline_str)
419 def testParameterConfigFormatting(self):
420 # verify that a config properly is formatted with parameters
421 pipeline_str = textwrap.dedent(
422 """
423 description: Test Pipeline
424 parameters:
425 value1: A
426 tasks:
427 modA:
428 class: ModuleA
429 config:
430 testKey: parameters.value1
431 """
432 )
433 pipeline = PipelineIR.from_string(pipeline_str)
434 newConfig = pipeline.tasks["modA"].config[0].formatted(pipeline.parameters)
435 self.assertEqual(newConfig.rest["testKey"], "A")
437 def testReadContracts(self):
438 # Verify that contracts are read in from a pipeline
439 location = "$TESTDIR/testPipeline1.yaml"
440 pipeline = PipelineIR.from_uri(location)
441 self.assertEqual(pipeline.contracts[0].contract, "modA.b == modA.c")
443 # Verify that a contract message is loaded
444 pipeline_str = textwrap.dedent(
445 """
446 description: Test Pipeline
447 tasks:
448 modA: test.modA
449 modB:
450 class: test.modB
451 contracts:
452 - contract: modA.foo == modB.Bar
453 msg: "Test message"
454 """
455 )
457 pipeline = PipelineIR.from_string(pipeline_str)
458 self.assertEqual(pipeline.contracts[0].msg, "Test message")
460 def testReadNamedSubsets(self):
461 pipeline_str = textwrap.dedent(
462 """
463 description: Test Pipeline
464 tasks:
465 modA: test.modA
466 modB:
467 class: test.modB
468 modC: test.modC
469 modD: test.modD
470 subsets:
471 subset1:
472 - modA
473 - modB
474 subset2:
475 subset:
476 - modC
477 - modD
478 description: "A test named subset"
479 """
480 )
481 pipeline = PipelineIR.from_string(pipeline_str)
482 self.assertEqual(pipeline.labeled_subsets.keys(), {"subset1", "subset2"})
484 self.assertEqual(pipeline.labeled_subsets["subset1"].subset, {"modA", "modB"})
485 self.assertEqual(pipeline.labeled_subsets["subset1"].description, None)
487 self.assertEqual(pipeline.labeled_subsets["subset2"].subset, {"modC", "modD"})
488 self.assertEqual(pipeline.labeled_subsets["subset2"].description, "A test named subset")
490 # verify that forgetting a subset key is an error
491 pipeline_str = textwrap.dedent(
492 """
493 description: Test Pipeline
494 tasks:
495 modA: test.modA
496 modB:
497 class: test.modB
498 modC: test.modC
499 modD: test.modD
500 subsets:
501 subset2:
502 sub:
503 - modC
504 - modD
505 description: "A test named subset"
506 """
507 )
508 with self.assertRaises(ValueError):
509 PipelineIR.from_string(pipeline_str)
511 # verify putting a label in a named subset that is not in the task is
512 # an error
513 pipeline_str = textwrap.dedent(
514 """
515 description: Test Pipeline
516 tasks:
517 modA: test.modA
518 modB:
519 class: test.modB
520 modC: test.modC
521 modD: test.modD
522 subsets:
523 subset2:
524 - modC
525 - modD
526 - modE
527 """
528 )
529 with self.assertRaises(ValueError):
530 PipelineIR.from_string(pipeline_str)
532 def testInstrument(self):
533 # Verify that if instrument is defined it is parsed out
534 pipeline_str = textwrap.dedent(
535 """
536 description: Test Pipeline
537 instrument: dummyCam
538 tasks:
539 modA: test.moduleA
540 """
541 )
543 pipeline = PipelineIR.from_string(pipeline_str)
544 self.assertEqual(pipeline.instrument, "dummyCam")
546 def testReadTaskConfig(self):
547 # Verify that a task with a config is read in correctly
548 pipeline_str = textwrap.dedent(
549 """
550 description: Test Pipeline
551 tasks:
552 modA:
553 class: test.moduleA
554 config:
555 propertyA: 6
556 propertyB: 7
557 file: testfile.py
558 python: "config.testDict['a'] = 9"
559 """
560 )
562 pipeline = PipelineIR.from_string(pipeline_str)
563 self.assertEqual(pipeline.tasks["modA"].config[0].file, ["testfile.py"])
564 self.assertEqual(pipeline.tasks["modA"].config[0].python, "config.testDict['a'] = 9")
565 self.assertEqual(pipeline.tasks["modA"].config[0].rest, {"propertyA": 6, "propertyB": 7})
567 # Verify that multiple files are read fine
568 pipeline_str = textwrap.dedent(
569 """
570 description: Test Pipeline
571 tasks:
572 modA:
573 class: test.moduleA
574 config:
575 file:
576 - testfile.py
577 - otherFile.py
578 """
579 )
581 pipeline = PipelineIR.from_string(pipeline_str)
582 self.assertEqual(pipeline.tasks["modA"].config[0].file, ["testfile.py", "otherFile.py"])
584 # Test reading multiple Config entries
585 pipeline_str = textwrap.dedent(
586 """
587 description: Test Pipeline
588 tasks:
589 modA:
590 class: test.moduleA
591 config:
592 - propertyA: 6
593 propertyB: 7
594 dataId: {"visit": 6}
595 - propertyA: 8
596 propertyB: 9
597 """
598 )
600 pipeline = PipelineIR.from_string(pipeline_str)
601 self.assertEqual(pipeline.tasks["modA"].config[0].rest, {"propertyA": 6, "propertyB": 7})
602 self.assertEqual(pipeline.tasks["modA"].config[0].dataId, {"visit": 6})
603 self.assertEqual(pipeline.tasks["modA"].config[1].rest, {"propertyA": 8, "propertyB": 9})
604 self.assertEqual(pipeline.tasks["modA"].config[1].dataId, None)
606 def testSerialization(self):
607 # Test creating a pipeline, writing it to a file, reading the file
608 pipeline_str = textwrap.dedent(
609 """
610 description: Test Pipeline
611 instrument: dummyCam
612 imports:
613 - location: $TESTDIR/testPipeline1.yaml
614 instrument: None
615 tasks:
616 modC:
617 class: test.moduleC
618 config:
619 - propertyA: 6
620 propertyB: 7
621 dataId: {"visit": 6}
622 - propertyA: 8
623 propertyB: 9
624 modD: test.moduleD
625 contracts:
626 - modA.foo == modB.bar
627 subsets:
628 subA:
629 - modA
630 - modC
631 """
632 )
634 pipeline = PipelineIR.from_string(pipeline_str)
636 # Create the temp file, write and read
637 with tempfile.NamedTemporaryFile() as tf:
638 pipeline.write_to_uri(tf.name)
639 loaded_pipeline = PipelineIR.from_uri(tf.name)
640 self.assertEqual(pipeline, loaded_pipeline)
642 def testPipelineYamlLoader(self):
643 # Tests that an exception is thrown in the case a key is used multiple
644 # times in a given scope within a pipeline file
645 pipeline_str = textwrap.dedent(
646 """
647 description: Test Pipeline
648 tasks:
649 modA: test1
650 modB: test2
651 modA: test3
652 """
653 )
654 self.assertRaises(KeyError, PipelineIR.from_string, pipeline_str)
656 def testMultiLineStrings(self):
657 """Test that multi-line strings in pipelines are written with
658 '|' continuation-syntax instead of explicit newlines.
659 """
660 pipeline_ir = PipelineIR({"description": "Line 1\nLine2\n", "tasks": {"modA": "task1"}})
661 string = str(pipeline_ir)
662 self.assertIn("|", string)
663 self.assertNotIn(r"\n", string)
666class MyMemoryTestCase(lsst.utils.tests.MemoryTestCase):
667 """Run file leak tests."""
670def setup_module(module):
671 """Configure pytest."""
672 lsst.utils.tests.init()
675if __name__ == "__main__":
676 lsst.utils.tests.init()
677 unittest.main()