Coverage for tests/test_cliCmdQueryCollections.py: 100%
115 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-27 09:25 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-27 09:25 +0000
1# This file is part of daf_butler.
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/>.
28"""Unit tests for daf_butler CLI query-collections command."""
30import unittest
32from astropy.table import Table
33from numpy import array
35from lsst.daf.butler import Butler, CollectionType
36from lsst.daf.butler.cli.butler import cli
37from lsst.daf.butler.cli.cmd import query_collections
38from lsst.daf.butler.cli.utils import LogCliRunner, clickResultMsg
39from lsst.daf.butler.script import queryCollections
40from lsst.daf.butler.tests import CliCmdTestBase, DatastoreMock
41from lsst.daf.butler.tests._repo_template_cache import make_repo_for_test
42from lsst.daf.butler.tests.utils import ButlerTestHelper, readTable
45class QueryCollectionsCmdTest(CliCmdTestBase, unittest.TestCase):
46 """Test the query-collections command-line."""
48 mockFuncName = "lsst.daf.butler.cli.cmd.commands.script.queryCollections"
50 @staticmethod
51 def defaultExpected():
52 return dict(
53 repo=None, collection_type=tuple(CollectionType.__members__.values()), chains="TABLE", glob=()
54 )
56 @staticmethod
57 def command():
58 return query_collections
60 def test_minimal(self):
61 """Test only required parameters, and omit optional parameters."""
62 self.run_test(
63 ["query-collections", "here", "--chains", "TABLE"],
64 self.makeExpected(
65 repo="here",
66 show_dataset_types=False,
67 exclude_dataset_types=("*_config", "*_log", "*_metadata", "packages"),
68 ),
69 )
71 def test_all(self):
72 """Test all parameters"""
73 self.run_test(
74 [
75 "query-collections",
76 "here",
77 "foo*",
78 "--collection-type",
79 "TAGGED",
80 "--collection-type",
81 "RUN",
82 "--chains",
83 "TABLE",
84 ],
85 self.makeExpected(
86 repo="here",
87 glob=("foo*",),
88 collection_type=(CollectionType.TAGGED, CollectionType.RUN),
89 chains="TABLE",
90 show_dataset_types=False,
91 exclude_dataset_types=("*_config", "*_log", "*_metadata", "packages"),
92 ),
93 )
95 def test_show_dataset_types(self):
96 """Test the --show-dataset-types option."""
97 self.run_test(
98 ["query-collections", "here", "--chains", "TABLE", "--show-dataset-types"],
99 self.makeExpected(
100 repo="here",
101 chains="TABLE",
102 show_dataset_types=True,
103 exclude_dataset_types=("*_config", "*_log", "*_metadata", "packages"),
104 ),
105 )
106 self.run_test(
107 ["query-collections", "here", "--chains", "TREE", "--show-dataset-types"],
108 self.makeExpected(
109 repo="here",
110 chains="TREE",
111 show_dataset_types=True,
112 exclude_dataset_types=("*_config", "*_log", "*_metadata", "packages"),
113 ),
114 )
115 self.run_test(
116 ["query-collections", "here", "--chains", "FLATTEN", "--show-dataset-types"],
117 self.makeExpected(
118 repo="here",
119 chains="FLATTEN",
120 show_dataset_types=True,
121 exclude_dataset_types=("*_config", "*_log", "*_metadata", "packages"),
122 ),
123 )
125 def test_exclude_dataset_types(self):
126 """Test the --exclude-dataset-types option."""
127 self.run_test(
128 [
129 "query-collections",
130 "here",
131 "--chains",
132 "TABLE",
133 "--show-dataset-types",
134 "--exclude-dataset-types",
135 "flat",
136 ],
137 self.makeExpected(
138 repo="here",
139 show_dataset_types=True,
140 exclude_dataset_types=("flat",),
141 ),
142 )
145class QueryCollectionsScriptTest(ButlerTestHelper, unittest.TestCase):
146 """Test the query-collections script interface."""
148 def setUp(self):
149 self.runner = LogCliRunner()
151 def testGetCollections(self):
152 run = "ingest/run"
153 tag = "tag"
154 with self.runner.isolated_filesystem():
155 butlerCfg = make_repo_for_test("here")
156 # the purpose of this call is to create some collections
157 butler = Butler.from_config(butlerCfg, run=run, collections=[tag], writeable=True)
158 self.enterContext(butler)
159 butler.registry.registerCollection(tag, CollectionType.TAGGED)
161 # Verify collections that were created are found by
162 # query-collections.
163 result = self.runner.invoke(cli, ["query-collections", "here"])
164 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
165 expected = Table((("ingest/run", "tag"), ("RUN", "TAGGED")), names=("Name", "Type"))
166 self.assertAstropyTablesEqual(readTable(result.output), expected)
168 # Verify that with a glob argument, that only collections whose
169 # name matches with the specified pattern are returned.
170 result = self.runner.invoke(cli, ["query-collections", "here", "t*"])
171 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
172 expected = Table((("tag",), ("TAGGED",)), names=("Name", "Type"))
173 self.assertAstropyTablesEqual(readTable(result.output), expected)
175 # Verify that with a collection type argument, only collections of
176 # that type are returned.
177 result = self.runner.invoke(cli, ["query-collections", "here", "--collection-type", "RUN"])
178 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
179 expected = Table((("ingest/run",), ("RUN",)), names=("Name", "Type"))
180 self.assertAstropyTablesEqual(readTable(result.output), expected)
183class ChainedCollectionsTest(ButlerTestHelper, unittest.TestCase):
184 """Test the collection-chain command-line interface."""
186 def setUp(self):
187 self.runner = LogCliRunner()
189 def assertChain(self, args: list[str], expected: str):
190 """Run collection-chain and check the expected result"""
191 result = self.runner.invoke(cli, ["collection-chain", "here", *args])
192 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
193 self.assertEqual(result.output.strip(), expected, clickResultMsg(result))
195 def testChained(self):
196 with self.runner.isolated_filesystem():
197 # Create a butler and add some chained collections:
198 butlerCfg = make_repo_for_test("here")
200 butler1 = Butler.from_config(butlerCfg, writeable=True)
201 self.enterContext(butler1)
203 # Replace datastore functions with mocks:
204 DatastoreMock.apply(butler1)
206 butler1.import_(filename="resource://lsst.daf.butler/tests/registry_data/base.yaml")
207 butler1.import_(filename="resource://lsst.daf.butler/tests/registry_data/datasets.yaml")
208 registry1 = butler1.registry
209 registry1.registerRun("run1")
210 registry1.registerCollection("tag1", CollectionType.TAGGED)
211 registry1.registerCollection("calibration1", CollectionType.CALIBRATION)
213 # Create the collection chain
214 self.assertChain(["chain2", "calibration1", "run1"], "[calibration1, run1]")
215 self.assertChain(
216 ["--mode", "redefine", "chain1", "tag1", "run1", "chain2"], "[tag1, run1, chain2]"
217 )
219 # Use the script function to test the query-collections TREE
220 # option, because the astropy.table.Table.read method, which we are
221 # using for verification elsewhere in this file, seems to strip
222 # leading whitespace from columns. This makes it impossible to test
223 # the nested TREE output of the query-collections subcommand from
224 # the command line interface.
225 table = queryCollections("here", glob=(), collection_type=CollectionType.all(), chains="TREE")
227 expected = Table(
228 array(
229 (
230 ("calibration1", "CALIBRATION"),
231 ("chain1", "CHAINED"),
232 (" tag1", "TAGGED"),
233 (" run1", "RUN"),
234 (" chain2", "CHAINED"),
235 (" calibration1", "CALIBRATION"),
236 (" run1", "RUN"),
237 ("chain2", "CHAINED"),
238 (" calibration1", "CALIBRATION"),
239 (" run1", "RUN"),
240 ("imported_g", "RUN"),
241 ("imported_r", "RUN"),
242 ("run1", "RUN"),
243 ("tag1", "TAGGED"),
244 )
245 ),
246 names=("Name", "Type"),
247 )
248 self.assertAstropyTablesEqual(table, expected)
250 # Test table with inverse == True
251 table = queryCollections(
252 "here",
253 glob=(),
254 collection_type=CollectionType.all(),
255 chains="INVERSE-TREE",
256 )
257 expected = Table(
258 array(
259 (
260 ("calibration1", "CALIBRATION"),
261 (" chain2", "CHAINED"),
262 (" chain1", "CHAINED"),
263 ("chain1", "CHAINED"),
264 ("chain2", "CHAINED"),
265 (" chain1", "CHAINED"),
266 ("imported_g", "RUN"),
267 ("imported_r", "RUN"),
268 ("run1", "RUN"),
269 (" chain1", "CHAINED"),
270 (" chain2", "CHAINED"),
271 (" chain1", "CHAINED"),
272 ("tag1", "TAGGED"),
273 (" chain1", "CHAINED"),
274 )
275 ),
276 names=("Name", "Type"),
277 )
278 self.assertAstropyTablesEqual(table, expected)
280 # Test table with show_dataset_types == True
281 table = queryCollections(
282 "here",
283 glob=(),
284 collection_type=CollectionType.all(),
285 chains="TREE",
286 show_dataset_types=True,
287 )
288 expected = Table(
289 array(
290 (
291 ("calibration1", "CALIBRATION", ""),
292 ("chain1", "CHAINED", ""),
293 (" tag1", "TAGGED", ""),
294 (" run1", "RUN", ""),
295 (" chain2", "CHAINED", ""),
296 (" calibration1", "CALIBRATION", ""),
297 (" run1", "RUN", ""),
298 ("chain2", "CHAINED", ""),
299 (" calibration1", "CALIBRATION", ""),
300 (" run1", "RUN", ""),
301 ("imported_g", "RUN", "bias"),
302 ("", "", "flat"),
303 ("imported_r", "RUN", "bias"),
304 ("", "", "flat"),
305 ("run1", "RUN", ""),
306 ("tag1", "TAGGED", ""),
307 )
308 ),
309 names=("Name", "Type", "Dataset Types"),
310 )
311 self.assertAstropyTablesEqual(table, expected)
313 # Test table with show_dataset_types, excluding some dataset types
314 table = queryCollections(
315 "here",
316 glob=(),
317 collection_type=CollectionType.all(),
318 chains="TREE",
319 show_dataset_types=True,
320 exclude_dataset_types=("bias",),
321 )
322 expected = Table(
323 array(
324 (
325 ("calibration1", "CALIBRATION", ""),
326 ("chain1", "CHAINED", ""),
327 (" tag1", "TAGGED", ""),
328 (" run1", "RUN", ""),
329 (" chain2", "CHAINED", ""),
330 (" calibration1", "CALIBRATION", ""),
331 (" run1", "RUN", ""),
332 ("chain2", "CHAINED", ""),
333 (" calibration1", "CALIBRATION", ""),
334 (" run1", "RUN", ""),
335 ("imported_g", "RUN", "flat"),
336 ("imported_r", "RUN", "flat"),
337 ("run1", "RUN", ""),
338 ("tag1", "TAGGED", ""),
339 )
340 ),
341 names=("Name", "Type", "Dataset Types"),
342 )
343 self.assertAstropyTablesEqual(table, expected)
345 result = self.runner.invoke(cli, ["query-collections", "here", "--chains", "TABLE"])
346 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
347 expected = Table(
348 array(
349 (
350 ("calibration1", "CALIBRATION", ""),
351 ("chain1", "CHAINED", "tag1"),
352 ("", "", "run1"),
353 ("", "", "chain2"),
354 ("chain2", "CHAINED", "calibration1"),
355 ("", "", "run1"),
356 ("imported_g", "RUN", ""),
357 ("imported_r", "RUN", ""),
358 ("run1", "RUN", ""),
359 ("tag1", "TAGGED", ""),
360 )
361 ),
362 names=("Name", "Type", "Children"),
363 )
364 table = readTable(result.output)
365 self.assertAstropyTablesEqual(readTable(result.output), expected)
367 result = self.runner.invoke(cli, ["query-collections", "here", "--chains", "INVERSE-TABLE"])
368 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
369 expected = Table(
370 array(
371 (
372 ("calibration1", "CALIBRATION", "chain2"),
373 ("chain1", "CHAINED", ""),
374 ("chain2", "CHAINED", "chain1"),
375 ("imported_g", "RUN", ""),
376 ("imported_r", "RUN", ""),
377 ("run1", "RUN", "chain1"),
378 ("", "", "chain2"),
379 ("tag1", "TAGGED", "chain1"),
380 )
381 ),
382 names=("Name", "Type", "Parents"),
383 )
384 table = readTable(result.output)
385 self.assertAstropyTablesEqual(readTable(result.output), expected)
387 result = self.runner.invoke(cli, ["query-collections", "here", "--chains", "FLATTEN"])
388 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
389 expected = Table(
390 array(
391 (
392 ("calibration1", "CALIBRATION"),
393 ("imported_g", "RUN"),
394 ("imported_r", "RUN"),
395 ("run1", "RUN"),
396 ("tag1", "TAGGED"),
397 )
398 ),
399 names=("Name", "Type"),
400 )
401 self.assertAstropyTablesEqual(readTable(result.output), expected, unorderedRows=True)
403 result = self.runner.invoke(cli, ["query-collections", "here", "--chains", "NO-CHILDREN"])
404 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
405 expected = Table(
406 array(
407 (
408 ("calibration1", "CALIBRATION"),
409 ("chain1", "CHAINED"),
410 ("chain2", "CHAINED"),
411 ("imported_g", "RUN"),
412 ("imported_r", "RUN"),
413 ("run1", "RUN"),
414 ("tag1", "TAGGED"),
415 )
416 ),
417 names=("Name", "Type"),
418 )
419 self.assertAstropyTablesEqual(readTable(result.output), expected, unorderedRows=True)
421 # Add a couple more run collections for chain testing
422 registry1.registerRun("run2")
423 registry1.registerRun("run3")
424 registry1.registerRun("run4")
426 self.assertChain(["--mode", "pop", "chain1"], "[run1, chain2]")
428 self.assertChain(["--mode", "extend", "chain1", "run2", "run3"], "[run1, chain2, run2, run3]")
430 self.assertChain(["--mode", "remove", "chain1", "chain2", "run2"], "[run1, run3]")
432 self.assertChain(["--mode", "prepend", "chain1", "chain2", "run2"], "[chain2, run2, run1, run3]")
434 self.assertChain(["--mode", "pop", "chain1", "1", "3"], "[chain2, run1]")
436 self.assertChain(
437 ["--mode", "redefine", "chain1", "chain2", "run2", "run3,run4", "--flatten"],
438 "[calibration1, run1, run2, run3, run4]",
439 )
441 self.assertChain(["--mode", "pop", "chain1", "--", "-1", "-3"], "[calibration1, run1, run3]")
443 # Out-of-bounds index
444 result = self.runner.invoke(cli, ["collection-chain", "here", "--mode", "pop", "chain1", "10"])
445 self.assertEqual(result.exit_code, 1)
448if __name__ == "__main__":
449 unittest.main()