Coverage for tests/test_pipelineIR.py: 15%

177 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-11-30 12:09 +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/>. 

27 

28import os 

29import tempfile 

30import textwrap 

31import unittest 

32 

33import lsst.utils.tests 

34from lsst.pipe.base.pipelineIR import ConfigIR, PipelineIR 

35 

36# Find where the test pipelines exist and store it in an environment variable. 

37os.environ["TESTDIR"] = os.path.dirname(__file__) 

38 

39 

40class ConfigIRTestCase(unittest.TestCase): 

41 """A test case for ConfigIR Objects 

42 

43 ConfigIR contains a method that is not exercised by the PipelineIR task, 

44 so it should be tested here 

45 """ 

46 

47 def setUp(self): 

48 pass 

49 

50 def tearDown(self): 

51 pass 

52 

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}) 

64 

65 # Merge configs with different dataIds, this should yield two elements 

66 self.assertEqual(list(config1.maybe_merge(config2)), [config1, config2]) 

67 

68 # Merge configs with python blocks defined, this should yield two 

69 # elements 

70 self.assertEqual(list(config1.maybe_merge(config3)), [config1, config3]) 

71 

72 # Merge configs with file defined, this should yield two elements 

73 self.assertEqual(list(config2.maybe_merge(config4)), [config2, config4]) 

74 

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}) 

79 

80 # Cant merge configs with shared keys 

81 self.assertEqual(list(config6.maybe_merge(config7)), [config6, config7]) 

82 

83 

84class PipelineIRTestCase(unittest.TestCase): 

85 """A test case for PipelineIR objects""" 

86 

87 def setUp(self): 

88 pass 

89 

90 def tearDown(self): 

91 pass 

92 

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) 

101 

102 # Missing tasks 

103 pipeline_str = """ 

104 description: Test Pipeline 

105 """ 

106 with self.assertRaises(ValueError): 

107 PipelineIR.from_string(pipeline_str) 

108 

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 ) 

118 

119 with self.assertRaises(FileNotFoundError): 

120 PipelineIR.from_string(pipeline_str) 

121 

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 ) 

133 

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"]) 

137 

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) 

151 

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"}) 

164 

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 ) 

175 

176 pipeline = PipelineIR.from_string(pipeline_str) 

177 self.assertEqual(set(pipeline.tasks.keys()), {"modA", "modB"}) 

178 

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 ) 

190 

191 with self.assertRaises(ValueError): 

192 PipelineIR.from_string(pipeline_str) 

193 

194 # Test that contracts are imported 

195 pipeline_str = textwrap.dedent( 

196 """ 

197 description: Test Pipeline 

198 imports: 

199 - $TESTDIR/testPipeline1.yaml 

200 """ 

201 ) 

202 

203 pipeline = PipelineIR.from_string(pipeline_str) 

204 self.assertEqual(pipeline.contracts[0].contract, "modA.b == modA.c") 

205 

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 ) 

215 

216 pipeline = PipelineIR.from_string(pipeline_str) 

217 self.assertEqual(pipeline.contracts, []) 

218 

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}) 

235 

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}) 

252 

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"}) 

264 

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"}) 

281 

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) 

294 

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) 

308 

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) 

320 

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"}) 

335 

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) 

347 

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 

358 

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 ) 

368 

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") 

381 

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) 

393 

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") 

405 

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) 

418 

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") 

436 

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") 

442 

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 ) 

456 

457 pipeline = PipelineIR.from_string(pipeline_str) 

458 self.assertEqual(pipeline.contracts[0].msg, "Test message") 

459 

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"}) 

483 

484 self.assertEqual(pipeline.labeled_subsets["subset1"].subset, {"modA", "modB"}) 

485 self.assertEqual(pipeline.labeled_subsets["subset1"].description, None) 

486 

487 self.assertEqual(pipeline.labeled_subsets["subset2"].subset, {"modC", "modD"}) 

488 self.assertEqual(pipeline.labeled_subsets["subset2"].description, "A test named subset") 

489 

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) 

510 

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) 

531 

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 ) 

542 

543 pipeline = PipelineIR.from_string(pipeline_str) 

544 self.assertEqual(pipeline.instrument, "dummyCam") 

545 

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 ) 

561 

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}) 

566 

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 ) 

580 

581 pipeline = PipelineIR.from_string(pipeline_str) 

582 self.assertEqual(pipeline.tasks["modA"].config[0].file, ["testfile.py", "otherFile.py"]) 

583 

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 ) 

599 

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) 

605 

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 ) 

633 

634 pipeline = PipelineIR.from_string(pipeline_str) 

635 

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) 

641 

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) 

655 

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) 

664 

665 

666class MyMemoryTestCase(lsst.utils.tests.MemoryTestCase): 

667 """Run file leak tests.""" 

668 

669 

670def setup_module(module): 

671 """Configure pytest.""" 

672 lsst.utils.tests.init() 

673 

674 

675if __name__ == "__main__": 

676 lsst.utils.tests.init() 

677 unittest.main()