Coverage for tests/test_cliUtilSplitKv.py: 21%
140 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-04-07 00:58 -0700
« prev ^ index » next coverage.py v6.5.0, created at 2023-04-07 00:58 -0700
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 program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <http://www.gnu.org/licenses/>.
22"""Unit tests for the daf_butler shared CLI options.
23"""
25import unittest
26from functools import partial
27from unittest.mock import MagicMock
29import click
30from lsst.daf.butler.cli.utils import LogCliRunner, clickResultMsg, split_kv
33class SplitKvTestCase(unittest.TestCase):
34 """Tests that call split_kv directly."""
36 def test_single_dict(self):
37 """Test that a single kv pair converts to a dict."""
38 self.assertEqual(split_kv("context", "param", "first=1"), {"first": "1"})
40 def test_single_tuple(self):
41 """Test that a single kv pair converts to a tuple when
42 return_type=tuple."""
43 self.assertEqual(split_kv("context", "param", "first=1", return_type=tuple), (("first", "1"),))
45 def test_multiple_dict(self):
46 """Test that multiple comma separated kv pairs convert to a dict."""
47 self.assertEqual(split_kv("context", "param", "first=1,second=2"), {"first": "1", "second": "2"})
49 def test_multiple_tuple(self):
50 """Test that multiple comma separated kv pairs convert to a tuple when
51 return_type=tuple."""
52 self.assertEqual(
53 split_kv("context", "param", "first=1,second=2", return_type=tuple),
54 (("first", "1"), ("second", "2")),
55 )
57 def test_unseparated(self):
58 """Test that a value without a key converts to a kv pair with an empty
59 string key."""
60 self.assertEqual(
61 split_kv("context", "param", "first,second=2", unseparated_okay=True),
62 {"": "first", "second": "2"},
63 )
65 def test_notMultiple(self):
66 """Test that multiple values are rejected if multiple=False."""
67 with self.assertRaisesRegex(
68 click.ClickException,
69 "Could not parse key-value pair "
70 "'first=1,second=2' using separator '=', with multiple values not "
71 "allowed.",
72 ):
73 split_kv("context", "param", "first=1,second=2", multiple=False)
75 def test_wrongSeparator(self):
76 """Test that an input with the wrong separator raises."""
77 with self.assertRaises(click.ClickException):
78 split_kv("context", "param", "first-1")
80 def test_missingSeparator(self):
81 """Test that an input with no separator raises when
82 unseparated_okay=False (this is the default value)."""
83 with self.assertRaises(click.ClickException):
84 split_kv("context", "param", "first 1")
86 def test_unseparatedOkay(self):
87 """Test that that the default key is used for values without a
88 separator when unseparated_okay=True."""
89 self.assertEqual(split_kv("context", "param", "foo", unseparated_okay=True), {"": "foo"})
91 def test_unseparatedOkay_list(self):
92 """Test that that the default key is used for values without a
93 separator when unseparated_okay=True and the return_type is tuple."""
94 self.assertEqual(
95 split_kv("context", "param", "foo,bar", unseparated_okay=True, return_type=tuple),
96 (("", "foo"), ("", "bar")),
97 )
99 def test_unseparatedOkay_defaultKey(self):
100 """Test that that the default key can be set and is used for values
101 without a separator when unseparated_okay=True."""
102 self.assertEqual(
103 split_kv("context", "param", "foo", unseparated_okay=True, default_key=...), {...: "foo"}
104 )
106 def test_dashSeparator(self):
107 """Test that specifying a separator is accepted and converts arguments
108 to a dict.
109 """
110 self.assertEqual(
111 split_kv("context", "param", "first-1,second-2", separator="-"), {"first": "1", "second": "2"}
112 )
114 def test_reverseKv(self):
115 self.assertEqual(
116 split_kv(
117 "context",
118 "param",
119 "first=1,second",
120 unseparated_okay=True,
121 default_key="key",
122 reverse_kv=True,
123 ),
124 {"1": "first", "second": "key"},
125 )
127 def test_invalidResultType(self):
128 with self.assertRaises(click.ClickException):
129 split_kv(
130 "context",
131 "param",
132 "first=1,second=2",
133 return_type=set,
134 )
137class SplitKvCmdTestCase(unittest.TestCase):
138 """Tests using split_kv with a command."""
140 def setUp(self):
141 self.runner = LogCliRunner()
143 def test_cli(self):
144 mock = MagicMock()
146 @click.command()
147 @click.option("--value", callback=split_kv, multiple=True)
148 def cli(value):
149 mock(value)
151 result = self.runner.invoke(cli, ["--value", "first=1"])
152 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result))
153 mock.assert_called_with({"first": "1"})
155 result = self.runner.invoke(cli, ["--value", "first=1,second=2"])
156 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result))
157 mock.assert_called_with({"first": "1", "second": "2"})
159 result = self.runner.invoke(cli, ["--value", "first=1", "--value", "second=2"])
160 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result))
161 mock.assert_called_with({"first": "1", "second": "2"})
163 # double separator "==" should fail:
164 result = self.runner.invoke(cli, ["--value", "first==1"])
165 self.assertEqual(result.exit_code, 1)
166 self.assertEqual(
167 result.output,
168 "Error: Could not parse key-value pair 'first==1' using separator '=', with "
169 "multiple values allowed: too many values to unpack (expected 2)\n",
170 )
172 def test_choice(self):
173 choices = ["FOO", "BAR", "BAZ"]
174 mock = MagicMock()
176 @click.command()
177 @click.option(
178 "--metasyntactic-var",
179 callback=partial(
180 split_kv,
181 unseparated_okay=True,
182 choice=click.Choice(choices=choices, case_sensitive=False),
183 normalize=True,
184 ),
185 )
186 def cli(metasyntactic_var):
187 mock(metasyntactic_var)
189 # check a valid choice without a kv separator
190 result = self.runner.invoke(cli, ["--metasyntactic-var", "FOO"])
191 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result))
192 mock.assert_called_with({"": "FOO"})
194 # check a valid choice with a kv separator
195 result = self.runner.invoke(cli, ["--metasyntactic-var", "lsst.daf.butler=BAR"])
196 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result))
197 mock.assert_called_with({"lsst.daf.butler": "BAR"})
199 # check that invalid choices with and without kv separators fail &
200 # return a non-zero exit code.
201 for val in ("BOZ", "lsst.daf.butler=BOZ"):
202 result = self.runner.invoke(cli, ["--metasyntactic-var", val])
203 self.assertNotEqual(result.exit_code, 0, msg=clickResultMsg(result))
205 # check value normalization (lower case "foo" should become "FOO")
206 result = self.runner.invoke(cli, ["--metasyntactic-var", "lsst.daf.butler=foo"])
207 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result))
208 mock.assert_called_with({"lsst.daf.butler": "FOO"})
210 def test_separatorDash(self):
211 def split_kv_dash(context, param, values):
212 return split_kv(context, param, values, separator="-")
214 mock = MagicMock()
216 @click.command()
217 @click.option("--value", callback=split_kv_dash, multiple=True)
218 def cli(value):
219 mock(value)
221 result = self.runner.invoke(cli, ["--value", "first-1"])
222 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result))
223 mock.assert_called_with({"first": "1"})
225 def test_separatorFunctoolsDash(self):
226 mock = MagicMock()
228 @click.command()
229 @click.option("--value", callback=partial(split_kv, separator="-"), multiple=True)
230 def cli(value):
231 mock(value)
233 result = self.runner.invoke(cli, ["--value", "first-1", "--value", "second-2"])
234 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result))
235 mock.assert_called_with({"first": "1", "second": "2"})
237 def test_separatorSpace(self):
238 @click.command()
239 @click.option("--value", callback=partial(split_kv, separator=" "), multiple=True)
240 def cli(value):
241 pass
243 result = self.runner.invoke(cli, ["--value", "first 1"])
244 self.assertEqual(str(result.exception), "' ' is not a supported separator for key-value pairs.")
246 def test_separatorComma(self):
247 @click.command()
248 @click.option("--value", callback=partial(split_kv, separator=","), multiple=True)
249 def cli(value):
250 pass
252 result = self.runner.invoke(cli, ["--value", "first,1"])
253 self.assertEqual(str(result.exception), "',' is not a supported separator for key-value pairs.")
255 def test_normalizeWithoutChoice(self):
256 """Test that normalize=True without Choice fails gracefully.
258 Normalize uses values in the provided Choice to create the normalized
259 value. Without a provided Choice, it can't normalize. Verify that this
260 does not cause a crash or other bad behavior, it just doesn't normalize
261 anything.
262 """
263 mock = MagicMock()
265 @click.command()
266 @click.option("--value", callback=partial(split_kv, normalize=True))
267 def cli(value):
268 mock(value)
270 result = self.runner.invoke(cli, ["--value", "foo=bar"])
271 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result))
272 mock.assert_called_with(dict(foo="bar"))
274 def test_addToDefaultValue(self):
275 """Verify that if add_to_default is True that passed-in values are
276 added to the default value set in the option.
277 """
278 mock = MagicMock()
280 @click.command()
281 @click.option(
282 "--value",
283 callback=partial(split_kv, add_to_default=True, unseparated_okay=True),
284 default=["INFO"],
285 multiple=True,
286 )
287 def cli(value):
288 mock(value)
290 result = self.runner.invoke(cli, ["--value", "lsst.daf.butler=DEBUG"])
291 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result))
292 mock.assert_called_with({"": "INFO", "lsst.daf.butler": "DEBUG"})
294 def test_replaceDefaultValue(self):
295 """Verify that if add_to_default is False (this is the default value),
296 that passed-in values replace any default value, even if keys are
297 different.
298 """
299 mock = MagicMock()
301 @click.command()
302 @click.option(
303 "--value", callback=partial(split_kv, unseparated_okay=True), default=["INFO"], multiple=True
304 )
305 def cli(value):
306 mock(value)
308 result = self.runner.invoke(cli, ["--value", "lsst.daf.butler=DEBUG"])
309 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result))
310 mock.assert_called_with({"lsst.daf.butler": "DEBUG"})
313if __name__ == "__main__":
314 unittest.main()