Coverage for tests/test_simple_pipeline_executor.py: 10%

279 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-10-10 01:31 -0700

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# (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 

28from __future__ import annotations 

29 

30import os 

31import shutil 

32import tempfile 

33import unittest 

34 

35import lsst.daf.butler 

36import lsst.pipe.base.quantum_provenance_graph as qpg 

37import lsst.utils.tests 

38from lsst.pipe.base import PipelineGraph, QuantumSuccessCaveats, RepeatableQuantumError 

39from lsst.pipe.base.simple_pipeline_executor import SimplePipelineExecutor 

40from lsst.pipe.base.tests.mocks import ( 

41 DynamicConnectionConfig, 

42 DynamicTestPipelineTask, 

43 DynamicTestPipelineTaskConfig, 

44 MockStorageClass, 

45 get_mock_name, 

46) 

47 

48TESTDIR = os.path.abspath(os.path.dirname(__file__)) 

49 

50 

51class SimplePipelineExecutorTests(lsst.utils.tests.TestCase): 

52 """Test the SimplePipelineExecutor API. 

53 

54 Because SimplePipelineExecutor is the easiest way to run simple pipelines 

55 in tests, this has also become a home for tests of execution edge cases 

56 that don't have a clear home in other test files. 

57 """ 

58 

59 def setUp(self): 

60 self.path = tempfile.mkdtemp() 

61 # standalone parameter forces the returned config to also include 

62 # the information from the search paths. 

63 config = lsst.daf.butler.Butler.makeRepo( 

64 self.path, standalone=True, searchPaths=[os.path.join(TESTDIR, "config")] 

65 ) 

66 self.butler = SimplePipelineExecutor.prep_butler(config, [], "fake") 

67 self.butler.registry.registerDatasetType( 

68 lsst.daf.butler.DatasetType( 

69 "input", 

70 dimensions=self.butler.dimensions.empty, 

71 storageClass="StructuredDataDict", 

72 ) 

73 ) 

74 self.butler.put({"zero": 0}, "input") 

75 MockStorageClass.get_or_register_mock("StructuredDataDict") 

76 MockStorageClass.get_or_register_mock("TaskMetadataLike") 

77 

78 def tearDown(self): 

79 shutil.rmtree(self.path, ignore_errors=True) 

80 

81 def test_from_task_class(self): 

82 """Test executing a single quantum with an executor created by the 

83 `from_task_class` factory method, and the 

84 `SimplePipelineExecutor.as_generator` method. 

85 """ 

86 config = DynamicTestPipelineTaskConfig() 

87 config.inputs["i"] = DynamicConnectionConfig( 

88 dataset_type_name="input", storage_class="StructuredDataDict", mock_storage_class=False 

89 ) 

90 config.outputs["o"] = DynamicConnectionConfig( 

91 dataset_type_name="output", storage_class="StructuredDataDict" 

92 ) 

93 executor = SimplePipelineExecutor.from_task_class( 

94 DynamicTestPipelineTask, 

95 config=config, 

96 butler=self.butler, 

97 label="a", 

98 ) 

99 (quantum,) = executor.as_generator(register_dataset_types=True) 

100 self.assertEqual(self.butler.get("output").storage_class, get_mock_name("StructuredDataDict")) 

101 

102 def test_metadata_input(self): 

103 """Test two tasks where the output uses metadata from input.""" 

104 config_a = DynamicTestPipelineTaskConfig() 

105 config_a.inputs["i"] = DynamicConnectionConfig( 

106 dataset_type_name="input", storage_class="StructuredDataDict", mock_storage_class=False 

107 ) 

108 config_b = DynamicTestPipelineTaskConfig() 

109 config_b.outputs["o"] = DynamicConnectionConfig( 

110 dataset_type_name="output", storage_class="StructuredDataDict" 

111 ) 

112 config_b.inputs["in_metadata"] = DynamicConnectionConfig( 

113 dataset_type_name="a_metadata", storage_class="TaskMetadata" 

114 ) 

