Coverage for tests/test_cliUtils.py: 25%

201 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-03-25 02:06 -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 things = "THINGS" if required else "[THINGS]" 

215 additional = "... " if numberOfArgs != 1 else "" 

216 expectedOutput = f"""Usage: cmd [OPTIONS] {things} {additional}OTHER 

217 

218 Cmd help text. 

219 

220 {helpText} 

221 

222 {self.otherHelpText} 

223""" 

224 self.assertIn(expectedOutput, result.output) 

225 

226 def testUse(self): 

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

228 mock = MagicMock() 

229 

230 @click.command() 

231 @self.things_argument() 

232 def cli(things): 

233 mock(things) 

234 

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

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

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

238 mock.assert_called_with("foo") 

239 

240 

241class MWOptionDecoratorTest(unittest.TestCase): 

242 """Tests for the MWOptionDecorator class.""" 

243 

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

245 

246 def testGetName(self): 

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

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

249 

250 def testGetOpts(self): 

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

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

253 

254 def testUse(self): 

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

256 mock = MagicMock() 

257 

258 @click.command() 

259 @self.test_option() 

260 def cli(test): 

261 mock(test) 

262 

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

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

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

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

267 

268 def testOverride(self): 

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

270 of the default values.""" 

271 mock = MagicMock() 

272 

273 @click.command() 

274 @self.test_option(multiple=False) 

275 def cli(test): 

276 mock(test) 

277 

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

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

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

281 mock.assert_called_with("foo") 

282 

283 

284class SectionOptionTest(unittest.TestCase): 

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

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

287 

288 @staticmethod 

289 @click.command() 

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

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

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

293 def cli(foo, bar): 

294 pass 

295 

296 def setUp(self): 

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

298 

299 def test_section_help(self): 

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

301 expected location and with expected formatting.""" 

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

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

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

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

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

307 expected = """Options: 

308 --foo TEXT 

309\x20\x20 

310Section break between metasyntactic variables. 

311 --bar TEXT""" 

312 self.assertIn(expected, result.output) 

313 

314 def test_section_function(self): 

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

316 the command function. 

317 

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

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

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

321 missing argument error). 

322 """ 

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

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

325 

326 

327class MWPathTest(unittest.TestCase): 

328 def getCmd(self, exists): 

329 @click.command() 

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

331 def cmd(name): 

332 pass 

333 

334 return cmd 

335 

336 def setUp(self): 

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

338 

339 def test_exist(self): 

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

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

342 or may not exist.""" 

343 with self.runner.isolated_filesystem(): 

344 mustExistCmd = self.getCmd(exists=True) 

345 mayExistCmd = self.getCmd(exists=None) 

346 mustNotExistCmd = self.getCmd(exists=False) 

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

348 

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

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

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

352 

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

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

355 

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

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

358 

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

360 # removed everything inside will be removed. 

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

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

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

364 

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

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

367 

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

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

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

371 

372 

373class MWCommandTest(unittest.TestCase): 

374 def setUp(self): 

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

376 self.ctx = None 

377 

378 def testCaptureOptions(self): 

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

380 

381 @click.command(cls=MWCommand) 

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

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

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

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

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

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

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

389 @click.pass_context 

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

391 self.assertIsNotNone(ctx) 

392 self.ctx = ctx 

393 

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

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

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

397 for args, expected in ( 

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

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

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

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

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

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

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

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

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

407 # equals sign, is part of the value! 

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

409 ("--do", None), 

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

411 ("-d", "--do"), 

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

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

414 # preserved: 

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

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

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

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

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

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

421 ): 

422 split_args = args.split() 

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

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

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

426 self.assertIsNotNone(self.ctx) 

427 ctx_obj = MWCtxObj.getFrom(self.ctx) 

428 self.assertEqual(ctx_obj.args, expected_args) 

429 

430 

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

432 unittest.main()