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

57 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-28 10:10 +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 

22 

23import abc 

24import copy 

25import os 

26from collections.abc import Callable 

27from typing import TYPE_CHECKING, Any 

28from unittest.mock import DEFAULT, call, patch 

29 

30from ..cli import butler 

31from ..cli.utils import LogCliRunner, clickResultMsg 

32 

33if TYPE_CHECKING: 

34 import unittest 

35 

36 import click 

37 

38 

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

43 

44 if TYPE_CHECKING: 

45 assertNotEqual: Callable 

46 assertRegex: Callable 

47 assertFalse: Callable 

48 assertEqual: Callable 

49 

50 @staticmethod 

51 @abc.abstractmethod 

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

53 pass 

54 

55 @staticmethod 

56 @abc.abstractmethod 

57 def command() -> click.Command: 

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

59 pass 

60 

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 

67 

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 

75 

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 

83 

84 def setUp(self) -> None: 

85 self.runner = LogCliRunner() 

86 

87 @classmethod 

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

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

90 expected.update(kwargs) 

91 return expected 

92 

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. 

96 

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. 

102 

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) 

110 

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. 

117 

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

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

120 

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. 

134 

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 

154 

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. 

159 

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) 

172 

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 )