115 pipeline_graph = PipelineGraph() 

116 pipeline_graph.add_task("a", DynamicTestPipelineTask, config_a) 

117 pipeline_graph.add_task("b", DynamicTestPipelineTask, config_b) 

118 executor = SimplePipelineExecutor.from_pipeline_graph(pipeline_graph, butler=self.butler) 

119 quanta = executor.run(register_dataset_types=True, save_versions=False) 

120 self.assertEqual(len(quanta), 2) 

121 output = self.butler.get("output") 

122 self.assertEqual(output.quantum.inputs["in_metadata"][0].original_type, "lsst.pipe.base.TaskMetadata") 

123 

124 def test_optional_intermediate(self): 

125 """Test a pipeline task with an optional regular input that is produced 

126 by another task. 

127 """ 

128 config_a = DynamicTestPipelineTaskConfig() 

129 config_a.inputs["i"] = DynamicConnectionConfig( 

130 dataset_type_name="input", storage_class="StructuredDataDict", mock_storage_class=False 

131 ) 

132 config_a.fail_exception = "lsst.pipe.base.NoWorkFound" 

133 config_a.fail_condition = "1=1" # butler query expression that is true 

134 config_a.outputs["o"] = DynamicConnectionConfig( 

135 dataset_type_name="intermediate", storage_class="StructuredDataDict" 

136 ) 

137 config_b = DynamicTestPipelineTaskConfig() 

138 config_b.inputs["i"] = DynamicConnectionConfig( 

139 dataset_type_name="intermediate", storage_class="StructuredDataDict", minimum=0 

140 ) 

141 config_b.outputs["o"] = DynamicConnectionConfig( 

142 dataset_type_name="output", storage_class="StructuredDataDict" 

143 ) 

144 pipeline_graph = PipelineGraph() 

145 pipeline_graph.add_task("a", DynamicTestPipelineTask, config_a) 

146 pipeline_graph.add_task("b", DynamicTestPipelineTask, config_b) 

147 executor = SimplePipelineExecutor.from_pipeline_graph(pipeline_graph, butler=self.butler) 

148 quanta = executor.run(register_dataset_types=True, save_versions=False) 

149 self.assertEqual(len(quanta), 2) 

150 # Both quanta ran successfully (NoWorkFound is a success). 

151 self.assertTrue(self.butler.exists("a_metadata")) 

152 self.assertTrue(self.butler.exists("b_metadata")) 

153 # The intermediate dataset was not written, but the final output was. 

154 self.assertFalse(self.butler.exists("intermediate")) 

155 self.assertTrue(self.butler.exists("output")) 

156 

157 def test_optional_input(self): 

158 """Test a pipeline task with an optional regular input that is an 

159 overall input to the pipeline. 

160 """ 

161 config_a = DynamicTestPipelineTaskConfig() 

162 config_a.inputs["i1"] = DynamicConnectionConfig( 

163 dataset_type_name="input", storage_class="StructuredDataDict", mock_storage_class=False 

164 ) 

165 config_a.outputs["i2"] = DynamicConnectionConfig( 

166 dataset_type_name="input_2", 

167 storage_class="StructuredDataDict", # will never exist 

168 ) 

169 config_a.outputs["o"] = DynamicConnectionConfig( 

170 dataset_type_name="output", storage_class="StructuredDataDict" 

171 ) 

172 pipeline_graph = PipelineGraph() 

173 pipeline_graph.add_task("a", DynamicTestPipelineTask, config_a) 

174 executor = SimplePipelineExecutor.from_pipeline_graph(pipeline_graph, butler=self.butler) 

175 quanta = executor.run(register_dataset_types=True, save_versions=False) 

176 self.assertEqual(len(quanta), 1) 

177 # The quanta ran successfully. 

178 self.assertTrue(self.butler.exists("a_metadata")) 

179 # The final output was written. 

