Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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 click 

26import unittest 

27from unittest.mock import call, MagicMock 

28 

29 

30from lsst.daf.butler.cli import butler 

31from lsst.daf.butler.cli.utils import (clickResultMsg, ForwardOptions, LogCliRunner, Mocker, mockEnvVar, 

32 MWArgumentDecorator, MWCommand, MWCtxObj, MWOption, MWOptionDecorator, 

33 MWPath, option_section, unwrap) 

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

35 

36 

37class MockerTestCase(unittest.TestCase): 

38 

39 def test_callMock(self): 

40 """Test that a mocked subcommand calls the Mocker and can be verified. 

41 """ 

42 runner = LogCliRunner(env=mockEnvVar) 

43 result = runner.invoke(butler.cli, ["create", "repo"]) 

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

45 Mocker.mock.assert_called_with(repo="repo", seed_config=None, standalone=False, override=False, 

46 outfile=None) 

47 

48 

49class ArgumentHelpGeneratorTestCase(unittest.TestCase): 

50 

51 @staticmethod 

52 @click.command() 

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

54 # do not break this test unnecessarily. 

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

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

57 def cli(): 

58 """The cli help message.""" 

59 pass 

60 

61 def test_help(self): 

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

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

64 command fucntion help, and that it's added in the correct order. See 

65 addArgumentHelp for more details.""" 

66 runner = LogCliRunner() 

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

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

69 

70 The cli help message. 

71 

72 repo help text 

73 

74 directory help text 

75 

76Options: 

77 --help Show this message and exit. 

78""" 

79 self.assertIn(expected, result.output) 

80 

81 

82class UnwrapStringTestCase(unittest.TestCase): 

83 

84 def test_leadingNewline(self): 

85 testStr = """ 

86 foo bar 

87 baz """ 

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

89 

90 def test_leadingContent(self): 

91 testStr = """foo bar 

92 baz """ 

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

94 

95 def test_trailingNewline(self): 

96 testStr = """ 

97 foo bar 

98 baz 

99 """ 

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

101 

102 def test_oneLine(self): 

103 testStr = """foo bar baz""" 

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

105 

106 def test_oneLineWithLeading(self): 

107 testStr = """ 

108 foo bar baz""" 

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

110 

111 def test_oneLineWithTrailing(self): 

112 testStr = """foo bar baz 

113 """ 

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

115 

116 def test_lineBreaks(self): 

117 testStr = """foo bar 

118 baz 

119 

120 boz 

121 

122 qux""" 

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

124 

125 

126class MWOptionTest(unittest.TestCase): 

127 

128 def setUp(self): 

129 self.runner = LogCliRunner() 

130 

131 def test_addElipsisToMultiple(self): 

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

133 `multiple=True` 

134 

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

136 have `multiple=True`.""" 

137 

138 @click.command() 

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

140 def cmd(things): 

141 pass 

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

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

144 expectedOutut = """Options: 

145 --things TEXT ...""" 

146 self.assertIn(expectedOutut, result.output) 

147 

148 def test_addElipsisToNargs(self): 

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

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

151 

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

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

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

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

156 

157 @click.command() 

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

159 def cmd(things): 

160 pass 

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

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

163 expectedOutut = f"""Options: 

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

165 self.assertIn(expectedOutut, result.output) 

166 

167 

168class MWArgumentDecoratorTest(unittest.TestCase): 

169 """Tests for the MWArgumentDecorator class.""" 

170 

171 things_argument = MWArgumentDecorator("things") 

172 otherHelpText = "Help text for OTHER." 

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

174 

175 def setUp(self): 

176 self.runner = LogCliRunner() 

177 

178 def test_help(self): 

179 """Verify expected help text output. 

180 

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

182 arguments are declared. 

183 

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

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

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

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

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

189 # number of arguments. 

190 

191 helpText = "Things help text." 

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

193 for required in (True, False): 

194 

195 @click.command() 

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

197 @self.other_argument() 

198 def cmd(things, other): 

199 """Cmd help text.""" 

200 pass 

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

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

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

204 

205 Cmd help text. 

206 

207 {helpText} 

208 

209 {self.otherHelpText} 

210""") 

211 self.assertIn(expectedOutut, result.output) 

212 

213 def testUse(self): 

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

215 mock = MagicMock() 

216 

217 @click.command() 

218 @self.things_argument() 

219 def cli(things): 

220 mock(things) 

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

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

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

224 mock.assert_called_with("foo") 

225 

226 

227class MWOptionDecoratorTest(unittest.TestCase): 

228 """Tests for the MWOptionDecorator class.""" 

229 

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

231 

232 def testGetName(self): 

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

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

235 

236 def testGetOpts(self): 

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

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

239 

240 def testUse(self): 

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

242 mock = MagicMock() 

243 

244 @click.command() 

245 @self.test_option() 

246 def cli(test): 

247 mock(test) 

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

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

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

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

252 

253 def testOverride(self): 

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

