Coverage for tests/test_cliUtils.py : 34%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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 click
26import unittest
27from unittest.mock import call, MagicMock
30from lsst.daf.butler.cli import butler
31from lsst.daf.butler.cli.utils import (clickResultMsg, ForwardOptions, LogCliRunner, Mocker, mockEnvVar,
32 MWArgumentDecorator, MWCommand, MWCtxObj, MWOption, MWOptionDecorator,
33 MWPath, option_section, unwrap)
34from lsst.daf.butler.cli.opt import directory_argument, repo_argument
37class MockerTestCase(unittest.TestCase):
39 def test_callMock(self):
40 """Test that a mocked subcommand calls the Mocker and can be verified.
41 """
42 runner = LogCliRunner(env=mockEnvVar)
43 result = runner.invoke(butler.cli, ["create", "repo"])
44 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
45 Mocker.mock.assert_called_with(repo="repo", seed_config=None, standalone=False, override=False,
46 outfile=None)
49class ArgumentHelpGeneratorTestCase(unittest.TestCase):
51 @staticmethod
52 @click.command()
53 # Use custom help in the arguments so that any changes to default help text
54 # do not break this test unnecessarily.
55 @repo_argument(help="repo help text")
56 @directory_argument(help="directory help text")
57 def cli():
58 """The cli help message."""
59 pass
61 def test_help(self):
62 """Tests `utils.addArgumentHelp` and its use in repo_argument and
63 directory_argument; verifies that the argument help gets added to the
64 command fucntion help, and that it's added in the correct order. See
65 addArgumentHelp for more details."""
66 runner = LogCliRunner()
67 result = runner.invoke(ArgumentHelpGeneratorTestCase.cli, ["--help"])
68 expected = """Usage: cli [OPTIONS] REPO DIRECTORY
70 The cli help message.
72 repo help text
74 directory help text
76Options:
77 --help Show this message and exit.
78"""
79 self.assertIn(expected, result.output)
82class UnwrapStringTestCase(unittest.TestCase):
84 def test_leadingNewline(self):
85 testStr = """
86 foo bar
87 baz """
88 self.assertEqual(unwrap(testStr), "foo bar baz")
90 def test_leadingContent(self):
91 testStr = """foo bar
92 baz """
93 self.assertEqual(unwrap(testStr), "foo bar baz")
95 def test_trailingNewline(self):
96 testStr = """
97 foo bar
98 baz
99 """
100 self.assertEqual(unwrap(testStr), "foo bar baz")
102 def test_oneLine(self):
103 testStr = """foo bar baz"""
104 self.assertEqual(unwrap(testStr), "foo bar baz")
106 def test_oneLineWithLeading(self):
107 testStr = """
108 foo bar baz"""
109 self.assertEqual(unwrap(testStr), "foo bar baz")
111 def test_oneLineWithTrailing(self):
112 testStr = """foo bar baz
113 """
114 self.assertEqual(unwrap(testStr), "foo bar baz")
116 def test_lineBreaks(self):
117 testStr = """foo bar
118 baz
120 boz
122 qux"""
123 self.assertEqual(unwrap(testStr), "foo bar baz\n\nboz\n\nqux")
126class MWOptionTest(unittest.TestCase):
128 def setUp(self):
129 self.runner = LogCliRunner()
131 def test_addElipsisToMultiple(self):
132 """Verify that MWOption adds elipsis to the option metavar when
133 `multiple=True`
135 The default behavior of click is to not add elipsis to options that
136 have `multiple=True`."""
138 @click.command()
139 @click.option("--things", cls=MWOption, multiple=True)
140 def cmd(things):
141 pass
142 result = self.runner.invoke(cmd, ["--help"])
143 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
144 expectedOutut = """Options:
145 --things TEXT ..."""
146 self.assertIn(expectedOutut, result.output)
148 def test_addElipsisToNargs(self):
149 """Verify that MWOption adds " ..." after the option metavar when
150 `nargs` is set to more than 1 and less than 1.
152 The default behavior of click is to add elipsis when nargs does not
153 equal 1, but it does not put a space before the elipsis and we prefer
154 a space between the metavar and the elipsis."""
155 for numberOfArgs in (0, 1, 2): # nargs must be >= 0 for an option
157 @click.command()
158 @click.option("--things", cls=MWOption, nargs=numberOfArgs)
159 def cmd(things):
160 pass
161 result = self.runner.invoke(cmd, ["--help"])
162 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
163 expectedOutut = f"""Options:
164 --things TEXT{' ...' if numberOfArgs != 1 else ''}"""
165 self.assertIn(expectedOutut, result.output)
168class MWArgumentDecoratorTest(unittest.TestCase):
169 """Tests for the MWArgumentDecorator class."""
171 things_argument = MWArgumentDecorator("things")
172 otherHelpText = "Help text for OTHER."
173 other_argument = MWArgumentDecorator("other", help=otherHelpText)
175 def setUp(self):
176 self.runner = LogCliRunner()
178 def test_help(self):
179 """Verify expected help text output.
181 Verify argument help gets inserted after the usage, in the order
182 arguments are declared.
184 Verify that MWArgument adds " ..." after the option metavar when
185 `nargs` != 1. The default behavior of click is to add elipsis when
186 nargs does not equal 1, but it does not put a space before the elipsis
187 and we prefer a space between the metavar and the elipsis."""
188 # nargs can be -1 for any number of args, or >= 1 for a specified
189 # number of arguments.
191 helpText = "Things help text."
192 for numberOfArgs in (-1, 1, 2):
193 for required in (True, False):
195 @click.command()
196 @self.things_argument(required=required, nargs=numberOfArgs, help=helpText)
197 @self.other_argument()
198 def cmd(things, other):
199 """Cmd help text."""
200 pass
201 result = self.runner.invoke(cmd, ["--help"])
202 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
203 expectedOutut = (f"""Usage: cmd [OPTIONS] {'THINGS' if required else '[THINGS]'} {'... ' if numberOfArgs != 1 else ''}OTHER
205 Cmd help text.
207 {helpText}
209 {self.otherHelpText}
210""")
211 self.assertIn(expectedOutut, result.output)
213 def testUse(self):
214 """Test using the MWArgumentDecorator with a command."""
215 mock = MagicMock()
217 @click.command()
218 @self.things_argument()
219 def cli(things):
220 mock(things)
221 self.runner = click.testing.CliRunner()
222 result = self.runner.invoke(cli, ("foo"))
223 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
224 mock.assert_called_with("foo")
227class MWOptionDecoratorTest(unittest.TestCase):
228 """Tests for the MWOptionDecorator class."""
230 test_option = MWOptionDecorator("-t", "--test", multiple=True)
232 def testGetName(self):
233 """Test getting the option name from the MWOptionDecorator."""
234 self.assertEqual(self.test_option.name(), "test")
236 def testGetOpts(self):
237 """Test getting the option flags from the MWOptionDecorator."""
238 self.assertEqual(self.test_option.opts(), ["-t", "--test"])
240 def testUse(self):
241 """Test using the MWOptionDecorator with a command."""
242 mock = MagicMock()
244 @click.command()
245 @self.test_option()
246 def cli(test):
247 mock(test)
248 self.runner = click.testing.CliRunner()
249 result = self.runner.invoke(cli, ("-t", "foo"))
250 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
251 mock.assert_called_with(("foo",))
253 def testOverride(self):
254 """Test using the MWOptionDecorator with a command and overriding one
255 of the default values."""
256 mock = MagicMock()
258 @click.command()
259 @self.test_option(multiple=False)
260 def cli(test):
261 mock(test)
262 self.runner = click.testing.CliRunner()
263 result = self.runner.invoke(cli, ("-t", "foo"))
264 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
265 mock.assert_called_with("foo")
268class SectionOptionTest(unittest.TestCase):
269 """Tests for the option_section decorator that inserts section break
270 headings between options in the --help output of a command."""
272 @staticmethod
273 @click.command()
274 @click.option("--foo")
275 @option_section("Section break between metasyntactic variables.")
276 @click.option("--bar")
277 def cli(foo, bar):
278 pass
280 def setUp(self):
281 self.runner = click.testing.CliRunner()
283 def test_section_help(self):
284 """Verify that the section break is printed in the help output in the
285 expected location and with expected formatting."""
286 result = self.runner.invoke(self.cli, ["--help"])
287 # \x20 is a space, added explicity below to prevent the
288 # normally-helpful editor setting "remove trailing whitespace" from
289 # stripping it out in this case. (The blank line with 2 spaces is an
290 # artifact of how click and our code generate help text.)
291 expected = """Options:
292 --foo TEXT
293\x20\x20
294Section break between metasyntactic variables.
295 --bar TEXT"""
296 self.assertIn(expected, result.output)
298 def test_section_function(self):
299 """Verify that the section does not cause any arguments to be passed to
300 the command function.
302 The command function `cli` implementation inputs `foo` and `bar`, but
303 does accept an argument for the section. When the command is invoked
304 and the function called it should result in exit_code=0 (not 1 with a
305 missing argument error).
306 """
307 result = self.runner.invoke(self.cli, [])
308 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
311class MWPathTest(unittest.TestCase):
313 def getCmd(self, exists):
315 @click.command()
316 @click.option("--name", type=MWPath(exists=exists))
317 def cmd(name):
318 pass
319 return cmd
321 def setUp(self):
322 self.runner = click.testing.CliRunner()
324 def test_exist(self):
325 """Test the exist argument, verify that True means the file must exist,
326 False means the file must not exist, and None means that the file may
327 or may not exist."""
328 with self.runner.isolated_filesystem():
329 mustExistCmd = self.getCmd(exists=True)
330 mayExistCmd = self.getCmd(exists=None)
331 mustNotExistCmd = self.getCmd(exists=False)
332 args = ["--name", "foo.txt"]
334 result = self.runner.invoke(mustExistCmd, args)
335 self.assertNotEqual(result.exit_code, 0, clickResultMsg(result))
336 self.assertRegex(result.output, """['"]foo.txt['"] does not exist.""")
338 result = self.runner.invoke(mayExistCmd, args)
339 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
341 result = self.runner.invoke(mustNotExistCmd, args)
342 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
344 # isolated_filesystem runs in a temporary directory, when it is
345 # removed everything inside will be removed.
346 with open("foo.txt", "w") as _:
347 result = self.runner.invoke(mustExistCmd, args)
348 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
350 result = self.runner.invoke(mayExistCmd, args)
351 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
353 result = self.runner.invoke(mustNotExistCmd, args)
354 self.assertNotEqual(result.exit_code, 0, clickResultMsg(result))
355 self.assertIn('"foo.txt" should not exist.', result.output)
358class ForwardObjectsTest(unittest.TestCase):
360 mock = MagicMock()
362 test_option = MWOptionDecorator("-t", "--test", "--atest")
364 @click.group(chain=True)
365 def cli():
366 pass
368 @staticmethod
369 @cli.command(cls=MWCommand)
370 @click.pass_context
371 @test_option(forward=True)
372 def forwards(ctx, **kwargs):
373 """A subcommand that forwards its test_option value to future
374 subcommands."""
375 def processor(objs):
376 newKwargs = objs.update(ctx.command.params, MWCtxObj.getFrom(ctx).args, **kwargs)
377 ForwardObjectsTest.mock("forwards", **newKwargs)
378 return objs
379 return processor
381 @staticmethod
382 @cli.command(cls=MWCommand)
383 @click.pass_context
384 @test_option() # default value of "foward" arg is False
385 def no_forward(ctx, **kwargs):
386 """A subcommand that accepts test_option but does not forward the value
387 to future subcommands."""
388 def processor(objs):
389 newKwargs = objs.update(ctx.command.params, MWCtxObj.getFrom(ctx).args, **kwargs)
390 ForwardObjectsTest.mock("no_forward", **newKwargs)
391 return objs
392 return processor
394 @staticmethod
395 @cli.command(cls=MWCommand)
396 @click.pass_context
397 def no_test_option(ctx, **kwargs):
398 """A subcommand that does not accept test_option."""
399 def processor(objs):
400 newKwargs = objs.update(ctx.command.params, MWCtxObj.getFrom(ctx).args, **kwargs)
401 ForwardObjectsTest.mock("no_test_option", **newKwargs)
402 return objs
403 return processor
405 @staticmethod
406 @cli.resultcallback()
407 def processCli(processors):
408 """Executes the subcommand 'processor' functions for all the
409 subcommands in the chained command group."""
410 objs = ForwardOptions()
411 for processor in processors:
412 objs = processor(objs)
414 def setUp(self):
415 self.runner = click.testing.CliRunner()
416 self.mock.reset_mock()
418 def testForward(self):
419 """Test that an option can be forward from one option to another."""
420 result = self.runner.invoke(self.cli, ["forwards", "-t", "foo", "forwards"])
421 print(result.output)
422 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
423 self.mock.assert_has_calls((call("forwards", test="foo"),
424 call("forwards", test="foo")))
426 def testNoForward(self):
427 """Test that when a subcommand that forwards an option value is called
428 before an option that does not use that option, that the stored option
429 does not get passed to the option that does not use it."""
430 result = self.runner.invoke(self.cli, ["forwards", "-t", "foo", "no-forward"])
431 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
432 self.mock.assert_has_calls((call("forwards", test="foo"),
433 call("no_forward", test="foo")))
435 def testForwardThrough(self):
436 """Test that forwarded option values persist when a subcommand that
437 does not use that value is called between subcommands that do use the
438 value."""
439 result = self.runner.invoke(self.cli, ["forwards", "-t", "foo",
440 "no-test-option",
441 "forwards"])
442 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
443 self.mock.assert_has_calls((call("forwards", test="foo"),
444 call("no_test_option"),
445 call("forwards", test="foo")))
448if __name__ == "__main__": 448 ↛ 449line 448 didn't jump to line 449, because the condition on line 448 was never true
449 unittest.main()