180 self.assertTrue(self.butler.exists("output")) 

181 

182 def test_from_pipeline_file(self) -> None: 

183 """Test executing a two quanta from different configurations of the 

184 same task, with an executor created by the `from_pipeline_filename` 

185 factory method, and the `SimplePipelineExecutor.run` method. 

186 """ 

187 filename = os.path.join(TESTDIR, "pipelines", "pipeline_simple.yaml") 

188 executor = SimplePipelineExecutor.from_pipeline_filename(filename, butler=self.butler) 

189 self._test_pipeline_file(executor) 

190 

191 def test_use_local_butler(self) -> None: 

192 """Test generating a local butler repository from a pipeline, then 

193 running that pipeline using the local butler. 

194 """ 

195 filename = os.path.join(TESTDIR, "pipelines", "pipeline_simple.yaml") 

196 executor = SimplePipelineExecutor.from_pipeline_filename( 

197 filename, butler=self.butler, output="u/someone/pipeline" 

198 ) 

199 with tempfile.TemporaryDirectory() as tempdir: 

200 root = os.path.join(tempdir, "butler_root") 

201 executor.use_local_butler(root) 

202 self._test_pipeline_file(executor) 

203 

204 def _test_pipeline_file(self, executor: SimplePipelineExecutor) -> None: 

205 quanta = executor.run(register_dataset_types=True, save_versions=False) 

206 self.assertEqual(len(quanta), 2) 

207 self.assertEqual( 

208 executor.butler.get("intermediate").storage_class, get_mock_name("StructuredDataDict") 

209 ) 

210 self.assertEqual(executor.butler.get("output").storage_class, get_mock_name("StructuredDataDict")) 

211 

212 def test_partial_outputs_success(self): 

213 """Test executing two quanta where the first raises 

214 `lsst.pipe.base.AnnotatedPartialOutputsError` and its output is an 

215 optional input to the second, while configuring the executor to 

216 consider this a success. 

217 """ 

218 config_a = DynamicTestPipelineTaskConfig() 

219 config_a.inputs["i"] = DynamicConnectionConfig( 

220 dataset_type_name="input", storage_class="StructuredDataDict", mock_storage_class=False 

221 ) 

222 config_a.fail_exception = "lsst.pipe.base.AnnotatedPartialOutputsError" 

223 config_a.fail_condition = "1=1" # butler query expression that is true 

224 config_a.outputs["o"] = DynamicConnectionConfig( 

225 dataset_type_name="intermediate", storage_class="StructuredDataDict" 

226 ) 

227 config_b = DynamicTestPipelineTaskConfig() 

228 config_b.inputs["i"] = DynamicConnectionConfig( 

229 dataset_type_name="intermediate", storage_class="StructuredDataDict", minimum=0 

230 ) 

231 config_b.outputs["o"] = DynamicConnectionConfig( 

232 dataset_type_name="output", storage_class="StructuredDataDict" 

233 ) 

234 pipeline_graph = PipelineGraph() 

235 pipeline_graph.add_task("a", DynamicTestPipelineTask, config_a) 

236 pipeline_graph.add_task("b", DynamicTestPipelineTask, config_b) 

237 # Consider the partial a success and proceed. 

238 executor = SimplePipelineExecutor.from_pipeline_graph( 

239 pipeline_graph, butler=self.butler, raise_on_partial_outputs=False 

240 ) 

241 (_, _) = executor.as_generator(register_dataset_types=True) 

242 self.assertFalse(self.butler.exists("intermediate")) 

243 self.assertEqual(self.butler.get("output").storage_class, get_mock_name("StructuredDataDict")) 

244 prov = qpg.QuantumProvenanceGraph(self.butler, [executor.quantum_graph], read_caveats="exhaustive") 

245 (quantum_key_a,) = prov.quanta["a"] 

246 quantum_info_a = prov.get_quantum_info(quantum_key_a) 

