Coverage for python/lsst/daf/butler/tests/cliCmdTestBase.py: 59%
57 statements
« prev ^ index » next coverage.py v7.5.0, created at 2024-04-30 02:53 -0700
« prev ^ index » next coverage.py v7.5.0, created at 2024-04-30 02:53 -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 software is dual licensed under the GNU General Public License and also
10# under a 3-clause BSD license. Recipients may choose which of these licenses
11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt,
12# respectively. If you choose the GPL option then the following text applies
13# (but note that there is still no warranty even if you opt for BSD instead):
14#
15# This program is free software: you can redistribute it and/or modify
16# it under the terms of the GNU General Public License as published by
17# the Free Software Foundation, either version 3 of the License, or
18# (at your option) any later version.
19#
20# This program is distributed in the hope that it will be useful,
21# but WITHOUT ANY WARRANTY; without even the implied warranty of
22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23# GNU General Public License for more details.
24#
25# You should have received a copy of the GNU General Public License
26# along with this program. If not, see <http://www.gnu.org/licenses/>.
27from __future__ import annotations
29import abc
30import copy
31import os
32from collections.abc import Callable
33from typing import TYPE_CHECKING, Any
34from unittest.mock import DEFAULT, call, patch
36from ..cli import butler
37from ..cli.utils import LogCliRunner, clickResultMsg
39if TYPE_CHECKING:
40 import unittest
42 import click
45class CliCmdTestBase(abc.ABC):
46 """A test case base that is used to verify click command functions import
47 and call their respective script functions correctly.
48 """
50 if TYPE_CHECKING:
51 assertNotEqual: Callable
52 assertRegex: Callable
53 assertFalse: Callable
54 assertEqual: Callable
56 @staticmethod
57 @abc.abstractmethod
58 def defaultExpected() -> dict[str, Any]:
59 pass
61 @staticmethod
62 @abc.abstractmethod
63 def command() -> click.Command:
64 """Get the click.Command being tested."""
65 pass
67 @property
68 def cli(self) -> click.core.Command:
69 """Get the command line interface function under test, can be
70 overridden to test CLIs other than butler.
71 """
72 return butler.cli
74 @property
75 def mock(self) -> unittest.mock.Mock:
76 """Get the mock object to use in place of `mockFuncName`. If not
77 provided will use the default provided by `unittest.mock.patch`, this
78 is usually a `unittest.mock.MagicMock`.
79 """
80 return DEFAULT
82 @property
83 @abc.abstractmethod
84 def mockFuncName(self) -> str:
85 """The qualified name of the function to mock, will be passed to
86 unittest.mock.patch, see python docs for details.
87 """
88 pass
90 def setUp(self) -> None:
91 self.runner = LogCliRunner()
93 @classmethod
94 def makeExpected(cls, **kwargs: Any) -> dict[str, Any]:
95 expected = copy.copy(cls.defaultExpected())
96 expected.update(kwargs)
97 return expected
99 def run_command(self, inputs: list[str]) -> click.testing.Result:
100 """Use the LogCliRunner with the mock environment variable set to
101 execute a butler subcommand and parameters specified in inputs.
103 Parameters
104 ----------
105 inputs : [`str`]
106 A list of strings that begins with the subcommand name and is
107 followed by arguments, option keys and option values.
109 Returns
110 -------
111 result : `click.testing.Result`
112 The Result object contains the results from calling
113 self.runner.invoke.
114 """
115 return self.runner.invoke(self.cli, inputs)
117 def run_test(
118 self, inputs: list[str], expectedKwargs: dict[str, str], withTempFile: str | None = None
119 ) -> click.testing.Result:
120 """Run the subcommand specified in inputs and verify a successful
121 outcome where exit code = 0 and the mock object has been called with
122 the expected arguments.
124 Returns the result object for inspection, e.g. sometimes it's useful to
125 be able to inspect or print `result.output`.
127 Parameters
128 ----------
129 inputs : [`str`]
130 A list of strings that begins with the subcommand name and is
131 followed by arguments, option keys and option values.
132 expectedKwargs : `dict` [`str`, `str`]
133 The arguments that the subcommand function is expected to have been
134 called with. Keys are the argument name and values are the argument
135 value.
136 withTempFile : `str`, optional
137 If not None, will run in a temporary directory and create a file
138 with the given name, can be used with commands with parameters that
139 require a file to exist.
141 Returns
142 -------
143 result : `click.testing.Result`
144 The result object produced by invocation of the command under test.
145 """
146 with self.runner.isolated_filesystem():
147 if withTempFile is not None:
148 directory, filename = os.path.split(withTempFile)
149 if directory:
150 os.makedirs(os.path.dirname(withTempFile), exist_ok=True)
151 with open(withTempFile, "w") as _:
152 # just need to make the file, don't need to keep it open.
153 pass
154 with patch(self.mockFuncName, self.mock) as mock:
155 result = self.run_command(inputs)
156 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
157 calls = (call(**expectedKwargs),)
158 mock.assert_has_calls(list(calls))
159 return result
161 def run_missing(self, inputs: list[str], expectedMsg: str) -> None:
162 """Run the subcommand specified in inputs and verify a failed outcome
163 where exit code != 0 and an expected message has been written to
164 stdout.
166 Parameters
167 ----------
168 inputs : [`str`]
169 A list of strings that begins with the subcommand name and is
170 followed by arguments, option keys and option values.
171 expectedMsg : `str`
172 An error message that should be present in stdout after running the
173 subcommand. Can be a regular expression string.
174 """
175 result = self.run_command(inputs)
176 self.assertNotEqual(result.exit_code, 0, clickResultMsg(result))
177 self.assertRegex(result.stdout, expectedMsg)
179 def test_help(self) -> None:
180 self.assertFalse(
181 self.command().get_short_help_str().endswith("..."),
182 msg="The command help message is being truncated to "
183 f'"{self.command().get_short_help_str()}". It should be shortened, or define '
184 '@command(short_help="something short and helpful")',
185 )