Coverage for tests/test_cliUtils.py : 32%

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 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 @staticmethod
46 @click.command()
47 # Use custom help in the arguments so that any changes to default help text
48 # 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 def test_help(self):
56 """Tests `utils.addArgumentHelp` and its use in repo_argument and
57 directory_argument; verifies that the argument help gets added to the
58 command fucntion help, and that it's added in the correct order. See
59 addArgumentHelp for more details."""
60 runner = LogCliRunner()
61 result = runner.invoke(ArgumentHelpGeneratorTestCase.cli, ["--help"])
62 expected = """Usage: cli [OPTIONS] REPO DIRECTORY
64 The cli help message.
66 repo help text
68 directory help text
70Options:
71 --help Show this message and exit.
72"""
73 self.assertIn(expected, result.output)
76class UnwrapStringTestCase(unittest.TestCase):
78 def test_leadingNewline(self):
79 testStr = """
80 foo bar
81 baz """
82 self.assertEqual(unwrap(testStr), "foo bar baz")
84 def test_leadingContent(self):
85 testStr = """foo bar
86 baz """
87 self.assertEqual(unwrap(testStr), "foo bar baz")
89 def test_trailingNewline(self):
90 testStr = """
91 foo bar
92 baz
93 """
94 self.assertEqual(unwrap(testStr), "foo bar baz")
96 def test_oneLine(self):
97 testStr = """foo bar baz"""
98 self.assertEqual(unwrap(testStr), "foo bar baz")
100 def test_oneLineWithLeading(self):
101 testStr = """
102 foo bar baz"""
103 self.assertEqual(unwrap(testStr), "foo bar baz")
105 def test_oneLineWithTrailing(self):
106 testStr = """foo bar baz
107 """
108 self.assertEqual(unwrap(testStr), "foo bar baz")
110 def test_lineBreaks(self):
111 testStr = """foo bar
112 baz
114 boz
116 qux"""
117 self.assertEqual(unwrap(testStr), "foo bar baz\n\nboz\n\nqux")
120class MWOptionTest(unittest.TestCase):
122 def setUp(self):
123 self.runner = LogCliRunner()
125 def test_addElipsisToMultiple(self):
126 """Verify that MWOption adds elipsis to the option metavar when
127 `multiple=True`
129 The default behavior of click is to not add elipsis to options that
130 have `multiple=True`."""
132 @click.command()
133 @click.option("--things", cls=MWOption, multiple=True)
134 def cmd(things):
135 pass
136 result = self.runner.invoke(cmd, ["--help"])
137 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
138 expectedOutut = """Options:
139 --things TEXT ..."""
140 self.assertIn(expectedOutut, result.output)
142 def test_addElipsisToNargs(self):
143 """Verify that MWOption adds " ..." after the option metavar when
144 `nargs` is set to more than 1 and less than 1.
146 The default behavior of click is to add elipsis when nargs does not
147 equal 1, but it does not put a space before the elipsis and we prefer
148 a space between the metavar and the elipsis."""
149 for numberOfArgs in (0, 1, 2): # nargs must be >= 0 for an option
151 @click.command()
152 @click.option("--things", cls=MWOption, nargs=numberOfArgs)
153 def cmd(things):
154 pass
155 result = self.runner.invoke(cmd, ["--help"])
156 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
157 expectedOutut = f"""Options:
158 --things TEXT{' ...' if numberOfArgs != 1 else ''}"""
159 self.assertIn(expectedOutut, result.output)
162class MWArgumentDecoratorTest(unittest.TestCase):
163 """Tests for the MWArgumentDecorator class."""
165 things_argument = MWArgumentDecorator("things")
166 otherHelpText = "Help text for OTHER."
167 other_argument = MWArgumentDecorator("other", help=otherHelpText)
169 def setUp(self):
170 self.runner = LogCliRunner()
172 def test_help(self):
173 """Verify expected help text output.
175 Verify argument help gets inserted after the usage, in the order
176 arguments are declared.
178 Verify that MWArgument adds " ..." after the option metavar when
179 `nargs` != 1. The default behavior of click is to add elipsis when
180 nargs does not equal 1, but it does not put a space before the elipsis
181 and we prefer a space between the metavar and the elipsis."""
182 # nargs can be -1 for any number of args, or >= 1 for a specified
183 # number of arguments.
185 helpText = "Things help text."
186 for numberOfArgs in (-1, 1, 2):
187 for required in (True, False):
189 @click.command()
190 @self.things_argument(required=required, nargs=numberOfArgs, help=helpText)
191 @self.other_argument()
192 def cmd(things, other):
193 """Cmd help text."""
194 pass
195 result = self.runner.invoke(cmd, ["--help"])
196 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
197 expectedOutut = (f"""Usage: cmd [OPTIONS] {'THINGS' if required else '[THINGS]'} {'... ' if numberOfArgs != 1 else ''}OTHER
199 Cmd help text.
201 {helpText}
203 {self.otherHelpText}
204""")
205 self.assertIn(expectedOutut, result.output)
207 def testUse(self):
208 """Test using the MWArgumentDecorator with a command."""
209 mock = MagicMock()
211 @click.command()
212 @self.things_argument()
213 def cli(things):
214 mock(things)
215 self.runner = click.testing.CliRunner()
216 result = self.runner.invoke(cli, ("foo"))
217 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
218 mock.assert_called_with("foo")
221class MWOptionDecoratorTest(unittest.TestCase):
222 """Tests for the MWOptionDecorator class."""
224 test_option = MWOptionDecorator("-t", "--test", multiple=True)
226 def testGetName(self):
227 """Test getting the option name from the MWOptionDecorator."""
228 self.assertEqual(self.test_option.name(), "test")
230 def testGetOpts(self):
231 """Test getting the option flags from the MWOptionDecorator."""
232 self.assertEqual(self.test_option.opts(), ["-t", "--test"])
234 def testUse(self):
235 """Test using the MWOptionDecorator with a command."""
236 mock = MagicMock()
238 @click.command()
239 @self.test_option()
240 def cli(test):
241 mock(test)
242 self.runner = click.testing.CliRunner()
243 result = self.runner.invoke(cli, ("-t", "foo"))
244 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
245 mock.assert_called_with(("foo",))
247 def testOverride(self):
248 """Test using the MWOptionDecorator with a command and overriding one
249 of the default values."""
250 mock = MagicMock()
252 @click.command()
253 @self.test_option(multiple=False)
254 def cli(test):
255 mock(test)
256 self.runner = click.testing.CliRunner()
257 result = self.runner.invoke(cli, ("-t", "foo"))
258 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
259 mock.assert_called_with("foo")
262class SectionOptionTest(unittest.TestCase):
263 """Tests for the option_section decorator that inserts section break
264 headings between options in the --help output of a command."""
266 @staticmethod
267 @click.command()
268 @click.option("--foo")
269 @option_section("Section break between metasyntactic variables.")
270 @click.option("--bar")
271 def cli(foo, bar):
272 pass
274 def setUp(self):
275 self.runner = click.testing.CliRunner()
277 def test_section_help(self):
278 """Verify that the section break is printed in the help output in the
279 expected location and with expected formatting."""
280 result = self.runner.invoke(self.cli, ["--help"])
281 # \x20 is a space, added explicity below to prevent the
282 # normally-helpful editor setting "remove trailing whitespace" from
283 # stripping it out in this case. (The blank line with 2 spaces is an
284 # artifact of how click and our code generate help text.)
285 expected = """Options:
286 --foo TEXT
287\x20\x20
288Section break between metasyntactic variables.
289 --bar TEXT"""
290 self.assertIn(expected, result.output)
292 def test_section_function(self):
293 """Verify that the section does not cause any arguments to be passed to
294 the command function.
296 The command function `cli` implementation inputs `foo` and `bar`, but
297 does accept an argument for the section. When the command is invoked
298 and the function called it should result in exit_code=0 (not 1 with a
299 missing argument error).
300 """
301 result = self.runner.invoke(self.cli, [])
302 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
305class MWPathTest(unittest.TestCase):
307 def getCmd(self, exists):
309 @click.command()
310 @click.option("--name", type=MWPath(exists=exists))
311 def cmd(name):
312 pass
313 return cmd
315 def setUp(self):
316 self.runner = click.testing.CliRunner()
318 def test_exist(self):
319 """Test the exist argument, verify that True means the file must exist,
320 False means the file must not exist, and None means that the file may
321 or may not exist."""
322 with self.runner.isolated_filesystem():
323 mustExistCmd = self.getCmd(exists=True)
324 mayExistCmd = self.getCmd(exists=None)
325 mustNotExistCmd = self.getCmd(exists=False)
326 args = ["--name", "foo.txt"]
328 result = self.runner.invoke(mustExistCmd, args)
329 self.assertNotEqual(result.exit_code, 0, clickResultMsg(result))
330 self.assertRegex(result.output, """['"]foo.txt['"] does not exist.""")
332 result = self.runner.invoke(mayExistCmd, args)
333 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
335 result = self.runner.invoke(mustNotExistCmd, args)
336 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
338 # isolated_filesystem runs in a temporary directory, when it is
339 # removed everything inside will be removed.
340 with open("foo.txt", "w") as _:
341 result = self.runner.invoke(mustExistCmd, args)
342 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
344 result = self.runner.invoke(mayExistCmd, args)
345 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
347 result = self.runner.invoke(mustNotExistCmd, args)
348 self.assertNotEqual(result.exit_code, 0, clickResultMsg(result))
349 self.assertIn('"foo.txt" should not exist.', result.output)
352if __name__ == "__main__": 352 ↛ 353line 352 didn't jump to line 353, because the condition on line 352 was never true
353 unittest.main()