Coverage for tests/test_cliUtils.py: 33%
174 statements
« prev ^ index » next coverage.py v6.4, created at 2022-05-24 02:27 -0700
« prev ^ index » next coverage.py v6.4, created at 2022-05-24 02:27 -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 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 MWOption,
34 MWOptionDecorator,
35 MWPath,
36 clickResultMsg,
37 option_section,
38 unwrap,
39)
42class ArgumentHelpGeneratorTestCase(unittest.TestCase):
43 def testHelp(self):
44 @click.command()
45 # Use custom help in the arguments so that any changes to default help
46 # text do not break this test unnecessarily.
47 @repo_argument(help="repo help text")
48 @directory_argument(help="directory help text")
49 def cli():
50 """The cli help message."""
51 pass
53 self.runTest(cli)
55 def testHelpWrapped(self):
56 @click.command()
57 # Use custom help in the arguments so that any changes to default help
58 # text do not break this test unnecessarily.
59 @repo_argument(help="repo help text")
60 @directory_argument(help="directory help text")
61 def cli():
62 """The cli
63 help
64 message."""
65 pass
67 self.runTest(cli)
69 def runTest(self, cli):
70 """Tests `utils.addArgumentHelp` and its use in repo_argument and
71 directory_argument; verifies that the argument help gets added to the
72 command function help, and that it's added in the correct order. See
73 addArgumentHelp for more details."""
74 expected = """Usage: cli [OPTIONS] REPO DIRECTORY
76 The cli help message.
78 repo help text
80 directory help text
82Options:
83 --help Show this message and exit.
84"""
85 runner = LogCliRunner()
86 result = runner.invoke(cli, ["--help"])
87 self.assertIn(expected, result.output)
90class UnwrapStringTestCase(unittest.TestCase):
91 def test_leadingNewline(self):
92 testStr = """
93 foo bar
94 baz """
95 self.assertEqual(unwrap(testStr), "foo bar baz")
97 def test_leadingContent(self):
98 testStr = """foo bar
99 baz """
100 self.assertEqual(unwrap(testStr), "foo bar baz")
102 def test_trailingNewline(self):
103 testStr = """
104 foo bar
105 baz
106 """
107 self.assertEqual(unwrap(testStr), "foo bar baz")
109 def test_oneLine(self):
110 testStr = """foo bar baz"""
111 self.assertEqual(unwrap(testStr), "foo bar baz")
113 def test_oneLineWithLeading(self):
114 testStr = """
115 foo bar baz"""
116 self.assertEqual(unwrap(testStr), "foo bar baz")
118 def test_oneLineWithTrailing(self):
119 testStr = """foo bar baz
120 """
121 self.assertEqual(unwrap(testStr), "foo bar baz")
123 def test_lineBreaks(self):
124 testStr = """foo bar
125 baz
127 boz
129 qux"""
130 self.assertEqual(unwrap(testStr), "foo bar baz\n\nboz\n\nqux")
133class MWOptionTest(unittest.TestCase):
134 def setUp(self):
135 self.runner = LogCliRunner()
137 def test_addElipsisToMultiple(self):
138 """Verify that MWOption adds elipsis to the option metavar when
139 `multiple=True`
141 The default behavior of click is to not add elipsis to options that
142 have `multiple=True`."""
144 @click.command()
145 @click.option("--things", cls=MWOption, multiple=True)
146 def cmd(things):
147 pass
149 result = self.runner.invoke(cmd, ["--help"])
150 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
151 expectedOutput = """Options:
152 --things TEXT ..."""
153 self.assertIn(expectedOutput, result.output)
155 def test_addElipsisToNargs(self):
156 """Verify that MWOption adds " ..." after the option metavar when
157 `nargs` is set to more than 1 and less than 1.
159 The default behavior of click is to add elipsis when nargs does not
160 equal 1, but it does not put a space before the elipsis and we prefer
161 a space between the metavar and the elipsis."""
162 for numberOfArgs in (0, 1, 2): # nargs must be >= 0 for an option
164 @click.command()
165 @click.option("--things", cls=MWOption, nargs=numberOfArgs)
166 def cmd(things):
167 pass
169 result = self.runner.invoke(cmd, ["--help"])
170 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
171 expectedOutput = f"""Options:
172 --things TEXT{' ...' if numberOfArgs != 1 else ''}"""
173 self.assertIn(expectedOutput, result.output)
176class MWArgumentDecoratorTest(unittest.TestCase):
177 """Tests for the MWArgumentDecorator class."""
179 things_argument = MWArgumentDecorator("things")
180 otherHelpText = "Help text for OTHER."
181 other_argument = MWArgumentDecorator("other", help=otherHelpText)
183 def setUp(self):
184 self.runner = LogCliRunner()
186 def test_help(self):
187 """Verify expected help text output.
189 Verify argument help gets inserted after the usage, in the order
190 arguments are declared.
192 Verify that MWArgument adds " ..." after the option metavar when
193 `nargs` != 1. The default behavior of click is to add elipsis when
194 nargs does not equal 1, but it does not put a space before the elipsis
195 and we prefer a space between the metavar and the elipsis."""
196 # nargs can be -1 for any number of args, or >= 1 for a specified
197 # number of arguments.
199 helpText = "Things help text."
200 for numberOfArgs in (-1, 1, 2):
201 for required in (True, False):
203 @click.command()
204 @self.things_argument(required=required, nargs=numberOfArgs, help=helpText)
205 @self.other_argument()
206 def cmd(things, other):
207 """Cmd help text."""
208 pass
210 result = self.runner.invoke(cmd, ["--help"])
211 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
212 expectedOutput = f"""Usage: cmd [OPTIONS] {'THINGS' if required else '[THINGS]'} {'... ' if numberOfArgs != 1 else ''}OTHER
214 Cmd help text.
216 {helpText}
218 {self.otherHelpText}
219"""
220 self.assertIn(expectedOutput, result.output)
222 def testUse(self):
223 """Test using the MWArgumentDecorator with a command."""
224 mock = MagicMock()
226 @click.command()
227 @self.things_argument()
228 def cli(things):
229 mock(things)
231 self.runner = click.testing.CliRunner()
232 result = self.runner.invoke(cli, ("foo"))
233 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
234 mock.assert_called_with("foo")
237class MWOptionDecoratorTest(unittest.TestCase):
238 """Tests for the MWOptionDecorator class."""
240 test_option = MWOptionDecorator("-t", "--test", multiple=True)
242 def testGetName(self):
243 """Test getting the option name from the MWOptionDecorator."""
244 self.assertEqual(self.test_option.name(), "test")
246 def testGetOpts(self):
247 """Test getting the option flags from the MWOptionDecorator."""
248 self.assertEqual(self.test_option.opts(), ["-t", "--test"])
250 def testUse(self):
251 """Test using the MWOptionDecorator with a command."""
252 mock = MagicMock()
254 @click.command()
255 @self.test_option()
256 def cli(test):
257 mock(test)
259 self.runner = click.testing.CliRunner()
260 result = self.runner.invoke(cli, ("-t", "foo"))
261 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
262 mock.assert_called_with(("foo",))
264 def testOverride(self):
265 """Test using the MWOptionDecorator with a command and overriding one
266 of the default values."""
267 mock = MagicMock()
269 @click.command()
270 @self.test_option(multiple=False)
271 def cli(test):
272 mock(test)
274 self.runner = click.testing.CliRunner()
275 result = self.runner.invoke(cli, ("-t", "foo"))
276 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
277 mock.assert_called_with("foo")
280class SectionOptionTest(unittest.TestCase):
281 """Tests for the option_section decorator that inserts section break
282 headings between options in the --help output of a command."""
284 @staticmethod
285 @click.command()
286 @click.option("--foo")
287 @option_section("Section break between metasyntactic variables.")
288 @click.option("--bar")
289 def cli(foo, bar):
290 pass
292 def setUp(self):
293 self.runner = click.testing.CliRunner()
295 def test_section_help(self):
296 """Verify that the section break is printed in the help output in the
297 expected location and with expected formatting."""
298 result = self.runner.invoke(self.cli, ["--help"])
299 # \x20 is a space, added explicitly below to prevent the
300 # normally-helpful editor setting "remove trailing whitespace" from
301 # stripping it out in this case. (The blank line with 2 spaces is an
302 # artifact of how click and our code generate help text.)
303 expected = """Options:
304 --foo TEXT
305\x20\x20
306Section break between metasyntactic variables.
307 --bar TEXT"""
308 self.assertIn(expected, result.output)
310 def test_section_function(self):
311 """Verify that the section does not cause any arguments to be passed to
312 the command function.
314 The command function `cli` implementation inputs `foo` and `bar`, but
315 does accept an argument for the section. When the command is invoked
316 and the function called it should result in exit_code=0 (not 1 with a
317 missing argument error).
318 """
319 result = self.runner.invoke(self.cli, [])
320 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
323class MWPathTest(unittest.TestCase):
324 def getCmd(self, exists):
325 @click.command()
326 @click.option("--name", type=MWPath(exists=exists))
327 def cmd(name):
328 pass
330 return cmd
332 def setUp(self):
333 self.runner = click.testing.CliRunner()
335 def test_exist(self):
336 """Test the exist argument, verify that True means the file must exist,
337 False means the file must not exist, and None means that the file may
338 or may not exist."""
339 with self.runner.isolated_filesystem():
340 mustExistCmd = self.getCmd(exists=True)
341 mayExistCmd = self.getCmd(exists=None)
342 mustNotExistCmd = self.getCmd(exists=False)
343 args = ["--name", "foo.txt"]
345 result = self.runner.invoke(mustExistCmd, args)
346 self.assertNotEqual(result.exit_code, 0, clickResultMsg(result))
347 self.assertRegex(result.output, """['"]foo.txt['"] does not exist.""")
349 result = self.runner.invoke(mayExistCmd, args)
350 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
352 result = self.runner.invoke(mustNotExistCmd, args)
353 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
355 # isolated_filesystem runs in a temporary directory, when it is
356 # removed everything inside will be removed.
357 with open("foo.txt", "w") as _:
358 result = self.runner.invoke(mustExistCmd, args)
359 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
361 result = self.runner.invoke(mayExistCmd, args)
362 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
364 result = self.runner.invoke(mustNotExistCmd, args)
365 self.assertNotEqual(result.exit_code, 0, clickResultMsg(result))
366 self.assertIn('"foo.txt" should not exist.', result.output)
369if __name__ == "__main__": 369 ↛ 370line 369 didn't jump to line 370, because the condition on line 369 was never true
370 unittest.main()