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