Coverage for tests/test_cliUtils.py: 28%
Shortcuts 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
Shortcuts 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 MagicMock
30from lsst.daf.butler.cli.utils import (
31 clickResultMsg,
32 LogCliRunner,
33 MWArgumentDecorator,
34 MWOption,
35 MWOptionDecorator,
36 MWPath,
37 option_section,
38 unwrap
39)
40from lsst.daf.butler.cli.opt import directory_argument, repo_argument
43class 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
68 self.runTest(cli)
70 def runTest(self, cli):
71 """Tests `utils.addArgumentHelp` and its use in repo_argument and
72 directory_argument; verifies that the argument help gets added to the
73 command function help, and that it's added in the correct order. See
74 addArgumentHelp for more details."""
75 expected = """Usage: cli [OPTIONS] REPO DIRECTORY
77 The cli help message.
79 repo help text
81 directory help text
83Options:
84 --help Show this message and exit.
85"""
86 runner = LogCliRunner()
87 result = runner.invoke(cli, ["--help"])
88 self.assertIn(expected, result.output)
91class 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):
137 def setUp(self):
138 self.runner = LogCliRunner()
140 def test_addElipsisToMultiple(self):
141 """Verify that MWOption adds elipsis to the option metavar when
142 `multiple=True`
144 The default behavior of click is to not add elipsis to options that
145 have `multiple=True`."""
147 @click.command()
148 @click.option("--things", cls=MWOption, multiple=True)
149 def cmd(things):
150 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
170 result = self.runner.invoke(cmd, ["--help"])
171 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
172 expectedOutput = f"""Options:
173 --things TEXT{' ...' if numberOfArgs != 1 else ''}"""
174 self.assertIn(expectedOutput, result.output)
177class MWArgumentDecoratorTest(unittest.TestCase):
178 """Tests for the MWArgumentDecorator class."""
180 things_argument = MWArgumentDecorator("things")
181 otherHelpText = "Help text for OTHER."
182 other_argument = MWArgumentDecorator("other", help=otherHelpText)
184 def setUp(self):
185 self.runner = LogCliRunner()
187 def test_help(self):
188 """Verify expected help text output.
190 Verify argument help gets inserted after the usage, in the order
191 arguments are declared.
193 Verify that MWArgument adds " ..." after the option metavar when
194 `nargs` != 1. The default behavior of click is to add elipsis when
195 nargs does not equal 1, but it does not put a space before the elipsis
196 and we prefer a space between the metavar and the elipsis."""
197 # nargs can be -1 for any number of args, or >= 1 for a specified
198 # number of arguments.
200 helpText = "Things help text."
201 for numberOfArgs in (-1, 1, 2):
202 for required in (True, False):
204 @click.command()
205 @self.things_argument(required=required, nargs=numberOfArgs, help=helpText)
206 @self.other_argument()
207 def cmd(things, other):
208 """Cmd help text."""
209 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)
230 self.runner = click.testing.CliRunner()
231 result = self.runner.invoke(cli, ("foo"))
232 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
233 mock.assert_called_with("foo")
236class MWOptionDecoratorTest(unittest.TestCase):
237 """Tests for the MWOptionDecorator class."""
239 test_option = MWOptionDecorator("-t", "--test", multiple=True)
241 def testGetName(self):
242 """Test getting the option name from the MWOptionDecorator."""
243 self.assertEqual(self.test_option.name(), "test")
245 def testGetOpts(self):
246 """Test getting the option flags from the MWOptionDecorator."""
247 self.assertEqual(self.test_option.opts(), ["-t", "--test"])
249 def testUse(self):
250 """Test using the MWOptionDecorator with a command."""
251 mock = MagicMock()
253 @click.command()
254 @self.test_option()
255 def cli(test):
256 mock(test)
257 self.runner = click.testing.CliRunner()
258 result = self.runner.invoke(cli, ("-t", "foo"))
259 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
260 mock.assert_called_with(("foo",))
262 def testOverride(self):
263 """Test using the MWOptionDecorator with a command and overriding one
264 of the default values."""
265 mock = MagicMock()
267 @click.command()
268 @self.test_option(multiple=False)
269 def cli(test):
270 mock(test)
271 self.runner = click.testing.CliRunner()
272 result = self.runner.invoke(cli, ("-t", "foo"))
273 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
274 mock.assert_called_with("foo")
277class SectionOptionTest(unittest.TestCase):
278 """Tests for the option_section decorator that inserts section break
279 headings between options in the --help output of a command."""
281 @staticmethod
282 @click.command()
283 @click.option("--foo")
284 @option_section("Section break between metasyntactic variables.")
285 @click.option("--bar")
286 def cli(foo, bar):
287 pass
289 def setUp(self):
290 self.runner = click.testing.CliRunner()
292 def test_section_help(self):
293 """Verify that the section break is printed in the help output in the
294 expected location and with expected formatting."""
295 result = self.runner.invoke(self.cli, ["--help"])
296 # \x20 is a space, added explicitly below to prevent the
297 # normally-helpful editor setting "remove trailing whitespace" from
298 # stripping it out in this case. (The blank line with 2 spaces is an
299 # artifact of how click and our code generate help text.)
300 expected = """Options:
301 --foo TEXT
302\x20\x20
303Section break between metasyntactic variables.
304 --bar TEXT"""
305 self.assertIn(expected, result.output)
307 def test_section_function(self):
308 """Verify that the section does not cause any arguments to be passed to
309 the command function.
311 The command function `cli` implementation inputs `foo` and `bar`, but
312 does accept an argument for the section. When the command is invoked
313 and the function called it should result in exit_code=0 (not 1 with a
314 missing argument error).
315 """
316 result = self.runner.invoke(self.cli, [])
317 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
320class MWPathTest(unittest.TestCase):
322 def getCmd(self, exists):
324 @click.command()
325 @click.option("--name", type=MWPath(exists=exists))
326 def cmd(name):
327 pass
328 return cmd
330 def setUp(self):
331 self.runner = click.testing.CliRunner()
333 def test_exist(self):
334 """Test the exist argument, verify that True means the file must exist,
335 False means the file must not exist, and None means that the file may
336 or may not exist."""
337 with self.runner.isolated_filesystem():
338 mustExistCmd = self.getCmd(exists=True)
339 mayExistCmd = self.getCmd(exists=None)
340 mustNotExistCmd = self.getCmd(exists=False)
341 args = ["--name", "foo.txt"]
343 result = self.runner.invoke(mustExistCmd, args)
344 self.assertNotEqual(result.exit_code, 0, clickResultMsg(result))
345 self.assertRegex(result.output, """['"]foo.txt['"] does not exist.""")
347 result = self.runner.invoke(mayExistCmd, args)
348 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
350 result = self.runner.invoke(mustNotExistCmd, args)
351 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
353 # isolated_filesystem runs in a temporary directory, when it is
354 # removed everything inside will be removed.
355 with open("foo.txt", "w") as _:
356 result = self.runner.invoke(mustExistCmd, args)
357 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
359 result = self.runner.invoke(mayExistCmd, args)
360 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
362 result = self.runner.invoke(mustNotExistCmd, args)
363 self.assertNotEqual(result.exit_code, 0, clickResultMsg(result))
364 self.assertIn('"foo.txt" should not exist.', result.output)
367if __name__ == "__main__": 367 ↛ 368line 367 didn't jump to line 368, because the condition on line 367 was never true
368 unittest.main()