247 _, quantum_run_a = qpg.QuantumRun.find_final(quantum_info_a) 

248 self.assertEqual( 

249 quantum_run_a.caveats, 

250 QuantumSuccessCaveats.ALL_OUTPUTS_MISSING 

251 | QuantumSuccessCaveats.ANY_OUTPUTS_MISSING 

252 | QuantumSuccessCaveats.PARTIAL_OUTPUTS_ERROR, 

253 ) 

254 self.assertEqual( 

255 quantum_run_a.exception.type_name, 

256 "lsst.pipe.base.tests.mocks.MockAlgorithmError", 

257 ) 

258 self.assertEqual( 

259 quantum_run_a.exception.metadata, 

260 {"badness": 12}, 

261 ) 

262 (quantum_key_b,) = prov.quanta["b"] 

263 quantum_info_b = prov.get_quantum_info(quantum_key_b) 

264 _, quantum_run_b = qpg.QuantumRun.find_final(quantum_info_b) 

265 self.assertEqual(quantum_run_b.caveats, QuantumSuccessCaveats.NO_CAVEATS) 

266 prov_summary = prov.to_summary(self.butler) 

267 # One partial-outputs case, with an empty data ID: 

268 self.assertEqual(prov_summary.tasks["a"].caveats, {"*P": [{}]}) 

269 self.assertEqual( 

270 prov_summary.tasks["a"].exceptions.keys(), {"lsst.pipe.base.tests.mocks.MockAlgorithmError"} 

271 ) 

272 self.assertEqual( 

273 prov_summary.tasks["a"] 

274 .exceptions["lsst.pipe.base.tests.mocks.MockAlgorithmError"][0] 

275 .exception.metadata, 

276 {"badness": 12}, 

277 ) 

278 # No caveats for the second task, since it didn't need the first task's 

279 # output anyway. 

280 self.assertEqual(prov_summary.tasks["b"].caveats, {}) 

281 self.assertEqual(prov_summary.tasks["b"].exceptions, {}) 

282 # Check table forms for summaries of the same information. 

283 quantum_table = prov_summary.make_quantum_table() 

284 self.assertEqual(list(quantum_table["Task"]), ["a", "b"]) 

285 self.assertEqual(list(quantum_table["Unknown"]), [0, 0]) 

286 self.assertEqual(list(quantum_table["Successful"]), [1, 1]) 

287 self.assertEqual(list(quantum_table["Caveats"]), ["*P(1)", ""]) 

288 self.assertEqual(list(quantum_table["Blocked"]), [0, 0]) 

289 self.assertEqual(list(quantum_table["Failed"]), [0, 0]) 

290 self.assertEqual(list(quantum_table["Wonky"]), [0, 0]) 

291 self.assertEqual(list(quantum_table["TOTAL"]), [1, 1]) 

292 self.assertEqual(list(quantum_table["EXPECTED"]), [1, 1]) 

293 dataset_table = prov_summary.make_dataset_table() 

294 self.assertEqual( 

295 list(dataset_table["Dataset"]), 

296 ["intermediate", "a_metadata", "a_log", "output", "b_metadata", "b_log"], 

297 ) 

298 self.assertEqual(list(dataset_table["Visible"]), [0, 1, 1, 1, 1, 1]) 

299 self.assertEqual(list(dataset_table["Shadowed"]), [0, 0, 0, 0, 0, 0]) 

300 self.assertEqual(list(dataset_table["Predicted Only"]), [1, 0, 0, 0, 0, 0]) 

301 self.assertEqual(list(dataset_table["Unsuccessful"]), [0, 0, 0, 0, 0, 0]) 

302 self.assertEqual(list(dataset_table["Cursed"]), [0, 0, 0, 0, 0, 0]) 

303 self.assertEqual(list(dataset_table["TOTAL"]), [1, 1, 1, 1, 1, 1]) 

304 self.assertEqual(list(dataset_table["EXPECTED"]), [1, 1, 1, 1, 1, 1]) 

