Coverage for tests/test_cliUtils.py: 31%
199 statements
« prev ^ index » next coverage.py v6.4.1, created at 2022-06-23 09:44 +0000
« prev ^ index » next coverage.py v6.4.1, created at 2022-06-23 09:44 +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 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 unittest.mock import MagicMock
28import click
29from lsst.daf.butler.cli.opt import directory_argument, repo_argument
30from lsst.daf.butler.cli.utils import (
31 LogCliRunner,
32 MWArgumentDecorator,
33 MWCommand,
34 MWCtxObj,
35 MWOption,
36 MWOptionDecorator,
37 MWPath,
38 clickResultMsg,
39 option_section,
40 unwrap,
41)
44class ArgumentHelpGeneratorTestCase(unittest.TestCase):
45 def testHelp(self):
46 @click.command()
47 # Use custom help in the arguments so that any changes to default help
48 # text do not break this test unnecessarily.
49 @repo_argument(help="repo help text")
50 @directory_argument(help="directory help text")
51 def cli():
52 """The cli help message."""
53 pass
55 self.runTest(cli)
57 def testHelpWrapped(self):
58 @click.command()
59 # Use custom help in the arguments so that any changes to default help
60 # text do not break this test unnecessarily.
61 @repo_argument(help="repo help text")
62 @directory_argument(help="directory help text")
63 def cli():
64 """The cli
65 help
66 message."""
67 pass
69 self.runTest(cli)
71 def runTest(self, cli):
72 """Tests `utils.addArgumentHelp` and its use in repo_argument and
73 directory_argument; verifies that the argument help gets added to the
74 command function help, and that it's added in the correct order. See
75 addArgumentHelp for more details."""
76 expected = """Usage: cli [OPTIONS] REPO DIRECTORY
78 The cli help message.
80 repo help text
82 directory help text
84Options:
85 --help Show this message and exit.
86"""
87 runner = LogCliRunner()
88 result = runner.invoke(cli, ["--help"])
89 self.assertIn(expected, result.output)
92class UnwrapStringTestCase(unittest.TestCase):
93 def test_leadingNewline(self):
94 testStr = """
95 foo bar
96 baz """
97 self.assertEqual(unwrap(testStr), "foo bar baz")
99 def test_leadingContent(self):
100 testStr = """foo bar
101 baz """
102 self.assertEqual(unwrap(testStr), "foo bar baz")
104 def test_trailingNewline(self):
105 testStr = """
106 foo bar
107 baz
108 """
109 self.assertEqual(unwrap(testStr), "foo bar baz")
111 def test_oneLine(self):
112 testStr = """foo bar baz"""
113 self.assertEqual(unwrap(testStr), "foo bar baz")
115 def test_oneLineWithLeading(self):
116 testStr = """
117 foo bar baz"""
118 self.assertEqual(unwrap(testStr), "foo bar baz")
120 def test_oneLineWithTrailing(self):
121 testStr = """foo bar baz
122 """
123 self.assertEqual(unwrap(testStr), "foo bar baz")
125 def test_lineBreaks(self):
126 testStr = """foo bar
127 baz
129 boz
131 qux"""
132 self.assertEqual(unwrap(testStr), "foo bar baz\n\nboz\n\nqux")
135class MWOptionTest(unittest.TestCase):
136 def setUp(self):
137 self.runner = LogCliRunner()
139 def test_addElipsisToMultiple(self):
140 """Verify that MWOption adds elipsis to the option metavar when
141 `multiple=True`
143 The default behavior of click is to not add elipsis to options that
144 have `multiple=True`."""
146 @click.command()
147 @click.option("--things", cls=MWOption, multiple=True)
148 def cmd(things):
149 pass
151 result = self.runner.invoke(cmd, ["--help"])
152 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
153 expectedOutput = """Options:
154 --things TEXT ..."""
155 self.assertIn(expectedOutput, result.output)
157 def test_addElipsisToNargs(self):
158 """Verify that MWOption adds " ..." after the option metavar when
159 `nargs` is set to more than 1 and less than 1.
161 The default behavior of click is to add elipsis when nargs does not
162 equal 1, but it does not put a space before the elipsis and we prefer
163 a space between the metavar and the elipsis."""
164 for numberOfArgs in (0, 1, 2): # nargs must be >= 0 for an option
166 @click.command()
167 @click.option("--things", cls=MWOption, nargs=numberOfArgs)
168 def cmd(things):
169 pass
171 result = self.runner.invoke(cmd, ["--help"])
172 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
173 expectedOutput = f"""Options:
174 --things TEXT{' ...' if numberOfArgs != 1 else ''}"""
175 self.assertIn(expectedOutput, result.output)
178class MWArgumentDecoratorTest(unittest.TestCase):
179 """Tests for the MWArgumentDecorator class."""
181 things_argument = MWArgumentDecorator("things")
182 otherHelpText = "Help text for OTHER."
183 other_argument = MWArgumentDecorator("other", help=otherHelpText)
185 def setUp(self):
186 self.runner = LogCliRunner()
188 def test_help(self):
189 """Verify expected help text output.
191 Verify argument help gets inserted after the usage, in the order
192 arguments are declared.
194 Verify that MWArgument adds " ..." after the option metavar when
195 `nargs` != 1. The default behavior of click is to add elipsis when
196 nargs does not equal 1, but it does not put a space before the elipsis
197 and we prefer a space between the metavar and the elipsis."""
198 # nargs can be -1 for any number of args, or >= 1 for a specified
199 # number of arguments.
201 helpText = "Things help text."
202 for numberOfArgs in (-1, 1, 2):
203 for required in (True, False):
205 @click.command()
206 @self.things_argument(required=required, nargs=numberOfArgs, help=helpText)
207 @self.other_argument()
208 def cmd(things, other):
209 """Cmd help text."""
210 pass
212 result = self.runner.invoke(cmd, ["--help"])
213 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
214 expectedOutput = f"""Usage: cmd [OPTIONS] {'THINGS' if required else '[THINGS]'} {'... ' if numberOfArgs != 1 else ''}OTHER
216 Cmd help text.
218 {helpText}
220 {self.otherHelpText}
221"""
222 self.assertIn(expectedOutput, result.output)
224 def testUse(self):
225 """Test using the MWArgumentDecorator with a command."""
226 mock = MagicMock()
228 @click.command()
229 @self.things_argument()
230 def cli(things):
231 mock(things)
233 self.runner = click.testing.CliRunner()
234 result = self.runner.invoke(cli, ("foo"))
235 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
236 mock.assert_called_with("foo")
239class MWOptionDecoratorTest(unittest.TestCase):
240 """Tests for the MWOptionDecorator class."""
242 test_option = MWOptionDecorator("-t", "--test", multiple=True)
244 def testGetName(self):
245 """Test getting the option name from the MWOptionDecorator."""
246 self.assertEqual(self.test_option.name(), "test")
248 def testGetOpts(self):
249 """Test getting the option flags from the MWOptionDecorator."""
250 self.assertEqual(self.test_option.opts(), ["-t", "--test"])
252 def testUse(self):
253 """Test using the MWOptionDecorator with a command."""
254 mock = MagicMock()
256 @click.command()
257 @self.test_option()
258 def cli(test):
259 mock(test)
261 self.runner = click.testing.CliRunner()
262 result = self.runner.invoke(cli, ("-t", "foo"))
263 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
264 mock.assert_called_with(("foo",))
266 def testOverride(self):
267 """Test using the MWOptionDecorator with a command and overriding one
268 of the default values."""
269 mock = MagicMock()
271 @click.command()
272 @self.test_option(multiple=False)
273 def cli(test):
274 mock(test)
276 self.runner = click.testing.CliRunner()
277 result = self.runner.invoke(cli, ("-t", "foo"))
278 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
279 mock.assert_called_with("foo")
282class SectionOptionTest(unittest.TestCase):
283 """Tests for the option_section decorator that inserts section break
284 headings between options in the --help output of a command."""
286 @staticmethod
287 @click.command()
288 @click.option("--foo")
289 @option_section("Section break between metasyntactic variables.")
290 @click.option("--bar")
291 def cli(foo, bar):
292 pass
294 def setUp(self):
295 self.runner = click.testing.CliRunner()
297 def test_section_help(self):
298 """Verify that the section break is printed in the help output in the
299 expected location and with expected formatting."""
300 result = self.runner.invoke(self.cli, ["--help"])
301 # \x20 is a space, added explicitly below to prevent the
302 # normally-helpful editor setting "remove trailing whitespace" from
303 # stripping it out in this case. (The blank line with 2 spaces is an
304 # artifact of how click and our code generate help text.)
305 expected = """Options:
306 --foo TEXT
307\x20\x20
308Section break between metasyntactic variables.
309 --bar TEXT"""
310 self.assertIn(expected, result.output)
312 def test_section_function(self):
313 """Verify that the section does not cause any arguments to be passed to
314 the command function.
316 The command function `cli` implementation inputs `foo` and `bar`, but
317 does accept an argument for the section. When the command is invoked
318 and the function called it should result in exit_code=0 (not 1 with a
319 missing argument error).
320 """
321 result = self.runner.invoke(self.cli, [])
322 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
325class MWPathTest(unittest.TestCase):
326 def getCmd(self, exists):
327 @click.command()
328 @click.option("--name", type=MWPath(exists=exists))
329 def cmd(name):
330 pass
332 return cmd
334 def setUp(self):
335 self.runner = click.testing.CliRunner()
337 def test_exist(self):
338 """Test the exist argument, verify that True means the file must exist,
339 False means the file must not exist, and None means that the file may
340 or may not exist."""
341 with self.runner.isolated_filesystem():
342 mustExistCmd = self.getCmd(exists=True)
343 mayExistCmd = self.getCmd(exists=None)
344 mustNotExistCmd = self.getCmd(exists=False)
345 args = ["--name", "foo.txt"]
347 result = self.runner.invoke(mustExistCmd, args)
348 self.assertNotEqual(result.exit_code, 0, clickResultMsg(result))
349 self.assertRegex(result.output, """['"]foo.txt['"] does not exist.""")
351 result = self.runner.invoke(mayExistCmd, args)
352 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
354 result = self.runner.invoke(mustNotExistCmd, args)
355 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
357 # isolated_filesystem runs in a temporary directory, when it is
358 # removed everything inside will be removed.
359 with open("foo.txt", "w") as _:
360 result = self.runner.invoke(mustExistCmd, args)
361 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
363 result = self.runner.invoke(mayExistCmd, args)
364 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
366 result = self.runner.invoke(mustNotExistCmd, args)
367 self.assertNotEqual(result.exit_code, 0, clickResultMsg(result))
368 self.assertIn('"foo.txt" should not exist.', result.output)
371class MWCommandTest(unittest.TestCase):
372 def setUp(self):
373 self.runner = click.testing.CliRunner()
374 self.ctx = None
376 def testCaptureOptions(self):
377 """Test that command options are captured in order."""
379 @click.command(cls=MWCommand)
380 @click.argument("ARG_A", required=False)
381 @click.argument("ARG_B", required=False)
382 @click.option("-o", "--an-option")
383 @click.option("-s", "--second-option")
384 @click.option("-f", is_flag=True)
385 @click.option("-d/-n", "--do/--do-not", "do_or_do_not")
386 @click.option("--multi", multiple=True)
387 @click.pass_context
388 def cmd(ctx, arg_a, arg_b, an_option, second_option, f, do_or_do_not, multi):
389 self.assertIsNotNone(ctx)
390 self.ctx = ctx
392 # When `expected` is `None`, the expected args are exactly the same as
393 # as the result of `args.split()`. If `expected` is not `None` then
394 # the expected args are the same as `expected.split()`.
395 for args, expected in (
396 ("--an-option foo --second-option bar", None),
397 ("--second-option bar --an-option foo", None),
398 ("--an-option foo", None),
399 ("--second-option bar", None),
400 ("--an-option foo -f --second-option bar", None),
401 ("-o foo -s bar", "--an-option foo --second-option bar"),
402 ("--an-option foo -f -s bar", "--an-option foo -f --second-option bar"),
403 ("--an-option=foo", "--an-option foo"),
404 # NB when using a short flag everything that follows, including an
405 # equals sign, is part of the value!
406 ("-o=foo", "--an-option =foo"),
407 ("--do", None),
408 ("--do-not", None),
409 ("-d", "--do"),
410 ("-n", "--do-not"),
411 # Arguments always come last, but the order of Arguments is still
412 # preserved:
413 ("myarg --an-option foo", "--an-option foo myarg"),
414 ("argA --an-option foo", "--an-option foo argA"),
415 ("argA argB --an-option foo", "--an-option foo argA argB"),
416 ("argA --an-option foo argB", "--an-option foo argA argB"),
417 ("--an-option foo argA argB", "--an-option foo argA argB"),
418 ("--multi one --multi two", None),
419 ):
420 split_args = args.split()
421 expected_args = split_args if expected is None else expected.split()
422 result = self.runner.invoke(cmd, split_args)
423 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
424 self.assertIsNotNone(self.ctx)
425 ctx_obj = MWCtxObj.getFrom(self.ctx)
426 self.assertEqual(ctx_obj.args, expected_args)
429if __name__ == "__main__": 429 ↛ 430line 429 didn't jump to line 430, because the condition on line 429 was never true
430 unittest.main()