Coverage for tests/test_pipelineIR.py: 15%
188 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-06 10:56 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-06 10:56 +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, PipelineSubsetCtrl
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 labeledSubsetModifyMode: DROP
173 - $TESTDIR/testPipeline2.yaml
174 """
175 )
177 pipeline = PipelineIR.from_string(pipeline_str)
178 self.assertEqual(set(pipeline.tasks.keys()), {"modA", "modB"})
180 # Test that you cant include and exclude a task
181 pipeline_str = textwrap.dedent(
182 """
183 description: Test Pipeline
184 imports:
185 - location: $TESTDIR/testPipeline1.yaml
186 exclude: modA
187 include: modB
188 labeledSubsetModifyMode: EDIT
189 - $TESTDIR/testPipeline2.yaml
190 """
191 )
193 with self.assertRaises(ValueError):
194 PipelineIR.from_string(pipeline_str)
196 # Test unknown labeledSubsetModifyModes raise
197 pipeline_str = textwrap.dedent(
198 """
199 description: Test Pipeline
200 imports:
201 - location: $TESTDIR/testPipeline1.yaml
202 exclude: modA
203 include: modB
204 labeledSubsetModifyMode: WRONG
205 - $TESTDIR/testPipeline2.yaml
206 """
207 )
208 with self.assertRaises(ValueError):
209 PipelineIR.from_string(pipeline_str)
211 # Test that contracts are imported
212 pipeline_str = textwrap.dedent(
213 """
214 description: Test Pipeline
215 imports:
216 - $TESTDIR/testPipeline1.yaml
217 """
218 )
220 pipeline = PipelineIR.from_string(pipeline_str)
221 self.assertEqual(pipeline.contracts[0].contract, "modA.b == modA.c")
223 # Test that contracts are not imported
224 pipeline_str = textwrap.dedent(
225 """
226 description: Test Pipeline
227 imports:
228 - location: $TESTDIR/testPipeline1.yaml
229 importContracts: False
230 """
231 )
233 pipeline = PipelineIR.from_string(pipeline_str)
234 self.assertEqual(pipeline.contracts, [])
236 # Test that configs are imported when defining the same task again
237 # with the same label
238 pipeline_str = textwrap.dedent(
239 """
240 description: Test Pipeline
241 imports:
242 - $TESTDIR/testPipeline2.yaml
243 tasks:
244 modA:
245 class: "test.moduleA"
246 config:
247 value2: 2
248 """
249 )
250 pipeline = PipelineIR.from_string(pipeline_str)
251 self.assertEqual(pipeline.tasks["modA"].config[0].rest, {"value1": 1, "value2": 2})
253 # Test that configs are not imported when redefining the task
254 # associated with a label
255 pipeline_str = textwrap.dedent(
256 """
257 description: Test Pipeline
258 imports:
259 - $TESTDIR/testPipeline2.yaml
260 tasks:
261 modA:
262 class: "test.moduleAReplace"
263 config:
264 value2: 2
265 """
266 )
267 pipeline = PipelineIR.from_string(pipeline_str)
268 self.assertEqual(pipeline.tasks["modA"].config[0].rest, {"value2": 2})
270 # Test that named subsets are imported
271 pipeline_str = textwrap.dedent(
272 """
273 description: Test Pipeline
274 imports:
275 - $TESTDIR/testPipeline2.yaml
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, {"modA"})
282 # Test that imported and redeclaring a named subset works
283 pipeline_str = textwrap.dedent(
284 """
285 description: Test Pipeline
286 imports:
287 - $TESTDIR/testPipeline2.yaml
288 tasks:
289 modE: "test.moduleE"
290 subsets:
291 modSubset:
292 - modE
293 """
294 )
295 pipeline = PipelineIR.from_string(pipeline_str)
296 self.assertEqual(pipeline.labeled_subsets.keys(), {"modSubset"})
297 self.assertEqual(pipeline.labeled_subsets["modSubset"].subset, {"modE"})
299 # Test that imported from two pipelines that both declare a named
300 # subset with the same name fails
301 pipeline_str = textwrap.dedent(
302 """
303 description: Test Pipeline
304 imports:
305 - $TESTDIR/testPipeline2.yaml
306 - $TESTDIR/testPipeline3.yaml
307 """
308 )
309 with self.assertRaises(ValueError):
310 PipelineIR.from_string(pipeline_str)
312 # Test that imported a named subset that duplicates a label declared
313 # in this pipeline fails
314 pipeline_str = textwrap.dedent(
315 """
316 description: Test Pipeline
317 imports:
318 - $TESTDIR/testPipeline2.yaml
319 tasks:
320 modSubset: "test.moduleE"
321 """
322 )
323 with self.assertRaises(ValueError):
324 PipelineIR.from_string(pipeline_str)
326 # Test that imported fails if a named subset and task label conflict
327 pipeline_str = textwrap.dedent(
328 """
329 description: Test Pipeline
330 imports:
331 - $TESTDIR/testPipeline2.yaml
332 - $TESTDIR/testPipeline4.yaml
333 """
334 )
335 with self.assertRaises(ValueError):
336 PipelineIR.from_string(pipeline_str)
338 def testReadParameters(self):
339 # verify that parameters section are read in from a pipeline
340 pipeline_str = textwrap.dedent(
341 """
342 description: Test Pipeline
343 parameters:
344 value1: A
345 value2: B
346 tasks:
347 modA: ModuleA
348 """
349 )
350 pipeline = PipelineIR.from_string(pipeline_str)
351 self.assertEqual(pipeline.parameters.mapping, {"value1": "A", "value2": "B"})
353 def testTaskParameterLabel(self):
354 # verify that "parameters" cannot be used as a task label
355 pipeline_str = textwrap.dedent(
356 """
357 description: Test Pipeline
358 tasks:
359 parameters: modA
360 """
361 )
362 with self.assertRaises(ValueError):
363 PipelineIR.from_string(pipeline_str)
365 def testParameterImporting(self):
366 # verify that importing parameters happens correctly
367 pipeline_str = textwrap.dedent(
368 """
369 description: Test Pipeline
370 imports:
371 - $TESTDIR/testPipeline1.yaml
372 - location: $TESTDIR/testPipeline2.yaml
373 exclude:
374 - modA
376 parameters:
377 value4: valued
378 """
379 )
380 pipeline = PipelineIR.from_string(pipeline_str)
381 self.assertEqual(
382 pipeline.parameters.mapping,
383 {"value4": "valued", "value1": "valueNew", "value2": "valueB", "value3": "valueC"},
384 )
386 def testImportingInstrument(self):
387 # verify an instrument is imported, or ignored, (Or otherwise modified
388 # for potential future use)
389 pipeline_str = textwrap.dedent(
390 """
391 description: Test Pipeline
392 imports:
393 - $TESTDIR/testPipeline1.yaml
394 """
395 )
396 pipeline = PipelineIR.from_string(pipeline_str)
397 self.assertEqual(pipeline.instrument, "test.instrument")
399 # verify that an imported pipeline can have its instrument set to None
400 pipeline_str = textwrap.dedent(
401 """
402 description: Test Pipeline
403 imports:
404 - location: $TESTDIR/testPipeline1.yaml
405 instrument: None
406 """
407 )
408 pipeline = PipelineIR.from_string(pipeline_str)
409 self.assertEqual(pipeline.instrument, None)
411 # verify that an imported pipeline can have its instrument modified
412 pipeline_str = textwrap.dedent(
413 """
414 description: Test Pipeline
415 imports:
416 - location: $TESTDIR/testPipeline1.yaml
417 instrument: new.instrument
418 """
419 )
420 pipeline = PipelineIR.from_string(pipeline_str)
421 self.assertEqual(pipeline.instrument, "new.instrument")
423 # Test that multiple instruments can't be defined,
424 # and that the error message tells you what instruments were found.
425 pipeline_str = textwrap.dedent(
426 """
427 description: Test Pipeline
428 instrument: new.instrument
429 imports:
430 - location: $TESTDIR/testPipeline1.yaml
431 """
432 )
433 with self.assertRaisesRegex(ValueError, "new.instrument .* test.instrument."):
434 PipelineIR.from_string(pipeline_str)
436 def testParameterConfigFormatting(self):
437 # verify that a config properly is formatted with parameters
438 pipeline_str = textwrap.dedent(
439 """
440 description: Test Pipeline
441 parameters:
442 value1: A
443 tasks:
444 modA:
445 class: ModuleA
446 config:
447 testKey: parameters.value1
448 """
449 )
450 pipeline = PipelineIR.from_string(pipeline_str)
451 newConfig = pipeline.tasks["modA"].config[0].formatted(pipeline.parameters)
452 self.assertEqual(newConfig.rest["testKey"], "A")
454 def testReadContracts(self):
455 # Verify that contracts are read in from a pipeline
456 location = "$TESTDIR/testPipeline1.yaml"
457 pipeline = PipelineIR.from_uri(location)
458 self.assertEqual(pipeline.contracts[0].contract, "modA.b == modA.c")
460 # Verify that a contract message is loaded
461 pipeline_str = textwrap.dedent(
462 """
463 description: Test Pipeline
464 tasks:
465 modA: test.modA
466 modB:
467 class: test.modB
468 contracts:
469 - contract: modA.foo == modB.Bar
470 msg: "Test message"
471 """
472 )
474 pipeline = PipelineIR.from_string(pipeline_str)
475 self.assertEqual(pipeline.contracts[0].msg, "Test message")
477 def testReadNamedSubsets(self):
478 pipeline_str = textwrap.dedent(
479 """
480 description: Test Pipeline
481 tasks:
482 modA: test.modA
483 modB:
484 class: test.modB
485 modC: test.modC
486 modD: test.modD
487 subsets:
488 subset1:
489 - modA
490 - modB
491 subset2:
492 subset:
493 - modC
494 - modD
495 description: "A test named subset"
496 """
497 )
498 pipeline = PipelineIR.from_string(pipeline_str)
499 self.assertEqual(pipeline.labeled_subsets.keys(), {"subset1", "subset2"})
501 self.assertEqual(pipeline.labeled_subsets["subset1"].subset, {"modA", "modB"})
502 self.assertEqual(pipeline.labeled_subsets["subset1"].description, None)
504 self.assertEqual(pipeline.labeled_subsets["subset2"].subset, {"modC", "modD"})
505 self.assertEqual(pipeline.labeled_subsets["subset2"].description, "A test named subset")
507 # verify that forgetting a subset key is an error
508 pipeline_str = textwrap.dedent(
509 """
510 description: Test Pipeline
511 tasks:
512 modA: test.modA
513 modB:
514 class: test.modB
515 modC: test.modC
516 modD: test.modD
517 subsets:
518 subset2:
519 sub:
520 - modC
521 - modD
522 description: "A test named subset"
523 """
524 )
525 with self.assertRaises(ValueError):
526 PipelineIR.from_string(pipeline_str)
528 # verify putting a label in a named subset that is not in the task is
529 # an error
530 pipeline_str = textwrap.dedent(
531 """
532 description: Test Pipeline
533 tasks:
534 modA: test.modA
535 modB:
536 class: test.modB
537 modC: test.modC
538 modD: test.modD
539 subsets:
540 subset2:
541 - modC
542 - modD
543 - modE
544 """
545 )
546 with self.assertRaises(ValueError):
547 PipelineIR.from_string(pipeline_str)
549 def testSubsettingPipeline(self):
550 pipeline_str = textwrap.dedent(
551 """
552 description: Test Pipeline
553 tasks:
554 modA: test.modA
555 modB:
556 class: test.modB
557 modC: test.modC
558 modD: test.modD
559 subsets:
560 subset1:
561 - modA
562 - modB
563 subset2:
564 subset:
565 - modC
566 - modD
567 description: "A test named subset"
568 """
569 )
570 pipeline = PipelineIR.from_string(pipeline_str)
571 # verify that creating a pipeline subset with the default drop behavior
572 # removes any labeled subset that contains a label not in the set of
573 # all task labels.
574 pipelineSubset1 = pipeline.subset_from_labels({"modA", "modB", "modC"})
575 self.assertEqual(pipelineSubset1.labeled_subsets.keys(), {"subset1"})
576 # verify that creating a pipeline subset with the edit behavior
577 # edits any labeled subset that contains a label not in the set of
578 # all task labels.
579 pipelineSubset2 = pipeline.subset_from_labels({"modA", "modB", "modC"}, PipelineSubsetCtrl.EDIT)
580 self.assertEqual(pipelineSubset2.labeled_subsets.keys(), {"subset1", "subset2"})
581 self.assertEqual(pipelineSubset2.labeled_subsets["subset2"].subset, {"modC"})
583 def testInstrument(self):
584 # Verify that if instrument is defined it is parsed out
585 pipeline_str = textwrap.dedent(
586 """
587 description: Test Pipeline
588 instrument: dummyCam
589 tasks:
590 modA: test.moduleA
591 """
592 )
594 pipeline = PipelineIR.from_string(pipeline_str)
595 self.assertEqual(pipeline.instrument, "dummyCam")
597 def testReadTaskConfig(self):
598 # Verify that a task with a config is read in correctly
599 pipeline_str = textwrap.dedent(
600 """
601 description: Test Pipeline
602 tasks:
603 modA:
604 class: test.moduleA
605 config:
606 propertyA: 6
607 propertyB: 7
608 file: testfile.py
609 python: "config.testDict['a'] = 9"
610 """
611 )
613 pipeline = PipelineIR.from_string(pipeline_str)
614 self.assertEqual(pipeline.tasks["modA"].config[0].file, ["testfile.py"])
615 self.assertEqual(pipeline.tasks["modA"].config[0].python, "config.testDict['a'] = 9")
616 self.assertEqual(pipeline.tasks["modA"].config[0].rest, {"propertyA": 6, "propertyB": 7})
618 # Verify that multiple files are read fine
619 pipeline_str = textwrap.dedent(
620 """
621 description: Test Pipeline
622 tasks:
623 modA:
624 class: test.moduleA
625 config:
626 file:
627 - testfile.py
628 - otherFile.py
629 """
630 )
632 pipeline = PipelineIR.from_string(pipeline_str)
633 self.assertEqual(pipeline.tasks["modA"].config[0].file, ["testfile.py", "otherFile.py"])
635 # Test reading multiple Config entries
636 pipeline_str = textwrap.dedent(
637 """
638 description: Test Pipeline
639 tasks:
640 modA:
641 class: test.moduleA
642 config:
643 - propertyA: 6
644 propertyB: 7
645 dataId: {"visit": 6}
646 - propertyA: 8
647 propertyB: 9
648 """
649 )
651 pipeline = PipelineIR.from_string(pipeline_str)
652 self.assertEqual(pipeline.tasks["modA"].config[0].rest, {"propertyA": 6, "propertyB": 7})
653 self.assertEqual(pipeline.tasks["modA"].config[0].dataId, {"visit": 6})
654 self.assertEqual(pipeline.tasks["modA"].config[1].rest, {"propertyA": 8, "propertyB": 9})
655 self.assertEqual(pipeline.tasks["modA"].config[1].dataId, None)
657 def testSerialization(self):
658 # Test creating a pipeline, writing it to a file, reading the file
659 pipeline_str = textwrap.dedent(
660 """
661 description: Test Pipeline
662 instrument: dummyCam
663 imports:
664 - location: $TESTDIR/testPipeline1.yaml
665 instrument: None
666 tasks:
667 modC:
668 class: test.moduleC
669 config:
670 - propertyA: 6
671 propertyB: 7
672 dataId: {"visit": 6}
673 - propertyA: 8
674 propertyB: 9
675 modD: test.moduleD
676 contracts:
677 - modA.foo == modB.bar
678 subsets:
679 subA:
680 - modA
681 - modC
682 """
683 )
685 pipeline = PipelineIR.from_string(pipeline_str)
687 # Create the temp file, write and read
688 with tempfile.NamedTemporaryFile() as tf:
689 pipeline.write_to_uri(tf.name)
690 loaded_pipeline = PipelineIR.from_uri(tf.name)
691 self.assertEqual(pipeline, loaded_pipeline)
693 def testPipelineYamlLoader(self):
694 # Tests that an exception is thrown in the case a key is used multiple
695 # times in a given scope within a pipeline file
696 pipeline_str = textwrap.dedent(
697 """
698 description: Test Pipeline
699 tasks:
700 modA: test1
701 modB: test2
702 modA: test3
703 """
704 )
705 self.assertRaises(KeyError, PipelineIR.from_string, pipeline_str)
707 def testMultiLineStrings(self):
708 """Test that multi-line strings in pipelines are written with
709 '|' continuation-syntax instead of explicit newlines.
710 """
711 pipeline_ir = PipelineIR({"description": "Line 1\nLine2\n", "tasks": {"modA": "task1"}})
712 string = str(pipeline_ir)
713 self.assertIn("|", string)
714 self.assertNotIn(r"\n", string)
717class MyMemoryTestCase(lsst.utils.tests.MemoryTestCase):
718 """Run file leak tests."""
721def setup_module(module):
722 """Configure pytest."""
723 lsst.utils.tests.init()
726if __name__ == "__main__":
727 lsst.utils.tests.init()
728 unittest.main()