Coverage for tests/test_cliUtilSplitKv.py: 21%
140 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-12 10:56 -0700
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-12 10:56 -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 """
44 self.assertEqual(split_kv("context", "param", "first=1", return_type=tuple), (("first", "1"),))
46 def test_multiple_dict(self):
47 """Test that multiple comma separated kv pairs convert to a dict."""
48 self.assertEqual(split_kv("context", "param", "first=1,second=2"), {"first": "1", "second": "2"})
50 def test_multiple_tuple(self):
51 """Test that multiple comma separated kv pairs convert to a tuple when
52 return_type=tuple.
53 """
54 self.assertEqual(
55 split_kv("context", "param", "first=1,second=2", return_type=tuple),
56 (("first", "1"), ("second", "2")),
57 )
59 def test_unseparated(self):
60 """Test that a value without a key converts to a kv pair with an empty
61 string key.
62 """
63 self.assertEqual(
64 split_kv("context", "param", "first,second=2", unseparated_okay=True),
65 {"": "first", "second": "2"},
66 )
68 def test_notMultiple(self):
69 """Test that multiple values are rejected if multiple=False."""
70 with self.assertRaisesRegex(
71 click.ClickException,
72 "Could not parse key-value pair "
73 "'first=1,second=2' using separator '=', with multiple values not "
74 "allowed.",
75 ):
76 split_kv("context", "param", "first=1,second=2", multiple=False)
78 def test_wrongSeparator(self):
79 """Test that an input with the wrong separator raises."""
80 with self.assertRaises(click.ClickException):
81 split_kv("context", "param", "first-1")
83 def test_missingSeparator(self):
84 """Test that an input with no separator raises when
85 unseparated_okay=False (this is the default value).
86 """
87 with self.assertRaises(click.ClickException):
88 split_kv("context", "param", "first 1")
90 def test_unseparatedOkay(self):
91 """Test that that the default key is used for values without a
92 separator when unseparated_okay=True.
93 """
94 self.assertEqual(split_kv("context", "param", "foo", unseparated_okay=True), {"": "foo"})
96 def test_unseparatedOkay_list(self):
97 """Test that that the default key is used for values without a
98 separator when unseparated_okay=True and the return_type is tuple.
99 """
100 self.assertEqual(
101 split_kv("context", "param", "foo,bar", unseparated_okay=True, return_type=tuple),
102 (("", "foo"), ("", "bar")),
103 )
105 def test_unseparatedOkay_defaultKey(self):
106 """Test that that the default key can be set and is used for values
107 without a separator when unseparated_okay=True.
108 """
109 self.assertEqual(
110 split_kv("context", "param", "foo", unseparated_okay=True, default_key=...), {...: "foo"}
111 )
113 def test_dashSeparator(self):
114 """Test that specifying a separator is accepted and converts arguments
115 to a dict.
116 """
117 self.assertEqual(
118 split_kv("context", "param", "first-1,second-2", separator="-"), {"first": "1", "second": "2"}
119 )
121 def test_reverseKv(self):
122 self.assertEqual(
123 split_kv(
124 "context",
125 "param",
126 "first=1,second",
127 unseparated_okay=True,
128 default_key="key",
129 reverse_kv=True,
130 ),
131 {"1": "first", "second": "key"},
132 )
134 def test_invalidResultType(self):
135 with self.assertRaises(click.ClickException):
136 split_kv(
137 "context",
138 "param",
139 "first=1,second=2",
140 return_type=set,
141 )
144class SplitKvCmdTestCase(unittest.TestCase):
145 """Tests using split_kv with a command."""
147 def setUp(self):
148 self.runner = LogCliRunner()
150 def test_cli(self):
151 mock = MagicMock()
153 @click.command()
154 @click.option("--value", callback=split_kv, multiple=True)
155 def cli(value):
156 mock(value)
158 result = self.runner.invoke(cli, ["--value", "first=1"])
159 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result))
160 mock.assert_called_with({"first": "1"})
162 result = self.runner.invoke(cli, ["--value", "first=1,second=2"])
163 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result))
164 mock.assert_called_with({"first": "1", "second": "2"})
166 result = self.runner.invoke(cli, ["--value", "first=1", "--value", "second=2"])
167 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result))
168 mock.assert_called_with({"first": "1", "second": "2"})
170 # double separator "==" should fail:
171 result = self.runner.invoke(cli, ["--value", "first==1"])
172 self.assertEqual(result.exit_code, 1)
173 self.assertEqual(
174 result.output,
175 "Error: Could not parse key-value pair 'first==1' using separator '=', with "
176 "multiple values allowed: too many values to unpack (expected 2)\n",
177 )
179 def test_choice(self):
180 choices = ["FOO", "BAR", "BAZ"]
181 mock = MagicMock()
183 @click.command()
184 @click.option(
185 "--metasyntactic-var",
186 callback=partial(
187 split_kv,
188 unseparated_okay=True,
189 choice=click.Choice(choices=choices, case_sensitive=False),
190 normalize=True,
191 ),
192 )
193 def cli(metasyntactic_var):
194 mock(metasyntactic_var)
196 # check a valid choice without a kv separator
197 result = self.runner.invoke(cli, ["--metasyntactic-var", "FOO"])
198 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result))
199 mock.assert_called_with({"": "FOO"})
201 # check a valid choice with a kv separator
202 result = self.runner.invoke(cli, ["--metasyntactic-var", "lsst.daf.butler=BAR"])
203 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result))
204 mock.assert_called_with({"lsst.daf.butler": "BAR"})
206 # check that invalid choices with and without kv separators fail &
207 # return a non-zero exit code.
208 for val in ("BOZ", "lsst.daf.butler=BOZ"):
209 result = self.runner.invoke(cli, ["--metasyntactic-var", val])
210 self.assertNotEqual(result.exit_code, 0, msg=clickResultMsg(result))
212 # check value normalization (lower case "foo" should become "FOO")
213 result = self.runner.invoke(cli, ["--metasyntactic-var", "lsst.daf.butler=foo"])
214 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result))
215 mock.assert_called_with({"lsst.daf.butler": "FOO"})
217 def test_separatorDash(self):
218 def split_kv_dash(context, param, values):
219 return split_kv(context, param, values, separator="-")
221 mock = MagicMock()
223 @click.command()
224 @click.option("--value", callback=split_kv_dash, multiple=True)
225 def cli(value):
226 mock(value)
228 result = self.runner.invoke(cli, ["--value", "first-1"])
229 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result))
230 mock.assert_called_with({"first": "1"})
232 def test_separatorFunctoolsDash(self):
233 mock = MagicMock()
235 @click.command()
236 @click.option("--value", callback=partial(split_kv, separator="-"), multiple=True)
237 def cli(value):
238 mock(value)
240 result = self.runner.invoke(cli, ["--value", "first-1", "--value", "second-2"])
241 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result))
242 mock.assert_called_with({"first": "1", "second": "2"})
244 def test_separatorSpace(self):
245 @click.command()
246 @click.option("--value", callback=partial(split_kv, separator=" "), multiple=True)
247 def cli(value):
248 pass
250 result = self.runner.invoke(cli, ["--value", "first 1"])
251 self.assertEqual(str(result.exception), "' ' is not a supported separator for key-value pairs.")
253 def test_separatorComma(self):
254 @click.command()
255 @click.option("--value", callback=partial(split_kv, separator=","), multiple=True)
256 def cli(value):
257 pass
259 result = self.runner.invoke(cli, ["--value", "first,1"])
260 self.assertEqual(str(result.exception), "',' is not a supported separator for key-value pairs.")
262 def test_normalizeWithoutChoice(self):
263 """Test that normalize=True without Choice fails gracefully.
265 Normalize uses values in the provided Choice to create the normalized
266 value. Without a provided Choice, it can't normalize. Verify that this
267 does not cause a crash or other bad behavior, it just doesn't normalize
268 anything.
269 """
270 mock = MagicMock()
272 @click.command()
273 @click.option("--value", callback=partial(split_kv, normalize=True))
274 def cli(value):
275 mock(value)
277 result = self.runner.invoke(cli, ["--value", "foo=bar"])
278 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result))
279 mock.assert_called_with(dict(foo="bar"))
281 def test_addToDefaultValue(self):
282 """Verify that if add_to_default is True that passed-in values are
283 added to the default value set in the option.
284 """
285 mock = MagicMock()
287 @click.command()
288 @click.option(
289 "--value",
290 callback=partial(split_kv, add_to_default=True, unseparated_okay=True),
291 default=["INFO"],
292 multiple=True,
293 )
294 def cli(value):
295 mock(value)
297 result = self.runner.invoke(cli, ["--value", "lsst.daf.butler=DEBUG"])
298 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result))
299 mock.assert_called_with({"": "INFO", "lsst.daf.butler": "DEBUG"})
301 def test_replaceDefaultValue(self):
302 """Verify that if add_to_default is False (this is the default value),
303 that passed-in values replace any default value, even if keys are
304 different.
305 """
306 mock = MagicMock()
308 @click.command()
309 @click.option(
310 "--value", callback=partial(split_kv, unseparated_okay=True), default=["INFO"], multiple=True
311 )
312 def cli(value):
313 mock(value)
315 result = self.runner.invoke(cli, ["--value", "lsst.daf.butler=DEBUG"])
316 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result))
317 mock.assert_called_with({"lsst.daf.butler": "DEBUG"})
320if __name__ == "__main__":
321 unittest.main()