Coverage for tests/test_cliUtils.py: 33%

174 statements  

« prev     ^ index     » next       coverage.py v6.4, created at 2022-05-24 02:27 -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 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 

22"""Unit tests for the daf_butler shared CLI options. 

23""" 

24 

25import unittest 

26from unittest.mock import MagicMock 

27 

28import click 

29from lsst.daf.butler.cli.opt import directory_argument, repo_argument 

30from lsst.daf.butler.cli.utils import ( 

31 LogCliRunner, 

32 MWArgumentDecorator, 

33 MWOption, 

34 MWOptionDecorator, 

35 MWPath, 

36 clickResultMsg, 

37 option_section, 

38 unwrap, 

39) 

40 

41 

42class ArgumentHelpGeneratorTestCase(unittest.TestCase): 

43 def testHelp(self): 

44 @click.command() 

45 # Use custom help in the arguments so that any changes to default help 

46 # text do not break this test unnecessarily. 

47 @repo_argument(help="repo help text") 

48 @directory_argument(help="directory help text") 

49 def cli(): 

50 """The cli help message.""" 

51 pass 

52 

53 self.runTest(cli) 

54 

55 def testHelpWrapped(self): 

56 @click.command() 

57 # Use custom help in the arguments so that any changes to default help 

58 # text do not break this test unnecessarily. 

59 @repo_argument(help="repo help text") 

60 @directory_argument(help="directory help text") 

61 def cli(): 

62 """The cli 

63 help 

64 message.""" 

65 pass 

66 

67 self.runTest(cli) 

68 

69 def runTest(self, cli): 

70 """Tests `utils.addArgumentHelp` and its use in repo_argument and 

71 directory_argument; verifies that the argument help gets added to the 

72 command function help, and that it's added in the correct order. See 

73 addArgumentHelp for more details.""" 

74 expected = """Usage: cli [OPTIONS] REPO DIRECTORY 

75 

76 The cli help message. 

77 

78 repo help text 

79 

80 directory help text 

81 

82Options: 

83 --help Show this message and exit. 

84""" 

85 runner = LogCliRunner() 

86 result = runner.invoke(cli, ["--help"]) 

87 self.assertIn(expected, result.output) 

88 

89 

90class UnwrapStringTestCase(unittest.TestCase): 

91 def test_leadingNewline(self): 

92 testStr = """ 

93 foo bar 

94 baz """ 

95 self.assertEqual(unwrap(testStr), "foo bar baz") 

96 

97 def test_leadingContent(self): 

98 testStr = """foo bar 

99 baz """ 

100 self.assertEqual(unwrap(testStr), "foo bar baz") 

101 

102 def test_trailingNewline(self): 

103 testStr = """ 

104 foo bar 

105 baz 

106 """ 

107 self.assertEqual(unwrap(testStr), "foo bar baz") 

108 

109 def test_oneLine(self): 

110 testStr = """foo bar baz""" 

111 self.assertEqual(unwrap(testStr), "foo bar baz") 

112 

113 def test_oneLineWithLeading(self): 

114 testStr = """ 

115 foo bar baz""" 

116 self.assertEqual(unwrap(testStr), "foo bar baz") 

117 

118 def test_oneLineWithTrailing(self): 

119 testStr = """foo bar baz 

120 """ 

121 self.assertEqual(unwrap(testStr), "foo bar baz") 

122 

123 def test_lineBreaks(self): 

124 testStr = """foo bar 

125 baz 

126 

127 boz 

128 

129 qux""" 

130 self.assertEqual(unwrap(testStr), "foo bar baz\n\nboz\n\nqux") 

131 

132 

133class MWOptionTest(unittest.TestCase): 

134 def setUp(self): 

135 self.runner = LogCliRunner() 

136 

137 def test_addElipsisToMultiple(self): 

138 """Verify that MWOption adds elipsis to the option metavar when 

139 `multiple=True` 

140 

141 The default behavior of click is to not add elipsis to options that 

142 have `multiple=True`.""" 

143 

144 @click.command() 

145 @click.option("--things", cls=MWOption, multiple=True) 

146 def cmd(things): 

147 pass 

148 

149 result = self.runner.invoke(cmd, ["--help"]) 

150 self.assertEqual(result.exit_code, 0, clickResultMsg(result)) 

151 expectedOutput = """Options: 

152 --things TEXT ...""" 

