Coverage for tests/test_cliUtils.py: 21%
199 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-05 02:53 -0700
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-05 02:53 -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 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 unittest.mock import MagicMock
34import click
35from lsst.daf.butler.cli.opt import directory_argument, repo_argument
36from lsst.daf.butler.cli.utils import (
37 LogCliRunner,
38 MWArgumentDecorator,
39 MWCommand,
40 MWCtxObj,
41 MWOption,
42 MWOptionDecorator,
43 MWPath,
44 clickResultMsg,
45 option_section,
46 unwrap,
47)
50class ArgumentHelpGeneratorTestCase(unittest.TestCase):
51 """Test the help system."""
53 def testHelp(self):
54 @click.command()
55 # Use custom help in the arguments so that any changes to default help
56 # text do not break this test unnecessarily.
57 @repo_argument(help="repo help text")
58 @directory_argument(help="directory help text")
59 def cli():
60 """The cli help message.""" # noqa: D401
61 pass
63 self.runTest(cli)
65 def testHelpWrapped(self):
66 @click.command()
67 # Use custom help in the arguments so that any changes to default help
68 # text do not break this test unnecessarily.
69 @repo_argument(help="repo help text")
70 @directory_argument(help="directory help text")
71 def cli():
72 """The cli help message.""" # noqa: D401
73 pass
75 self.runTest(cli)
77 def runTest(self, cli):
78 """Test `utils.addArgumentHelp` and its use in repo_argument and
79 directory_argument; verifies that the argument help gets added to the
80 command function help, and that it's added in the correct order. See
81 addArgumentHelp for more details.
82 """
83 expected = """Usage: cli [OPTIONS] REPO DIRECTORY
85 The cli help message.
87 repo help text
89 directory help text
91Options:
92 --help Show this message and exit.
93"""
94 runner = LogCliRunner()
95 result = runner.invoke(cli, ["--help"])
96 self.assertIn(expected, result.output)
99class UnwrapStringTestCase(unittest.TestCase):
100 """Test string unwrapping."""
102 def test_leadingNewline(self):
103 testStr = """
104 foo bar
105 baz """
106 self.assertEqual(unwrap(testStr), "foo bar baz")
108 def test_leadingContent(self):
109 testStr = """foo bar
110 baz """
111 self.assertEqual(unwrap(testStr), "foo bar baz")
113 def test_trailingNewline(self):
114 testStr = """
115 foo bar
116 baz
117 """
118 self.assertEqual(unwrap(testStr), "foo bar baz")
120 def test_oneLine(self):
121 testStr = """foo bar baz"""
122 self.assertEqual(unwrap(testStr), "foo bar baz")
124 def test_oneLineWithLeading(self):
125 testStr = """
126 foo bar baz"""
127 self.assertEqual(unwrap(testStr), "foo bar baz")
129 def test_oneLineWithTrailing(self):
130 testStr = """foo bar baz
131 """
132 self.assertEqual(unwrap(testStr), "foo bar baz")
134 def test_lineBreaks(self):
135 testStr = """foo bar
136 baz
138 boz
140 qux"""
141 self.assertEqual(unwrap(testStr), "foo bar baz\n\nboz\n\nqux")
144class MWOptionTest(unittest.TestCase):
145 """Test MWOption."""
147 def setUp(self):
148 self.runner = LogCliRunner()
150 def test_addEllipsisToMultiple(self):
151 """Verify that MWOption adds ellipsis to the option metavar when
152 `multiple=True`
154 The default behavior of click is to not add ellipsis to options that
155 have `multiple=True`.
156 """
158 @click.command()
159 @click.option("--things", cls=MWOption, multiple=True)
160 def cmd(things):
161 pass
163 result = self.runner.invoke(cmd, ["--help"])
164 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
165 expectedOutput = """Options:
166 --things TEXT ..."""
167 self.assertIn(expectedOutput, result.output)
169 def test_addEllipsisToNargs(self):
170 """Verify that MWOption adds " ..." after the option metavar when
171 `nargs` is set to more than 1 and less than 1.
173 The default behavior of click is to add ellipsis when nargs does not
174 equal 1, but it does not put a space before the ellipsis and we prefer
175 a space between the metavar and the ellipsis.
176 """
177 for numberOfArgs in (0, 1, 2): # nargs must be >= 0 for an option
179 @click.command()
180 @click.option("--things", cls=MWOption, nargs=numberOfArgs)
181 def cmd(things):
182 pass
184 result = self.runner.invoke(cmd, ["--help"])
185 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
186 expectedOutput = f"""Options:
187 --things TEXT{' ...' if numberOfArgs != 1 else ''}"""
188 self.assertIn(expectedOutput, result.output)
191class MWArgumentDecoratorTest(unittest.TestCase):
192 """Tests for the MWArgumentDecorator class."""
194 things_argument = MWArgumentDecorator("things")
195 otherHelpText = "Help text for OTHER."
196 other_argument = MWArgumentDecorator("other", help=otherHelpText)
198 def setUp(self):
199 self.runner = LogCliRunner()
201 def test_help(self):
202 """Verify expected help text output.
204 Verify argument help gets inserted after the usage, in the order
205 arguments are declared.
207 Verify that MWArgument adds " ..." after the option metavar when
208 `nargs` != 1. The default behavior of click is to add ellipsis when
209 nargs does not equal 1, but it does not put a space before the ellipsis
210 and we prefer a space between the metavar and the ellipsis.
211 """
212 # nargs can be -1 for any number of args, or >= 1 for a specified
213 # number of arguments.
215 helpText = "Things help text."
216 for numberOfArgs in (-1, 1, 2):
217 for required in (True, False):
219 @click.command()
220 @self.things_argument(required=required, nargs=numberOfArgs, help=helpText)
221 @self.other_argument()
222 def cmd(things, other):
223 """Cmd help text."""
224 pass
226 result = self.runner.invoke(cmd, ["--help"])
227 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
228 things = "THINGS" if required else "[THINGS]"
229 additional = "... " if numberOfArgs != 1 else ""
230 expectedOutput = f"""Usage: cmd [OPTIONS] {things} {additional}OTHER
232 Cmd help text.
234 {helpText}
236 {self.otherHelpText}
237"""
238 self.assertIn(expectedOutput, result.output)
240 def testUse(self):
241 """Test using the MWArgumentDecorator with a command."""
242 mock = MagicMock()
244 @click.command()
245 @self.things_argument()
246 def cli(things):
247 mock(things)
249 self.runner = click.testing.CliRunner()
250 result = self.runner.invoke(cli, "foo")
251 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
252 mock.assert_called_with("foo")
255class MWOptionDecoratorTest(unittest.TestCase):
256 """Tests for the MWOptionDecorator class."""
258 _test_option = MWOptionDecorator("-t", "--test", multiple=True)
260 def testGetName(self):
261 """Test getting the option name from the MWOptionDecorator."""
262 self.assertEqual(self._test_option.name(), "test")
264 def testGetOpts(self):
265 """Test getting the option flags from the MWOptionDecorator."""
266 self.assertEqual(self._test_option.opts(), ["-t", "--test"])
268 def testUse(self):
269 """Test using the MWOptionDecorator with a command."""
270 mock = MagicMock()
272 @click.command()
273 @self._test_option()
274 def cli(test):
275 mock(test)
277 self.runner = click.testing.CliRunner()
278 result = self.runner.invoke(cli, ("-t", "foo"))
279 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
280 mock.assert_called_with(("foo",))
282 def testOverride(self):
283 """Test using the MWOptionDecorator with a command and overriding one
284 of the default values.
285 """
286 mock = MagicMock()
288 @click.command()
289 @self._test_option(multiple=False)
290 def cli(test):
291 mock(test)
293 self.runner = click.testing.CliRunner()
294 result = self.runner.invoke(cli, ("-t", "foo"))
295 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
296 mock.assert_called_with("foo")
299class SectionOptionTest(unittest.TestCase):
300 """Tests for the option_section decorator that inserts section break
301 headings between options in the --help output of a command.
302 """
304 @staticmethod
305 @click.command()
306 @click.option("--foo")
307 @option_section("Section break between metasyntactic variables.")
308 @click.option("--bar")
309 def cli(foo, bar):
310 pass
312 def setUp(self):
313 self.runner = click.testing.CliRunner()
315 def test_section_help(self):
316 """Verify that the section break is printed in the help output in the
317 expected location and with expected formatting.
318 """
319 result = self.runner.invoke(self.cli, ["--help"])
320 # \x20 is a space, added explicitly below to prevent the
321 # normally-helpful editor setting "remove trailing whitespace" from
322 # stripping it out in this case. (The blank line with 2 spaces is an
323 # artifact of how click and our code generate help text.)
324 expected = """Options:
325 --foo TEXT
326\x20\x20
327Section break between metasyntactic variables.
328 --bar TEXT"""
329 self.assertIn(expected, result.output)
331 def test_section_function(self):
332 """Verify that the section does not cause any arguments to be passed to
333 the command function.
335 The command function `cli` implementation inputs `foo` and `bar`, but
336 does accept an argument for the section. When the command is invoked
337 and the function called it should result in exit_code=0 (not 1 with a
338 missing argument error).
339 """
340 result = self.runner.invoke(self.cli, [])
341 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
344class MWPathTest(unittest.TestCase):
345 """Test MWPath."""
347 def getCmd(self, exists):
348 @click.command()
349 @click.option("--name", type=MWPath(exists=exists))
350 def cmd(name):
351 pass
353 return cmd
355 def setUp(self):
356 self.runner = click.testing.CliRunner()
358 def test_exist(self):
359 """Test the exist argument, verify that True means the file must exist,
360 False means the file must not exist, and None means that the file may
361 or may not exist.
362 """
363 with self.runner.isolated_filesystem():
364 mustExistCmd = self.getCmd(exists=True)
365 mayExistCmd = self.getCmd(exists=None)
366 mustNotExistCmd = self.getCmd(exists=False)
367 args = ["--name", "foo.txt"]
369 result = self.runner.invoke(mustExistCmd, args)
370 self.assertNotEqual(result.exit_code, 0, clickResultMsg(result))
371 self.assertRegex(result.output, """['"]foo.txt['"] does not exist.""")
373 result = self.runner.invoke(mayExistCmd, args)
374 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
376 result = self.runner.invoke(mustNotExistCmd, args)
377 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
379 # isolated_filesystem runs in a temporary directory, when it is
380 # removed everything inside will be removed.
381 with open("foo.txt", "w") as _:
382 result = self.runner.invoke(mustExistCmd, args)
383 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
385 result = self.runner.invoke(mayExistCmd, args)
386 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
388 result = self.runner.invoke(mustNotExistCmd, args)
389 self.assertNotEqual(result.exit_code, 0, clickResultMsg(result))
390 self.assertIn('"foo.txt" should not exist.', result.output)
393class MWCommandTest(unittest.TestCase):
394 """Test MWCommand."""
396 def setUp(self):
397 self.runner = click.testing.CliRunner()
398 self.ctx = None
400 def testCaptureOptions(self):
401 """Test that command options are captured in order."""
403 @click.command(cls=MWCommand)
404 @click.argument("ARG_A", required=False)
405 @click.argument("ARG_B", required=False)
406 @click.option("-o", "--an-option")
407 @click.option("-s", "--second-option")
408 @click.option("-f", is_flag=True)
409 @click.option("-d/-n", "--do/--do-not", "do_or_do_not")
410 @click.option("--multi", multiple=True)
411 @click.pass_context
412 def cmd(ctx, arg_a, arg_b, an_option, second_option, f, do_or_do_not, multi):
413 self.assertIsNotNone(ctx)
414 self.ctx = ctx
416 # When `expected` is `None`, the expected args are exactly the same as
417 # as the result of `args.split()`. If `expected` is not `None` then
418 # the expected args are the same as `expected.split()`.
419 for args, expected in (
420 ("--an-option foo --second-option bar", None),
421 ("--second-option bar --an-option foo", None),
422 ("--an-option foo", None),
423 ("--second-option bar", None),
424 ("--an-option foo -f --second-option bar", None),
425 ("-o foo -s bar", "--an-option foo --second-option bar"),
426 ("--an-option foo -f -s bar", "--an-option foo -f --second-option bar"),
427 ("--an-option=foo", "--an-option foo"),
428 # NB when using a short flag everything that follows, including an
429 # equals sign, is part of the value!
430 ("-o=foo", "--an-option =foo"),
431 ("--do", None),
432 ("--do-not", None),
433 ("-d", "--do"),
434 ("-n", "--do-not"),
435 # Arguments always come last, but the order of Arguments is still
436 # preserved:
437 ("myarg --an-option foo", "--an-option foo myarg"),
438 ("argA --an-option foo", "--an-option foo argA"),
439 ("argA argB --an-option foo", "--an-option foo argA argB"),
440 ("argA --an-option foo argB", "--an-option foo argA argB"),
441 ("--an-option foo argA argB", "--an-option foo argA argB"),
442 ("--multi one --multi two", None),
443 ):
444 split_args = args.split()
445 expected_args = split_args if expected is None else expected.split()
446 result = self.runner.invoke(cmd, split_args)
447 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
448 self.assertIsNotNone(self.ctx)
449 ctx_obj = MWCtxObj.getFrom(self.ctx)
450 self.assertEqual(ctx_obj.args, expected_args)
453if __name__ == "__main__":
454 unittest.main()