305 exception_table = prov_summary.make_exception_table() 

306 self.assertEqual(list(exception_table["Task"]), ["a"]) 

307 self.assertEqual( 

308 list(exception_table["Exception"]), ["lsst.pipe.base.tests.mocks.MockAlgorithmError"] 

309 ) 

310 self.assertEqual(list(exception_table["Count"]), [1]) 

311 bad_quantum_tables = prov_summary.make_bad_quantum_tables() 

312 self.assertEqual(bad_quantum_tables.keys(), {"a"}) 

313 self.assertEqual(list(bad_quantum_tables["a"]["Status(Caveats)"]), ["SUCCESSFUL(P)"]) 

314 self.assertEqual(list(bad_quantum_tables["a"]["Exception"]), ["MockAlgorithmError"]) 

315 self.assertFalse(prov_summary.make_bad_dataset_tables()) 

316 

317 def test_no_work_found(self): 

318 """Test executing two quanta where the first raises 

319 `NoWorkFound` in `runQuantum`, leading the next to raise `NoWorkFound` 

320 in `adjustQuantum`. 

321 """ 

322 config_a = DynamicTestPipelineTaskConfig() 

323 config_a.inputs["i"] = DynamicConnectionConfig( 

324 dataset_type_name="input", storage_class="StructuredDataDict", mock_storage_class=False 

325 ) 

326 config_a.fail_exception = "lsst.pipe.base.NoWorkFound" 

327 config_a.fail_condition = "1=1" # butler query expression that is true 

328 config_a.outputs["o"] = DynamicConnectionConfig( 

329 dataset_type_name="intermediate", storage_class="StructuredDataDict" 

330 ) 

331 config_b = DynamicTestPipelineTaskConfig() 

332 config_b.inputs["i"] = DynamicConnectionConfig( 

333 dataset_type_name="intermediate", storage_class="StructuredDataDict" 

334 ) 

335 config_b.outputs["o"] = DynamicConnectionConfig( 

336 dataset_type_name="output", storage_class="StructuredDataDict" 

337 ) 

338 pipeline_graph = PipelineGraph() 

339 pipeline_graph.add_task("a", DynamicTestPipelineTask, config_a) 

340 pipeline_graph.add_task("b", DynamicTestPipelineTask, config_b) 

341 # Consider the partial a success and proceed. 

342 executor = SimplePipelineExecutor.from_pipeline_graph( 

343 pipeline_graph, butler=self.butler, raise_on_partial_outputs=False 

344 ) 

345 (_, _) = executor.as_generator(register_dataset_types=True) 

346 prov = qpg.QuantumProvenanceGraph() 

347 prov.assemble_quantum_provenance_graph(self.butler, [executor.quantum_graph]) 

348 (quantum_key_a,) = prov.quanta["a"] 

349 quantum_info_a = prov.get_quantum_info(quantum_key_a) 

350 _, quantum_run_a = qpg.QuantumRun.find_final(quantum_info_a) 

351 self.assertEqual( 

352 quantum_run_a.caveats, 

353 QuantumSuccessCaveats.ALL_OUTPUTS_MISSING 

354 | QuantumSuccessCaveats.ANY_OUTPUTS_MISSING 

355 | QuantumSuccessCaveats.NO_WORK, 

356 ) 

357 (quantum_key_b,) = prov.quanta["b"] 

358 quantum_info_b = prov.get_quantum_info(quantum_key_b) 

359 _, quantum_run_b = qpg.QuantumRun.find_final(quantum_info_b) 

360 self.assertEqual( 

361 quantum_run_b.caveats, 

362 QuantumSuccessCaveats.ALL_OUTPUTS_MISSING 

363 | QuantumSuccessCaveats.ANY_OUTPUTS_MISSING 

364 | QuantumSuccessCaveats.ADJUST_QUANTUM_RAISED 

365 | QuantumSuccessCaveats.NO_WORK, 

366 ) 

