Coverage for python/lsst/daf/butler/tests/cliCmdTestBase.py: 59%

57 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-05 10:00 +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 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 

28 

29import abc 

30import copy 

31import os 

32from collections.abc import Callable 

33from typing import TYPE_CHECKING, Any 

34from unittest.mock import DEFAULT, call, patch 

35 

36from ..cli import butler 

37from ..cli.utils import LogCliRunner, clickResultMsg 

38 

39if TYPE_CHECKING: 

40 import unittest 

41 

42 import click 

43 

44 

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 """ 

49 

50 if TYPE_CHECKING: 

51 assertNotEqual: Callable 

52 assertRegex: Callable 

53 assertFalse: Callable 

54 assertEqual: Callable 

55 

56 @staticmethod 

57 @abc.abstractmethod 

58 def defaultExpected() -> dict[str, Any]: 

59 pass 

60 

61 @staticmethod 

62 @abc.abstractmethod 

63 def command() -> click.Command: 

64 """Get the click.Command being tested.""" 

65 pass 

66 

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 

73 

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 

81 

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 

89 

90 def setUp(self) -> None: 

91 self.runner = LogCliRunner() 

92 

93 @classmethod 

94 def makeExpected(cls, **kwargs: Any) -> dict[str, Any]: 

95 expected = copy.copy(cls.defaultExpected()) 

96 expected.update(kwargs) 

97 return expected 

98 

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. 

102 

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. 

108 

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) 

116 

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. 

123 

124 Returns the result object for inspection, e.g. sometimes it's useful to 

125 be able to inspect or print `result.output`. 

126 

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. 

140 

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 

160 

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. 

165 

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) 

178 

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 )