Coverage for tests/test_cliUtils.py: 33%

Shortcuts 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

174 statements  

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 MagicMock 

28 

29 

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

31 clickResultMsg, 

32 LogCliRunner, 

33 MWArgumentDecorator, 

34 MWOption, 

35 MWOptionDecorator, 

36 MWPath, 

37 option_section, 

38 unwrap 

39) 

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

41 

42 

43class ArgumentHelpGeneratorTestCase(unittest.TestCase): 

44 

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 self.runTest(cli) 

69 

70 def runTest(self, cli): 

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

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

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

74 addArgumentHelp for more details.""" 

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

76 

77 The cli help message. 

78 

79 repo help text 

80 

81 directory help text 

82 

83Options: 

84 --help Show this message and exit. 

85""" 

86 runner = LogCliRunner() 

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

88 self.assertIn(expected, result.output) 

89 

90 

91class UnwrapStringTestCase(unittest.TestCase): 

92 

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 

137 def setUp(self): 

138 self.runner = LogCliRunner() 

139 

140 def test_addElipsisToMultiple(self): 

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

142 `multiple=True` 

143 

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

145 have `multiple=True`.""" 

146 

147 @click.command() 

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

149 def cmd(things): 

150 pass 

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 result = self.runner.invoke(cmd, ["--help"]) 

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

172 expectedOutput = f"""Options: 

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

174 self.assertIn(expectedOutput, result.output) 

175 

176 

177class MWArgumentDecoratorTest(unittest.TestCase): 

178 """Tests for the MWArgumentDecorator class.""" 

179 

180 things_argument = MWArgumentDecorator("things") 

181 otherHelpText = "Help text for OTHER." 

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

183 

184 def setUp(self): 

185 self.runner = LogCliRunner() 

186 

187 def test_help(self): 

188 """Verify expected help text output. 

189 

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

191 arguments are declared. 

192 

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

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

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

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

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

198 # number of arguments. 

199 

200 helpText = "Things help text." 

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

202 for required in (True, False): 

203 

204 @click.command() 

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

206 @self.other_argument() 

207 def cmd(things, other): 

208 """Cmd help text.""" 

209 pass 

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 self.runner = click.testing.CliRunner() 

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

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

233 mock.assert_called_with("foo") 

234 

235 

236class MWOptionDecoratorTest(unittest.TestCase): 

237 """Tests for the MWOptionDecorator class.""" 

238 

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

240 

241 def testGetName(self): 

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

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

244 

245 def testGetOpts(self): 

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

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

248 

249 def testUse(self): 

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

251 mock = MagicMock() 

252 

253 @click.command() 

254 @self.test_option() 

255 def cli(test): 

256 mock(test) 

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

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

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

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

261 

262 def testOverride(self): 

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

264 of the default values.""" 

265 mock = MagicMock() 

266 

267 @click.command() 

268 @self.test_option(multiple=False) 

269 def cli(test): 

270 mock(test) 

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

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

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

274 mock.assert_called_with("foo") 

275 

276 

277class SectionOptionTest(unittest.TestCase): 

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

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

280 

281 @staticmethod 

282 @click.command() 

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

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

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

286 def cli(foo, bar): 

287 pass 

288 

289 def setUp(self): 

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

291 

292 def test_section_help(self): 

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

294 expected location and with expected formatting.""" 

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

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

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

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

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

300 expected = """Options: 

301 --foo TEXT 

302\x20\x20 

303Section break between metasyntactic variables. 

304 --bar TEXT""" 

305 self.assertIn(expected, result.output) 

306 

307 def test_section_function(self): 

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

309 the command function. 

310 

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

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

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

314 missing argument error). 

315 """ 

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

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

318 

319 

320class MWPathTest(unittest.TestCase): 

321 

322 def getCmd(self, exists): 

323 

324 @click.command() 

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

326 def cmd(name): 

327 pass 

328 return cmd 

329 

330 def setUp(self): 

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

332 

333 def test_exist(self): 

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

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

336 or may not exist.""" 

337 with self.runner.isolated_filesystem(): 

338 mustExistCmd = self.getCmd(exists=True) 

339 mayExistCmd = self.getCmd(exists=None) 

340 mustNotExistCmd = self.getCmd(exists=False) 

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

342 

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

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

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

346 

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

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

349 

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

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

352 

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

354 # removed everything inside will be removed. 

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

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

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

358 

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

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

361 

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

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

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

365 

366 

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

368 unittest.main()