Coverage for python/lsst/daf/butler/tests/cliCmdTestBase.py: 39%
56 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 22:06 -0800
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 22:06 -0800
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/>.
22import abc
23import copy
24import os
25from unittest.mock import DEFAULT, call, patch
27from ..cli import butler
28from ..cli.utils import LogCliRunner, clickResultMsg
31class CliCmdTestBase(abc.ABC):
32 """A test case base that is used to verify click command functions import
33 and call their respective script functions correctly.
34 """
36 @staticmethod
37 @abc.abstractmethod
38 def defaultExpected():
39 pass
41 @staticmethod
42 @abc.abstractmethod
43 def command():
44 """Get the click.Command being tested."""
45 pass
47 @property
48 def cli(self):
49 """Get the command line interface function under test, can be
50 overridden to test CLIs other than butler."""
51 return butler.cli
53 @property
54 def mock(self):
55 """Get the mock object to use in place of `mockFuncName`. If not
56 provided will use the default provided by `unittest.patch`, this is
57 usually a `unittest.patch.MagicMock`."""
58 return DEFAULT
60 @property
61 @abc.abstractmethod
62 def mockFuncName(self):
63 """The qualified name of the function to mock, will be passed to
64 unittest.mock.patch, see python docs for details."""
65 pass
67 def setUp(self):
68 self.runner = LogCliRunner()
70 @classmethod
71 def makeExpected(cls, **kwargs):
72 expected = copy.copy(cls.defaultExpected())
73 expected.update(kwargs)
74 return expected
76 def run_command(self, inputs):
77 """Use the LogCliRunner with the mock environment variable set to
78 execute a butler subcommand and parameters specified in inputs.
80 Parameters
81 ----------
82 inputs : [`str`]
83 A list of strings that begins with the subcommand name and is
84 followed by arguments, option keys and option values.
86 Returns
87 -------
88 result : `click.testing.Result`
89 The Result object contains the results from calling
90 self.runner.invoke.
91 """
92 return self.runner.invoke(self.cli, inputs)
94 def run_test(self, inputs, expectedKwargs, withTempFile=None):
95 """Run the subcommand specified in inputs and verify a successful
96 outcome where exit code = 0 and the mock object has been called with
97 the expected arguments.
99 Returns the result object for inspection, e.g. sometimes it's useful to
100 be able to inspect or print `result.output`.
102 Parameters
103 ----------
104 inputs : [`str`]
105 A list of strings that begins with the subcommand name and is
106 followed by arguments, option keys and option values.
107 expectedKwargs : `dict` [`str`, `str`]
108 The arguments that the subcommand function is expected to have been
109 called with. Keys are the argument name and values are the argument
110 value.
111 withTempFile : `str`, optional
112 If not None, will run in a temporary directory and create a file
113 with the given name, can be used with commands with parameters that
114 require a file to exist.
116 Returns
117 -------
118 result : `click.testing.Result`
119 The result object produced by invocation of the command under test.
120 """
121 with self.runner.isolated_filesystem():
122 if withTempFile is not None:
123 directory, filename = os.path.split(withTempFile)
124 if directory:
125 os.makedirs(os.path.dirname(withTempFile), exist_ok=True)
126 with open(withTempFile, "w") as _:
127 # just need to make the file, don't need to keep it open.
128 pass
129 with patch(self.mockFuncName, self.mock) as mock:
130 result = self.run_command(inputs)
131 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
132 if isinstance(expectedKwargs, (list, tuple)):
133 calls = (call(**e) for e in expectedKwargs)
134 else:
135 calls = (call(**expectedKwargs),)
136 mock.assert_has_calls(list(calls))
137 return result
139 def run_missing(self, inputs, expectedMsg):
140 """Run the subcommand specified in inputs and verify a failed outcome
141 where exit code != 0 and an expected message has been written to
142 stdout.
144 Parameters
145 ----------
146 inputs : [`str`]
147 A list of strings that begins with the subcommand name and is
148 followed by arguments, option keys and option values.
149 expectedMsg : `str`
150 An error message that should be present in stdout after running the
151 subcommand. Can be a regular expression string.
152 """
153 result = self.run_command(inputs)
154 self.assertNotEqual(result.exit_code, 0, clickResultMsg(result))
155 self.assertRegex(result.stdout, expectedMsg)
157 def test_help(self):
158 self.assertFalse(
159 self.command().get_short_help_str().endswith("..."),
160 msg="The command help message is being truncated to "
161 f'"{self.command().get_short_help_str()}". It should be shortened, or define '
162 '@command(short_help="something short and helpful")',
163 )