Coverage for tests/test_cliUtilSplitKv.py: 16%
140 statements
« prev ^ index » next coverage.py v7.4.1, created at 2024-02-13 10:57 +0000
« prev ^ index » next coverage.py v7.4.1, created at 2024-02-13 10:57 +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 the daf_butler shared CLI options.
29"""
31import unittest
32from functools import partial
33from unittest.mock import MagicMock
35import click
36from lsst.daf.butler.cli.utils import LogCliRunner, clickResultMsg, split_kv
39class SplitKvTestCase(unittest.TestCase):
40 """Tests that call split_kv directly."""
42 def test_single_dict(self):
43 """Test that a single kv pair converts to a dict."""
44 self.assertEqual(split_kv("context", "param", "first=1"), {"first": "1"})
46 def test_single_tuple(self):
47 """Test that a single kv pair converts to a tuple when
48 return_type=tuple.
49 """
50 self.assertEqual(split_kv("context", "param", "first=1", return_type=tuple), (("first", "1"),))
52 def test_multiple_dict(self):
53 """Test that multiple comma separated kv pairs convert to a dict."""
54 self.assertEqual(split_kv("context", "param", "first=1,second=2"), {"first": "1", "second": "2"})
56 def test_multiple_tuple(self):
57 """Test that multiple comma separated kv pairs convert to a tuple when
58 return_type=tuple.
59 """
60 self.assertEqual(
61 split_kv("context", "param", "first=1,second=2", return_type=tuple),
62 (("first", "1"), ("second", "2")),
63 )
65 def test_unseparated(self):
66 """Test that a value without a key converts to a kv pair with an empty
67 string key.
68 """
69 self.assertEqual(
70 split_kv("context", "param", "first,second=2", unseparated_okay=True),
71 {"": "first", "second": "2"},
72 )
74 def test_notMultiple(self):
75 """Test that multiple values are rejected if multiple=False."""
76 with self.assertRaisesRegex(
77 click.ClickException,
78 "Could not parse key-value pair "
79 "'first=1,second=2' using separator '=', with multiple values not "
80 "allowed.",
81 ):
82 split_kv("context", "param", "first=1,second=2", multiple=False)
84 def test_wrongSeparator(self):
85 """Test that an input with the wrong separator raises."""
86 with self.assertRaises(click.ClickException):
87 split_kv("context", "param", "first-1")
89 def test_missingSeparator(self):
90 """Test that an input with no separator raises when
91 unseparated_okay=False (this is the default value).
92 """
93 with self.assertRaises(click.ClickException):
94 split_kv("context", "param", "first 1")
96 def test_unseparatedOkay(self):
97 """Test that that the default key is used for values without a
98 separator when unseparated_okay=True.
99 """
100 self.assertEqual(split_kv("context", "param", "foo", unseparated_okay=True), {"": "foo"})
102 def test_unseparatedOkay_list(self):
103 """Test that that the default key is used for values without a
104 separator when unseparated_okay=True and the return_type is tuple.
105 """
106 self.assertEqual(
107 split_kv("context", "param", "foo,bar", unseparated_okay=True, return_type=tuple),
108 (("", "foo"), ("", "bar")),
109 )
111 def test_unseparatedOkay_defaultKey(self):
112 """Test that that the default key can be set and is used for values
113 without a separator when unseparated_okay=True.
114 """
115 self.assertEqual(
116 split_kv("context", "param", "foo", unseparated_okay=True, default_key=...), {...: "foo"}
117 )
119 def test_dashSeparator(self):
120 """Test that specifying a separator is accepted and converts arguments
121 to a dict.
122 """
123 self.assertEqual(
124 split_kv("context", "param", "first-1,second-2", separator="-"), {"first": "1", "second": "2"}
125 )
127 def test_reverseKv(self):
128 self.assertEqual(
129 split_kv(
130 "context",
131 "param",
132 "first=1,second",
133 unseparated_okay=True,
134 default_key="key",
135 reverse_kv=True,
136 ),
137 {"1": "first", "second": "key"},
138 )
140 def test_invalidResultType(self):
141 with self.assertRaises(click.ClickException):
142 split_kv(
143 "context",
144 "param",
145 "first=1,second=2",
146 return_type=set,
147 )
150class SplitKvCmdTestCase(unittest.TestCase):
151 """Tests using split_kv with a command."""
153 def setUp(self):
154 self.runner = LogCliRunner()
156 def test_cli(self):
157 mock = MagicMock()
159 @click.command()
160 @click.option("--value", callback=split_kv, multiple=True)
161 def cli(value):
162 mock(value)
164 result = self.runner.invoke(cli, ["--value", "first=1"])
165 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result))
166 mock.assert_called_with({"first": "1"})
168 result = self.runner.invoke(cli, ["--value", "first=1,second=2"])
169 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result))
170 mock.assert_called_with({"first": "1", "second": "2"})
172 result = self.runner.invoke(cli, ["--value", "first=1", "--value", "second=2"])
173 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result))
174 mock.assert_called_with({"first": "1", "second": "2"})
176 # double separator "==" should fail:
177 result = self.runner.invoke(cli, ["--value", "first==1"])
178 self.assertEqual(result.exit_code, 1)
179 self.assertEqual(
180 result.output,
181 "Error: Could not parse key-value pair 'first==1' using separator '=', with "
182 "multiple values allowed: too many values to unpack (expected 2)\n",
183 )
185 def test_choice(self):
186 choices = ["FOO", "BAR", "BAZ"]
187 mock = MagicMock()
189 @click.command()
190 @click.option(
191 "--metasyntactic-var",
192 callback=partial(
193 split_kv,
194 unseparated_okay=True,
195 choice=click.Choice(choices=choices, case_sensitive=False),
196 normalize=True,
197 ),
198 )
199 def cli(metasyntactic_var):
200 mock(metasyntactic_var)
202 # check a valid choice without a kv separator
203 result = self.runner.invoke(cli, ["--metasyntactic-var", "FOO"])
204 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result))
205 mock.assert_called_with({"": "FOO"})
207 # check a valid choice with a kv separator
208 result = self.runner.invoke(cli, ["--metasyntactic-var", "lsst.daf.butler=BAR"])
209 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result))
210 mock.assert_called_with({"lsst.daf.butler": "BAR"})
212 # check that invalid choices with and without kv separators fail &
213 # return a non-zero exit code.
214 for val in ("BOZ", "lsst.daf.butler=BOZ"):
215 result = self.runner.invoke(cli, ["--metasyntactic-var", val])
216 self.assertNotEqual(result.exit_code, 0, msg=clickResultMsg(result))
218 # check value normalization (lower case "foo" should become "FOO")
219 result = self.runner.invoke(cli, ["--metasyntactic-var", "lsst.daf.butler=foo"])
220 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result))
221 mock.assert_called_with({"lsst.daf.butler": "FOO"})
223 def test_separatorDash(self):
224 def split_kv_dash(context, param, values):
225 return split_kv(context, param, values, separator="-")
227 mock = MagicMock()
229 @click.command()
230 @click.option("--value", callback=split_kv_dash, multiple=True)
231 def cli(value):
232 mock(value)
234 result = self.runner.invoke(cli, ["--value", "first-1"])
235 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result))
236 mock.assert_called_with({"first": "1"})
238 def test_separatorFunctoolsDash(self):
239 mock = MagicMock()
241 @click.command()
242 @click.option("--value", callback=partial(split_kv, separator="-"), multiple=True)
243 def cli(value):
244 mock(value)
246 result = self.runner.invoke(cli, ["--value", "first-1", "--value", "second-2"])
247 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result))
248 mock.assert_called_with({"first": "1", "second": "2"})
250 def test_separatorSpace(self):
251 @click.command()
252 @click.option("--value", callback=partial(split_kv, separator=" "), multiple=True)
253 def cli(value):
254 pass
256 result = self.runner.invoke(cli, ["--value", "first 1"])
257 self.assertEqual(str(result.exception), "' ' is not a supported separator for key-value pairs.")
259 def test_separatorComma(self):
260 @click.command()
261 @click.option("--value", callback=partial(split_kv, separator=","), multiple=True)
262 def cli(value):
263 pass
265 result = self.runner.invoke(cli, ["--value", "first,1"])
266 self.assertEqual(str(result.exception), "',' is not a supported separator for key-value pairs.")
268 def test_normalizeWithoutChoice(self):
269 """Test that normalize=True without Choice fails gracefully.
271 Normalize uses values in the provided Choice to create the normalized
272 value. Without a provided Choice, it can't normalize. Verify that this
273 does not cause a crash or other bad behavior, it just doesn't normalize
274 anything.
275 """
276 mock = MagicMock()
278 @click.command()
279 @click.option("--value", callback=partial(split_kv, normalize=True))
280 def cli(value):
281 mock(value)
283 result = self.runner.invoke(cli, ["--value", "foo=bar"])
284 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result))
285 mock.assert_called_with(dict(foo="bar"))
287 def test_addToDefaultValue(self):
288 """Verify that if add_to_default is True that passed-in values are
289 added to the default value set in the option.
290 """
291 mock = MagicMock()
293 @click.command()
294 @click.option(
295 "--value",
296 callback=partial(split_kv, add_to_default=True, unseparated_okay=True),
297 default=["INFO"],
298 multiple=True,
299 )
300 def cli(value):
301 mock(value)
303 result = self.runner.invoke(cli, ["--value", "lsst.daf.butler=DEBUG"])
304 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result))
305 mock.assert_called_with({"": "INFO", "lsst.daf.butler": "DEBUG"})
307 def test_replaceDefaultValue(self):
308 """Verify that if add_to_default is False (this is the default value),
309 that passed-in values replace any default value, even if keys are
310 different.
311 """
312 mock = MagicMock()
314 @click.command()
315 @click.option(
316 "--value", callback=partial(split_kv, unseparated_okay=True), default=["INFO"], multiple=True
317 )
318 def cli(value):
319 mock(value)
321 result = self.runner.invoke(cli, ["--value", "lsst.daf.butler=DEBUG"])
322 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result))
323 mock.assert_called_with({"lsst.daf.butler": "DEBUG"})
326if __name__ == "__main__":
327 unittest.main()