153 self.assertIn(expectedOutput, result.output) 

154 

155 def test_addElipsisToNargs(self): 

156 """Verify that MWOption adds " ..." after the option metavar when 

157 `nargs` is set to more than 1 and less than 1. 

158 

159 The default behavior of click is to add elipsis when nargs does not 

160 equal 1, but it does not put a space before the elipsis and we prefer 

161 a space between the metavar and the elipsis.""" 

162 for numberOfArgs in (0, 1, 2): # nargs must be >= 0 for an option 

163 

164 @click.command() 

165 @click.option("--things", cls=MWOption, nargs=numberOfArgs) 

166 def cmd(things): 

167 pass 

168 

169 result = self.runner.invoke(cmd, ["--help"]) 

170 self.assertEqual(result.exit_code, 0, clickResultMsg(result)) 

171 expectedOutput = f"""Options: 

172 --things TEXT{' ...' if numberOfArgs != 1 else ''}""" 

173 self.assertIn(expectedOutput, result.output) 

174 

175 

176class MWArgumentDecoratorTest(unittest.TestCase): 

177 """Tests for the MWArgumentDecorator class.""" 

178 

179 things_argument = MWArgumentDecorator("things") 

180 otherHelpText = "Help text for OTHER." 

181 other_argument = MWArgumentDecorator("other", help=otherHelpText) 

182 

183 def setUp(self): 

184 self.runner = LogCliRunner() 

185 

186 def test_help(self): 

187 """Verify expected help text output. 

188 

189 Verify argument help gets inserted after the usage, in the order 

190 arguments are declared. 

191 

192 Verify that MWArgument adds " ..." after the option metavar when 

193 `nargs` != 1. The default behavior of click is to add elipsis when 

194 nargs does not equal 1, but it does not put a space before the elipsis 

195 and we prefer a space between the metavar and the elipsis.""" 

196 # nargs can be -1 for any number of args, or >= 1 for a specified 

197 # number of arguments. 

198 

199 helpText = "Things help text." 

200 for numberOfArgs in (-1, 1, 2): 

201 for required in (True, False): 

202 

203 @click.command() 

204 @self.things_argument(required=required, nargs=numberOfArgs, help=helpText) 

205 @self.other_argument() 

206 def cmd(things, other): 

207 """Cmd help text.""" 

208 pass 

209 

210 result = self.runner.invoke(cmd, ["--help"]) 

211 self.assertEqual(result.exit_code, 0, clickResultMsg(result)) 

212 expectedOutput = f"""Usage: cmd [OPTIONS] {'THINGS' if required else '[THINGS]'} {'... ' if numberOfArgs != 1 else ''}OTHER 

213 

214 Cmd help text. 

215 

216 {helpText} 

217 

218 {self.otherHelpText} 

219""" 

220 self.assertIn(expectedOutput, result.output) 

221 

222 def testUse(self): 

223 """Test using the MWArgumentDecorator with a command.""" 

224 mock = MagicMock() 

225 

226 @click.command() 

227 @self.things_argument() 

228 def cli(things): 

229 mock(things) 

230 

231 self.runner = click.testing.CliRunner() 

232 result = self.runner.invoke(cli, ("foo")) 

233 self.assertEqual(result.exit_code, 0, clickResultMsg(result)) 

234 mock.assert_called_with("foo") 

235 

236 

237class MWOptionDecoratorTest(unittest.TestCase): 

238 """Tests for the MWOptionDecorator class.""" 

239 

240 test_option = MWOptionDecorator("-t", "--test", multiple=True) 

241 

242 def testGetName(self): 

243 """Test getting the option name from the MWOptionDecorator.""" 

244 self.assertEqual(self.test_option.name(), "test") 

245 

246 def testGetOpts(self): 

247 """Test getting the option flags from the MWOptionDecorator.""" 

248 self.assertEqual(self.test_option.opts(), ["-t", "--test"]) 

249 

250 def testUse(self): 

251 """Test using the MWOptionDecorator with a command.""" 

252 mock = MagicMock() 

253 

254 @click.command() 

255 @self.test_option() 

256 def cli(test): 

257 mock(test) 

258 

259 self.runner = click.testing.CliRunner() 

260 result = self.runner.invoke(cli, ("-t", "foo")) 

261 self.assertEqual(result.exit_code, 0, clickResultMsg(result)) 

262 mock.assert_called_with(("foo",)) 

263 

