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

56 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-17 02:01 -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/>. 

21 

22import abc 

23import copy 

24import os 

25from unittest.mock import DEFAULT, call, patch 

26 

27from ..cli import butler 

28from ..cli.utils import LogCliRunner, clickResultMsg 

29 

30 

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

35 

36 @staticmethod 

37 @abc.abstractmethod 

38 def defaultExpected(): 

39 pass 

40 

41 @staticmethod 

42 @abc.abstractmethod 

43 def command(): 

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

45 pass 

46 

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 

52 

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 

59 

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 

66 

67 def setUp(self): 

68 self.runner = LogCliRunner() 

69 

70 @classmethod 

71 def makeExpected(cls, **kwargs): 

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

73 expected.update(kwargs) 

74 return expected 

75 

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. 

79 

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. 

85 

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) 

93 

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. 

98 

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

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

101 

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. 

115 

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 

138 

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. 

143 

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) 

156 

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 )