255 of the default values.""" 

256 mock = MagicMock() 

257 

258 @click.command() 

259 @self.test_option(multiple=False) 

260 def cli(test): 

261 mock(test) 

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

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

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

265 mock.assert_called_with("foo") 

266 

267 

268class SectionOptionTest(unittest.TestCase): 

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

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

271 

272 @staticmethod 

273 @click.command() 

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

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

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

277 def cli(foo, bar): 

278 pass 

279 

280 def setUp(self): 

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

282 

283 def test_section_help(self): 

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

285 expected location and with expected formatting.""" 

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

287 # \x20 is a space, added explicity below to prevent the 

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

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

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

291 expected = """Options: 

292 --foo TEXT 

293\x20\x20 

294Section break between metasyntactic variables. 

295 --bar TEXT""" 

296 self.assertIn(expected, result.output) 

297 

298 def test_section_function(self): 

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

300 the command function. 

301 

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

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

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

305 missing argument error). 

306 """ 

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

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

309 

310 

311class MWPathTest(unittest.TestCase): 

312 

313 def getCmd(self, exists): 

314 

315 @click.command() 

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

317 def cmd(name): 

318 pass 

319 return cmd 

320 

321 def setUp(self): 

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

323 

324 def test_exist(self): 

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

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

327 or may not exist.""" 

328 with self.runner.isolated_filesystem(): 

329 mustExistCmd = self.getCmd(exists=True) 

330 mayExistCmd = self.getCmd(exists=None) 

331 mustNotExistCmd = self.getCmd(exists=False) 

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

333 

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

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

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

337 

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

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

340 

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

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

343 

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

345 # removed everything inside will be removed. 

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

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

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

349 

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

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

352 

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

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

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

356 

357 

358class ForwardObjectsTest(unittest.TestCase): 

359 

360 mock = MagicMock() 

361 

362 test_option = MWOptionDecorator("-t", "--test", "--atest") 

363 

364 @click.group(chain=True) 

365 def cli(): 

366 pass 

367 

368 @staticmethod 

369 @cli.command(cls=MWCommand) 

370 @click.pass_context 

371 @test_option(forward=True) 

372 def forwards(ctx, **kwargs): 

373 """A subcommand that forwards its test_option value to future 

374 subcommands.""" 

375 def processor(objs): 

376 newKwargs = objs.update(ctx.command.params, MWCtxObj.getFrom(ctx).args, **kwargs) 

377 ForwardObjectsTest.mock("forwards", **newKwargs) 

378 return objs 

379 return processor 

380 

381 @staticmethod 

382 @cli.command(cls=MWCommand) 

383 @click.pass_context 

384 @test_option() # default value of "foward" arg is False 

385 def no_forward(ctx, **kwargs): 

386 """A subcommand that accepts test_option but does not forward the value 

387 to future subcommands.""" 

388 def processor(objs): 

389 newKwargs = objs.update(ctx.command.params, MWCtxObj.getFrom(ctx).args, **kwargs) 

390 ForwardObjectsTest.mock("no_forward", **newKwargs) 

391 return objs 

392 return processor 

393 

394 @staticmethod 

395 @cli.command(cls=MWCommand) 

396 @click.pass_context 

397 def no_test_option(ctx, **kwargs): 

398 """A subcommand that does not accept test_option.""" 

399 def processor(objs): 

400 newKwargs = objs.update(ctx.command.params, MWCtxObj.getFrom(ctx).args, **kwargs) 

401 ForwardObjectsTest.mock("no_test_option", **newKwargs) 

402 return objs 

403 return processor 

404 

405 @staticmethod 

406 @cli.resultcallback() 

407 def processCli(processors): 

408 """Executes the subcommand 'processor' functions for all the 

409 subcommands in the chained command group.""" 

410 objs = ForwardOptions() 

411 for processor in processors: 

412 objs = processor(objs) 

413 

414 def setUp(self): 

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

416 self.mock.reset_mock() 

417 

418 def testForward(self): 

419 """Test that an option can be forward from one option to another.""" 

420 result = self.runner.invoke(self.cli, ["forwards", "-t", "foo", "forwards"]) 

421 print(result.output) 

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

423 self.mock.assert_has_calls((call("forwards", test="foo"), 

424 call("forwards", test="foo"))) 

425 

426 def testNoForward(self): 

427 """Test that when a subcommand that forwards an option value is called 

428 before an option that does not use that option, that the stored option 

429 does not get passed to the option that does not use it.""" 

430 result = self.runner.invoke(self.cli, ["forwards", "-t", "foo", "no-forward"]) 

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

432 self.mock.assert_has_calls((call("forwards", test="foo"), 

433 call("no_forward", test="foo"))) 

434 

435 def testForwardThrough(self): 

436 """Test that forwarded option values persist when a subcommand that 

437 does not use that value is called between subcommands that do use the 

438 value.""" 

439 result = self.runner.invoke(self.cli, ["forwards", "-t", "foo", 

440 "no-test-option", 

441 "forwards"]) 

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

443 self.mock.assert_has_calls((call("forwards", test="foo"), 

444 call("no_test_option"), 

445 call("forwards", test="foo"))) 

446 

447 

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

449 unittest.main()