Coverage for tests/test_cliUtils.py: 25%

199 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-10-29 02:20 -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 MWCommand, 

34 MWCtxObj, 

35 MWOption, 

36 MWOptionDecorator, 

37 MWPath, 

38 clickResultMsg, 

39 option_section, 

40 unwrap, 

41) 

42 

43 

44class ArgumentHelpGeneratorTestCase(unittest.TestCase): 

45 def testHelp(self): 

46 @click.command() 

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

48 # text do not break this test unnecessarily. 

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

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

51 def cli(): 

52 """The cli help message.""" 

53 pass 

54 

55 self.runTest(cli) 

56 

57 def testHelpWrapped(self): 

58 @click.command() 

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

60 # text do not break this test unnecessarily. 

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

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

63 def cli(): 

64 """The cli 

65 help 

66 message.""" 

67 pass 

68 

69 self.runTest(cli) 

70 

71 def runTest(self, cli): 

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

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

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

75 addArgumentHelp for more details.""" 

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

77 

78 The cli help message. 

79 

80 repo help text 

81 

82 directory help text 

83 

84Options: 

85 --help Show this message and exit. 

86""" 

87 runner = LogCliRunner() 

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

89 self.assertIn(expected, result.output) 

90 

91 

92class UnwrapStringTestCase(unittest.TestCase): 

93 def test_leadingNewline(self): 

94 testStr = """ 

95 foo bar 

96 baz """ 

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

98 

99 def test_leadingContent(self): 

100 testStr = """foo bar 

101 baz """ 

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

103 

104 def test_trailingNewline(self): 

105 testStr = """ 

106 foo bar 

107 baz 

108 """ 

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

110 

111 def test_oneLine(self): 

112 testStr = """foo bar baz""" 

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

114 

115 def test_oneLineWithLeading(self): 

116 testStr = """ 

117 foo bar baz""" 

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

119 

120 def test_oneLineWithTrailing(self): 

121 testStr = """foo bar baz 

122 """ 

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

124 

125 def test_lineBreaks(self): 

126 testStr = """foo bar 

127 baz 

128 

129 boz 

130 

131 qux""" 

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

133 

134 

135class MWOptionTest(unittest.TestCase): 

136 def setUp(self): 

137 self.runner = LogCliRunner() 

138 

139 def test_addElipsisToMultiple(self): 

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

141 `multiple=True` 

142 

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

144 have `multiple=True`.""" 

145 

146 @click.command() 

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

148 def cmd(things): 

149 pass 

150 

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

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

153 expectedOutput = """Options: 

154 --things TEXT ...""" 

155 self.assertIn(expectedOutput, result.output) 

156 

157 def test_addElipsisToNargs(self): 

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

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

160 

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

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

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

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

165 

166 @click.command() 

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

168 def cmd(things): 

169 pass 

170 

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

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

173 expectedOutput = f"""Options: 

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

175 self.assertIn(expectedOutput, result.output) 

176 

177 

178class MWArgumentDecoratorTest(unittest.TestCase): 

179 """Tests for the MWArgumentDecorator class.""" 

180 

181 things_argument = MWArgumentDecorator("things") 

182 otherHelpText = "Help text for OTHER." 

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

184 

185 def setUp(self): 

186 self.runner = LogCliRunner() 

187 

188 def test_help(self): 

189 """Verify expected help text output. 

190 

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

192 arguments are declared. 

193 

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

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

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

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

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

199 # number of arguments. 

200 

201 helpText = "Things help text." 

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

203 for required in (True, False): 

204 

205 @click.command() 

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

207 @self.other_argument() 

208 def cmd(things, other): 

209 """Cmd help text.""" 

210 pass 

211 

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

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

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

215 

216 Cmd help text. 

217 

218 {helpText} 

219 

220 {self.otherHelpText} 

221""" 

222 self.assertIn(expectedOutput, result.output) 

223 

224 def testUse(self): 

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

226 mock = MagicMock() 

227 

228 @click.command() 

229 @self.things_argument() 

230 def cli(things): 

231 mock(things) 

232 

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

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

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

236 mock.assert_called_with("foo") 

237 

238 

239class MWOptionDecoratorTest(unittest.TestCase): 

240 """Tests for the MWOptionDecorator class.""" 

241 

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

243 

244 def testGetName(self): 

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

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

247 

248 def testGetOpts(self): 

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

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

251 

252 def testUse(self): 

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

254 mock = MagicMock() 

255 

256 @click.command() 

257 @self.test_option() 