367 prov_summary = prov.to_summary(self.butler) 

368 # One NoWorkFound, raised by runQuantum, with an empty data ID: 

369 self.assertEqual(prov_summary.tasks["a"].caveats, {"*N": [{}]}) 

370 self.assertEqual(prov_summary.tasks["a"].exceptions, {}) 

371 # One NoWorkFound, raised by adjustQuantum, with an empty data ID. 

372 self.assertEqual(prov_summary.tasks["b"].caveats, {"*A": [{}]}) 

373 self.assertEqual(prov_summary.tasks["b"].exceptions, {}) 

374 # Check table forms for summaries of the same information. 

375 quantum_table = prov_summary.make_quantum_table() 

376 self.assertEqual(list(quantum_table["Task"]), ["a", "b"]) 

377 self.assertEqual(list(quantum_table["Unknown"]), [0, 0]) 

378 self.assertEqual(list(quantum_table["Successful"]), [1, 1]) 

379 self.assertEqual(list(quantum_table["Caveats"]), ["*N(1)", "*A(1)"]) 

380 self.assertEqual(list(quantum_table["Blocked"]), [0, 0]) 

381 self.assertEqual(list(quantum_table["Failed"]), [0, 0]) 

382 self.assertEqual(list(quantum_table["Wonky"]), [0, 0]) 

383 self.assertEqual(list(quantum_table["TOTAL"]), [1, 1]) 

384 self.assertEqual(list(quantum_table["EXPECTED"]), [1, 1]) 

385 dataset_table = prov_summary.make_dataset_table() 

386 self.assertEqual( 

387 list(dataset_table["Dataset"]), 

388 ["intermediate", "a_metadata", "a_log", "output", "b_metadata", "b_log"], 

389 ) 

390 self.assertEqual(list(dataset_table["Visible"]), [0, 1, 1, 0, 1, 1]) 

391 self.assertEqual(list(dataset_table["Shadowed"]), [0, 0, 0, 0, 0, 0]) 

392 self.assertEqual(list(dataset_table["Predicted Only"]), [1, 0, 0, 1, 0, 0]) 

393 self.assertEqual(list(dataset_table["Unsuccessful"]), [0, 0, 0, 0, 0, 0]) 

394 self.assertEqual(list(dataset_table["Cursed"]), [0, 0, 0, 0, 0, 0]) 

395 self.assertEqual(list(dataset_table["TOTAL"]), [1, 1, 1, 1, 1, 1]) 

396 self.assertEqual(list(dataset_table["EXPECTED"]), [1, 1, 1, 1, 1, 1]) 

397 self.assertFalse(prov_summary.make_exception_table()) 

398 self.assertFalse(prov_summary.make_bad_quantum_tables()) 

399 self.assertFalse(prov_summary.make_bad_dataset_tables()) 

400 

401 def test_partial_outputs_failure(self): 

402 """Test executing two quanta where the first raises 

403 `lsst.pipe.base.AnnotatedPartialOutputsError` and its output is an 

404 optional input to the second, while configuring the executor to 

405 consider this a failure. 

406 """ 

407 config_a = DynamicTestPipelineTaskConfig() 

408 config_a.inputs["i"] = DynamicConnectionConfig( 

409 dataset_type_name="input", storage_class="StructuredDataDict", mock_storage_class=False 

410 ) 

411 config_a.fail_exception = "lsst.pipe.base.AnnotatedPartialOutputsError" 

412 config_a.fail_condition = "1=1" # butler query expression that is true 

413 config_a.outputs["o"] = DynamicConnectionConfig( 

414 dataset_type_name="intermediate", storage_class="StructuredDataDict" 

415 ) 

416 config_b = DynamicTestPipelineTaskConfig() 

417 config_b.inputs["i"] = DynamicConnectionConfig( 

418 dataset_type_name="intermediate", storage_class="StructuredDataDict", minimum=0 

419 ) 

