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 import butler
31from lsst.daf.butler.cli.utils import (clickResultMsg, LogCliRunner, Mocker, mockEnvVar, MWArgumentDecorator,
32 MWOption, MWOptionDecorator, MWPath, option_section, unwrap)
33from lsst.daf.butler.cli.opt import directory_argument, repo_argument
36class MockerTestCase(unittest.TestCase):
38 def test_callMock(self):
39 """Test that a mocked subcommand calls the Mocker and can be verified.
40 """
41 runner = LogCliRunner(env=mockEnvVar)
42 result = runner.invoke(butler.cli, ["create", "repo"])
43 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
44 Mocker.mock.assert_called_with(repo="repo", seed_config=None, dimension_config=None,
45 standalone=False, override=False, outfile=None)
48class ArgumentHelpGeneratorTestCase(unittest.TestCase):
50 @staticmethod
51 @click.command()
52 # Use custom help in the arguments so that any changes to default help text
53 # do not break this test unnecessarily.
54 @repo_argument(help="repo help text")
55 @directory_argument(help="directory help text")
56 def cli():
57 """The cli help message."""
58 pass
60 def test_help(self):
61 """Tests `utils.addArgumentHelp` and its use in repo_argument and
62 directory_argument; verifies that the argument help gets added to the
63 command fucntion help, and that it's added in the correct order. See
64 addArgumentHelp for more details."""
65 runner = LogCliRunner()
66 result = runner.invoke(ArgumentHelpGeneratorTestCase.cli, ["--help"])
67 expected = """Usage: cli [OPTIONS] REPO DIRECTORY
69 The cli help message.
71 repo help text
73 directory help text
75Options:
76 --help Show this message and exit.
77"""
78 self.assertIn(expected, result.output)
81class UnwrapStringTestCase(unittest.TestCase):
83 def test_leadingNewline(self):
84 testStr = """
85 foo bar
86 baz """
87 self.assertEqual(unwrap(testStr), "foo bar baz")
89 def test_leadingContent(self):
90 testStr = """foo bar
91 baz """
92 self.assertEqual(unwrap(testStr), "foo bar baz")
94 def test_trailingNewline(self):
95 testStr = """
96 foo bar
97 baz
98 """
99 self.assertEqual(unwrap(testStr), "foo bar baz")
101 def test_oneLine(self):
102 testStr = """foo bar baz"""
103 self.assertEqual(unwrap(testStr), "foo bar baz")
105 def test_oneLineWithLeading(self):
106 testStr = """
107 foo bar baz"""
108 self.assertEqual(unwrap(testStr), "foo bar baz")
110 def test_oneLineWithTrailing(self):
111 testStr = """foo bar baz
112 """
113 self.assertEqual(unwrap(testStr), "foo bar baz")
115 def test_lineBreaks(self):
116 testStr = """foo bar
117 baz
119 boz
121 qux"""
122 self.assertEqual(unwrap(testStr), "foo bar baz\n\nboz\n\nqux")
125class MWOptionTest(unittest.TestCase):
127 def setUp(self):
128 self.runner = LogCliRunner()
130 def test_addElipsisToMultiple(self):
131 """Verify that MWOption adds elipsis to the option metavar when
132 `multiple=True`
134 The default behavior of click is to not add elipsis to options that
135 have `multiple=True`."""
137 @click.command()
138 @click.option("--things", cls=MWOption, multiple=True)
139 def cmd(things):
140 pass
141 result = self.runner.invoke(cmd, ["--help"])
142 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
143 expectedOutut = """Options:
144 --things TEXT ..."""
145 self.assertIn(expectedOutut, result.output)
147 def test_addElipsisToNargs(self):
148 """Verify that MWOption adds " ..." after the option metavar when
149 `nargs` is set to more than 1 and less than 1.
151 The default behavior of click is to add elipsis when nargs does not
152 equal 1, but it does not put a space before the elipsis and we prefer
153 a space between the metavar and the elipsis."""
154 for numberOfArgs in (0, 1, 2): # nargs must be >= 0 for an option
156 @click.command()
157 @click.option("--things", cls=MWOption, nargs=numberOfArgs)
158 def cmd(things):
159 pass
160 result = self.runner.invoke(cmd, ["--help"])
161 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
162 expectedOutut = f"""Options:
163 --things TEXT{' ...' if numberOfArgs != 1 else ''}"""
164 self.assertIn(expectedOutut, result.output)
167class MWArgumentDecoratorTest(unittest.TestCase):
168 """Tests for the MWArgumentDecorator class."""
170 things_argument = MWArgumentDecorator("things")
171 otherHelpText = "Help text for OTHER."
172 other_argument = MWArgumentDecorator("other", help=otherHelpText)
174 def setUp(self):
175 self.runner = LogCliRunner()
177 def test_help(self):
178 """Verify expected help text output.
180 Verify argument help gets inserted after the usage, in the order
181 arguments are declared.
183 Verify that MWArgument adds " ..." after the option metavar when
184 `nargs` != 1. The default behavior of click is to add elipsis when
185 nargs does not equal 1, but it does not put a space before the elipsis
186 and we prefer a space between the metavar and the elipsis."""
187 # nargs can be -1 for any number of args, or >= 1 for a specified
188 # number of arguments.
190 helpText = "Things help text."
191 for numberOfArgs in (-1, 1, 2):
192 for required in (True, False):
194 @click.command()
195 @self.things_argument(required=required, nargs=numberOfArgs, help=helpText)
196 @self.other_argument()
197 def cmd(things, other):
198 """Cmd help text."""
199 pass
200 result = self.runner.invoke(cmd, ["--help"])
201 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
202 expectedOutut = (f"""Usage: cmd [OPTIONS] {'THINGS' if required else '[THINGS]'} {'... ' if numberOfArgs != 1 else ''}OTHER
204 Cmd help text.
206 {helpText}
208 {self.otherHelpText}
209""")
210 self.assertIn(expectedOutut, result.output)
212 def testUse(self):
213 """Test using the MWArgumentDecorator with a command."""
214 mock = MagicMock()
216 @click.command()
217 @self.things_argument()
218 def cli(things):
219 mock(things)
220 self.runner = click.testing.CliRunner()
221 result = self.runner.invoke(cli, ("foo"))
222 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
223 mock.assert_called_with("foo")
226class MWOptionDecoratorTest(unittest.TestCase):
227 """Tests for the MWOptionDecorator class."""
229 test_option = MWOptionDecorator("-t", "--test", multiple=True)
231 def testGetName(self):
232 """Test getting the option name from the MWOptionDecorator."""
233 self.assertEqual(self.test_option.name(), "test")
235 def testGetOpts(self):
236 """Test getting the option flags from the MWOptionDecorator."""
237 self.assertEqual(self.test_option.opts(), ["-t", "--test"])
239 def testUse(self):
240 """Test using the MWOptionDecorator with a command."""
241 mock = MagicMock()
243 @click.command()
244 @self.test_option()
245 def cli(test):
246 mock(test)
247 self.runner = click.testing.CliRunner()
248 result = self.runner.invoke(cli, ("-t", "foo"))
249 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
250 mock.assert_called_with(("foo",))
252 def testOverride(self):
253 """Test using the MWOptionDecorator with a command and overriding one
254 of the default values."""
255 mock = MagicMock()
257 @click.command()
258 @self.test_option(multiple=False)
259 def cli(test):
260 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")
267class SectionOptionTest(unittest.TestCase):
268 """Tests for the option_section decorator that inserts section break
269 headings between options in the --help output of a command."""
271 @staticmethod
272 @click.command()
273 @click.option("--foo")
274 @option_section("Section break between metasyntactic variables.")
275 @click.option("--bar")
276 def cli(foo, bar):
277 pass
279 def setUp(self):
280 self.runner = click.testing.CliRunner()
282 def test_section_help(self):
283 """Verify that the section break is printed in the help output in the
284 expected location and with expected formatting."""
285 result = self.runner.invoke(self.cli, ["--help"])
286 # \x20 is a space, added explicity below to prevent the
287 # normally-helpful editor setting "remove trailing whitespace" from
288 # stripping it out in this case. (The blank line with 2 spaces is an
289 # artifact of how click and our code generate help text.)
290 expected = """Options:
291 --foo TEXT
292\x20\x20
293Section break between metasyntactic variables.
294 --bar TEXT"""
295 self.assertIn(expected, result.output)
297 def test_section_function(self):
298 """Verify that the section does not cause any arguments to be passed to
299 the command function.
301 The command function `cli` implementation inputs `foo` and `bar`, but
302 does accept an argument for the section. When the command is invoked
303 and the function called it should result in exit_code=0 (not 1 with a
304 missing argument error).
305 """
306 result = self.runner.invoke(self.cli, [])
307 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
310class MWPathTest(unittest.TestCase):
312 def getCmd(self, exists):
314 @click.command()
315 @click.option("--name", type=MWPath(exists=exists))
316 def cmd(name):
317 pass
318 return cmd
320 def setUp(self):
321 self.runner = click.testing.CliRunner()
323 def test_exist(self):
324 """Test the exist argument, verify that True means the file must exist,
325 False means the file must not exist, and None means that the file may
326 or may not exist."""
327 with self.runner.isolated_filesystem():
328 mustExistCmd = self.getCmd(exists=True)
329 mayExistCmd = self.getCmd(exists=None)
330 mustNotExistCmd = self.getCmd(exists=False)
331 args = ["--name", "foo.txt"]
333 result = self.runner.invoke(mustExistCmd, args)
334 self.assertNotEqual(result.exit_code, 0, clickResultMsg(result))
335 self.assertRegex(result.output, """['"]foo.txt['"] does not exist.""")
337 result = self.runner.invoke(mayExistCmd, args)
338 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
340 result = self.runner.invoke(mustNotExistCmd, args)
341 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
343 # isolated_filesystem runs in a temporary directory, when it is
344 # removed everything inside will be removed.
345 with open("foo.txt", "w") as _:
346 result = self.runner.invoke(mustExistCmd, args)
347 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
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.assertNotEqual(result.exit_code, 0, clickResultMsg(result))
354 self.assertIn('"foo.txt" should not exist.', result.output)
357if __name__ == "__main__": 357 ↛ 358line 357 didn't jump to line 358, because the condition on line 357 was never true
358 unittest.main()