264 def testOverride(self): 

265 """Test using the MWOptionDecorator with a command and overriding one 

266 of the default values.""" 

267 mock = MagicMock() 

268 

269 @click.command() 

270 @self.test_option(multiple=False) 

271 def cli(test): 

272 mock(test) 

273 

274 self.runner = click.testing.CliRunner() 

275 result = self.runner.invoke(cli, ("-t", "foo")) 

276 self.assertEqual(result.exit_code, 0, clickResultMsg(result)) 

277 mock.assert_called_with("foo") 

278 

279 

280class SectionOptionTest(unittest.TestCase): 

281 """Tests for the option_section decorator that inserts section break 

282 headings between options in the --help output of a command.""" 

283 

284 @staticmethod 

285 @click.command() 

286 @click.option("--foo") 

287 @option_section("Section break between metasyntactic variables.") 

288 @click.option("--bar") 

289 def cli(foo, bar): 

290 pass 

291 

292 def setUp(self): 

293 self.runner = click.testing.CliRunner() 

294 

295 def test_section_help(self): 

296 """Verify that the section break is printed in the help output in the 

297 expected location and with expected formatting.""" 

298 result = self.runner.invoke(self.cli, ["--help"]) 

299 # \x20 is a space, added explicitly below to prevent the 

300 # normally-helpful editor setting "remove trailing whitespace" from 

301 # stripping it out in this case. (The blank line with 2 spaces is an 

302 # artifact of how click and our code generate help text.) 

303 expected = """Options: 

304 --foo TEXT 

305\x20\x20 

306Section break between metasyntactic variables. 

307 --bar TEXT""" 

308 self.assertIn(expected, result.output) 

309 

310 def test_section_function(self): 

311 """Verify that the section does not cause any arguments to be passed to 

312 the command function. 

313 

314 The command function `cli` implementation inputs `foo` and `bar`, but 

315 does accept an argument for the section. When the command is invoked 

316 and the function called it should result in exit_code=0 (not 1 with a 

317 missing argument error). 

318 """ 

319 result = self.runner.invoke(self.cli, []) 

320 self.assertEqual(result.exit_code, 0, clickResultMsg(result)) 

321 

322 

323class MWPathTest(unittest.TestCase): 

324 def getCmd(self, exists): 

325 @click.command() 

326 @click.option("--name", type=MWPath(exists=exists)) 

327 def cmd(name): 

328 pass 

329 

330 return cmd 

331 

332 def setUp(self): 

333 self.runner = click.testing.CliRunner() 

334 

335 def test_exist(self): 

336 """Test the exist argument, verify that True means the file must exist, 

337 False means the file must not exist, and None means that the file may 

338 or may not exist.""" 

339 with self.runner.isolated_filesystem(): 

340 mustExistCmd = self.getCmd(exists=True) 

341 mayExistCmd = self.getCmd(exists=None) 

342 mustNotExistCmd = self.getCmd(exists=False) 

343 args = ["--name", "foo.txt"] 

344 

345 result = self.runner.invoke(mustExistCmd, args) 

346 self.assertNotEqual(result.exit_code, 0, clickResultMsg(result)) 

347 self.assertRegex(result.output, """['"]foo.txt['"] does not exist.""") 

348 

349 result = self.runner.invoke(mayExistCmd, args) 

350 self.assertEqual(result.exit_code, 0, clickResultMsg(result)) 

351 

352 result = self.runner.invoke(mustNotExistCmd, args) 

353 self.assertEqual(result.exit_code, 0, clickResultMsg(result)) 

354 

355 # isolated_filesystem runs in a temporary directory, when it is 

356 # removed everything inside will be removed. 

357 with open("foo.txt", "w") as _: 

358 result = self.runner.invoke(mustExistCmd, args) 

359 self.assertEqual(result.exit_code, 0, clickResultMsg(result)) 

360 

361 result = self.runner.invoke(mayExistCmd, args) 

362 self.assertEqual(result.exit_code, 0, clickResultMsg(result)) 

363 

364 result = self.runner.invoke(mustNotExistCmd, args) 

365 self.assertNotEqual(result.exit_code, 0, clickResultMsg(result)) 

366 self.assertIn('"foo.txt" should not exist.', result.output) 

367 

368 

369if __name__ == "__main__": 369 ↛ 370line 369 didn't jump to line 370, because the condition on line 369 was never true

370 unittest.main()