420 config_b.outputs["o"] = DynamicConnectionConfig( 

421 dataset_type_name="output", storage_class="StructuredDataDict" 

422 ) 

423 pipeline_graph = PipelineGraph() 

424 pipeline_graph.add_task("a", DynamicTestPipelineTask, config_a) 

425 pipeline_graph.add_task("b", DynamicTestPipelineTask, config_b) 

426 executor = SimplePipelineExecutor.from_pipeline_graph( 

427 pipeline_graph, 

428 butler=self.butler, 

429 raise_on_partial_outputs=True, 

430 ) 

431 # The executor should raise the chained exception 

432 # (RepeatableQuantumError, since that's what the mocking system in 

433 # pipe_base uses here), not AnnotatedPartialOutputsError. 

434 with self.assertRaises(RepeatableQuantumError): 

435 executor.run(register_dataset_types=True) 

436 self.assertFalse(self.butler.exists("intermediate")) 

437 self.assertFalse(self.butler.exists("output")) 

438 prov = qpg.QuantumProvenanceGraph() 

439 prov.assemble_quantum_provenance_graph(self.butler, [executor.quantum_graph]) 

440 (quantum_key_a,) = prov.quanta["a"] 

441 quantum_info_a = prov.get_quantum_info(quantum_key_a) 

442 _, quantum_run_a = qpg.QuantumRun.find_final(quantum_info_a) 

443 self.assertEqual(quantum_run_a.status, qpg.QuantumRunStatus.FAILED) 

444 self.assertIsNone(quantum_run_a.caveats) 

445 self.assertIsNone(quantum_run_a.exception) 

446 (quantum_key_b,) = prov.quanta["b"] 

447 quantum_info_b = prov.get_quantum_info(quantum_key_b) 

448 self.assertEqual(quantum_info_b["status"], qpg.QuantumInfoStatus.BLOCKED) 

449 prov_summary = prov.to_summary(self.butler) 

450 # One partial-outputs failure case for the first task. 

451 self.assertEqual(prov_summary.tasks["a"].n_failed, 1) 

452 # No direct failures, but one blocked for the second 

453 self.assertEqual(prov_summary.tasks["b"].n_failed, 0) 

454 self.assertEqual(prov_summary.tasks["b"].n_blocked, 1) 

455 # Check table forms for summaries of the same information. 

456 quantum_table = prov_summary.make_quantum_table() 

457 self.assertEqual(list(quantum_table["Task"]), ["a", "b"]) 

458 self.assertEqual(list(quantum_table["Unknown"]), [0, 0]) 

459 self.assertEqual(list(quantum_table["Successful"]), [0, 0]) 

460 self.assertEqual(list(quantum_table["Caveats"]), ["", ""]) 

461 self.assertEqual(list(quantum_table["Blocked"]), [0, 1]) 

462 self.assertEqual(list(quantum_table["Failed"]), [1, 0]) 

463 self.assertEqual(list(quantum_table["Wonky"]), [0, 0]) 

464 self.assertEqual(list(quantum_table["TOTAL"]), [1, 1]) 

465 self.assertEqual(list(quantum_table["EXPECTED"]), [1, 1]) 

466 dataset_table = prov_summary.make_dataset_table() 

467 self.assertEqual( 

468 list(dataset_table["Dataset"]), 

469 ["intermediate", "a_metadata", "a_log", "output", "b_metadata", "b_log"], 

470 ) 

471 # Note that a_log is UNSUCCESSFUL, not VISIBLE, despite being present 

472 # in butler because those categories are mutually exclusive and we 

473 # don't want to consider any outputs of failed quanta to be successful. 

474 self.assertEqual(list(dataset_table["Visible"]), [0, 0, 0, 0, 0, 0]) 

475 self.assertEqual(list(dataset_table["Shadowed"]), [0, 0, 0, 0, 0, 0]) 

476 self.assertEqual(list(dataset_table["Predicted Only"]), [0, 0, 0, 0, 0, 0]) 