258 def cli(test): 

259 mock(test) 

260 

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

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

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

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

265 

266 def testOverride(self): 

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

268 of the default values.""" 

269 mock = MagicMock() 

270 

271 @click.command() 

272 @self.test_option(multiple=False) 

273 def cli(test): 

274 mock(test) 

275 

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

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

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

279 mock.assert_called_with("foo") 

280 

281 

282class SectionOptionTest(unittest.TestCase): 

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

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

285 

286 @staticmethod 

287 @click.command() 

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

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

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

291 def cli(foo, bar): 

292 pass 

293 

294 def setUp(self): 

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

296 

297 def test_section_help(self): 

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

299 expected location and with expected formatting.""" 

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

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

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

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

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

305 expected = """Options: 

306 --foo TEXT 

307\x20\x20 

308Section break between metasyntactic variables. 

309 --bar TEXT""" 

310 self.assertIn(expected, result.output) 

311 

312 def test_section_function(self): 

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

314 the command function. 

315 

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

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

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

319 missing argument error). 

320 """ 

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

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

323 

324 

325class MWPathTest(unittest.TestCase): 

326 def getCmd(self, exists): 

327 @click.command() 

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

329 def cmd(name): 

330 pass 

331 

332 return cmd 

333 

334 def setUp(self): 

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

336 

337 def test_exist(self): 

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

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

340 or may not exist.""" 

341 with self.runner.isolated_filesystem(): 

342 mustExistCmd = self.getCmd(exists=True) 

343 mayExistCmd = self.getCmd(exists=None) 

344 mustNotExistCmd = self.getCmd(exists=False) 

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

346 

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

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

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

350 

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

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

353 

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

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

356 

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

358 # removed everything inside will be removed. 

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

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

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

362 

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

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

365 

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

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

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

369 

370 

371class MWCommandTest(unittest.TestCase): 

372 def setUp(self): 

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

374 self.ctx = None 

375 

376 def testCaptureOptions(self): 

377 """Test that command options are captured in order.""" 

378 

379 @click.command(cls=MWCommand) 

380 @click.argument("ARG_A", required=False) 

381 @click.argument("ARG_B", required=False) 

382 @click.option("-o", "--an-option") 

383 @click.option("-s", "--second-option") 

384 @click.option("-f", is_flag=True) 

385 @click.option("-d/-n", "--do/--do-not", "do_or_do_not") 

386 @click.option("--multi", multiple=True) 

387 @click.pass_context 

388 def cmd(ctx, arg_a, arg_b, an_option, second_option, f, do_or_do_not, multi): 

389 self.assertIsNotNone(ctx) 

390 self.ctx = ctx 

391 

392 # When `expected` is `None`, the expected args are exactly the same as 

393 # as the result of `args.split()`. If `expected` is not `None` then 

394 # the expected args are the same as `expected.split()`. 

395 for args, expected in ( 

396 ("--an-option foo --second-option bar", None), 

397 ("--second-option bar --an-option foo", None), 

398 ("--an-option foo", None), 

399 ("--second-option bar", None), 

400 ("--an-option foo -f --second-option bar", None), 

401 ("-o foo -s bar", "--an-option foo --second-option bar"), 

402 ("--an-option foo -f -s bar", "--an-option foo -f --second-option bar"), 

403 ("--an-option=foo", "--an-option foo"), 

404 # NB when using a short flag everything that follows, including an 

405 # equals sign, is part of the value! 

406 ("-o=foo", "--an-option =foo"), 

407 ("--do", None), 

408 ("--do-not", None), 

409 ("-d", "--do"), 

410 ("-n", "--do-not"), 

411 # Arguments always come last, but the order of Arguments is still 

412 # preserved: 

413 ("myarg --an-option foo", "--an-option foo myarg"), 

414 ("argA --an-option foo", "--an-option foo argA"), 

415 ("argA argB --an-option foo", "--an-option foo argA argB"), 

416 ("argA --an-option foo argB", "--an-option foo argA argB"), 

417 ("--an-option foo argA argB", "--an-option foo argA argB"), 

418 ("--multi one --multi two", None), 

419 ): 

420 split_args = args.split() 

421 expected_args = split_args if expected is None else expected.split() 

422 result = self.runner.invoke(cmd, split_args) 

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

424 self.assertIsNotNone(self.ctx) 

425 ctx_obj = MWCtxObj.getFrom(self.ctx) 

426 self.assertEqual(ctx_obj.args, expected_args) 

427 

428 

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

430 unittest.main()