477 self.assertEqual(list(dataset_table["Unsuccessful"]), [1, 1, 1, 1, 1, 1]) 

478 self.assertEqual(list(dataset_table["Cursed"]), [0, 0, 0, 0, 0, 0]) 

479 self.assertEqual(list(dataset_table["TOTAL"]), [1, 1, 1, 1, 1, 1]) 

480 self.assertEqual(list(dataset_table["EXPECTED"]), [1, 1, 1, 1, 1, 1]) 

481 self.assertFalse(prov_summary.make_exception_table()) 

482 bad_quantum_tables = prov_summary.make_bad_quantum_tables() 

483 self.assertEqual(bad_quantum_tables.keys(), {"a"}) 

484 self.assertEqual(list(bad_quantum_tables["a"]["Status(Caveats)"]), ["FAILED"]) 

485 self.assertEqual(list(bad_quantum_tables["a"]["Exception"]), [""]) 

486 self.assertFalse(prov_summary.make_bad_dataset_tables()) 

487 

488 def test_existence_check_skips(self): 

489 """Test that pre-execution existence checks are not performed for 

490 overall-input datasets, as this those checks could otherwise mask 

491 repository configuration problems or downtime as NoWorkFound cases. 

492 """ 

493 # First we configure and execute task A, which is just a way to get a 

494 # MockDataset in the repo for us to play with; the important test can't 

495 # use the non-mock 'input' dataset because the mock runQuantum only 

496 # actually reads MockDatasets. 

497 config_a = DynamicTestPipelineTaskConfig() 

498 config_a.inputs["i"] = DynamicConnectionConfig( 

499 dataset_type_name="input", storage_class="StructuredDataDict", mock_storage_class=False 

500 ) 

501 config_a.outputs["o"] = DynamicConnectionConfig( 

502 dataset_type_name="intermediate", storage_class="StructuredDataDict" 

503 ) 

504 executor_a = SimplePipelineExecutor.from_task_class( 

505 DynamicTestPipelineTask, 

506 config=config_a, 

507 butler=self.butler, 

508 label="a", 

509 ) 

510 executor_a.run(register_dataset_types=True) 

511 # Now we can do the real test. 

512 config_b = DynamicTestPipelineTaskConfig() 

513 config_b.inputs["i"] = DynamicConnectionConfig( 

514 dataset_type_name="intermediate", storage_class="StructuredDataDict", minimum=0 

515 ) 

516 config_b.outputs["o"] = DynamicConnectionConfig( 

517 dataset_type_name="output", storage_class="StructuredDataDict" 

518 ) 

519 butler = self.butler.clone(run="new_run") 

520 executor_b = SimplePipelineExecutor.from_task_class( 

521 DynamicTestPipelineTask, 

522 config=config_b, 

523 butler=butler, 

524 label="b", 

525 attach_datastore_records=True, 

526 ) 

527 # Delete the input dataset after the QG has already been built. 

528 intermediate_refs = butler.query_datasets("intermediate") 

529 self.assertEqual(len(intermediate_refs), 1) 

530 butler.pruneDatasets(intermediate_refs, purge=True, unstore=True) 

531 with self.assertRaises(FileNotFoundError): 

532 # We should get an exception rather than NoWorkFound, because for 

533 # this single-task pipeline, the missing dataset is an 

534 # overall-input (name notwithstanding). 

535 executor_b.run(register_dataset_types=True) 

536 

537 

538class MemoryTester(lsst.utils.tests.MemoryTestCase): 

539 """Generic tests for file leaks.""" 

540 

541 

542def setup_module(module): 

543 """Set up the module for pytest. 

544 

545 Parameters 

546 ---------- 

547 module : `~types.ModuleType` 

548 Module to set up. 

549 """ 

550 lsst.utils.tests.init() 

551 

552 

553if __name__ == "__main__": 

554 lsst.utils.tests.init() 

555 unittest.main()