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 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 @staticmethod 

46 @click.command() 

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

48 # 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 def test_help(self): 

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

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

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

59 addArgumentHelp for more details.""" 

60 runner = LogCliRunner() 

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

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

63 

64 The cli help message. 

65 

66 repo help text 

67 

68 directory help text 

69 

70Options: 

71 --help Show this message and exit. 

72""" 

73 self.assertIn(expected, result.output) 

74 

75 

76class UnwrapStringTestCase(unittest.TestCase): 

77 

78 def test_leadingNewline(self): 

79 testStr = """ 

80 foo bar 

81 baz """ 

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

83 

84 def test_leadingContent(self): 

85 testStr = """foo bar 

86 baz """ 

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

88 

89 def test_trailingNewline(self): 

90 testStr = """ 

91 foo bar 

92 baz 

93 """ 

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

95 

96 def test_oneLine(self): 

97 testStr = """foo bar baz""" 

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

99 

100 def test_oneLineWithLeading(self): 

101 testStr = """ 

102 foo bar baz""" 

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

104 

105 def test_oneLineWithTrailing(self): 

106 testStr = """foo bar baz 

107 """ 

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

109 

110 def test_lineBreaks(self): 

111 testStr = """foo bar 

112 baz 

113 

114 boz 

115 

116 qux""" 

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

118 

119 

120class MWOptionTest(unittest.TestCase): 

121 

122 def setUp(self): 

123 self.runner = LogCliRunner() 

124 

125 def test_addElipsisToMultiple(self): 

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

127 `multiple=True` 

128 

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

130 have `multiple=True`.""" 

131 

132 @click.command() 

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

134 def cmd(things): 

135 pass 

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

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

138 expectedOutut = """Options: 

139 --things TEXT ...""" 

140 self.assertIn(expectedOutut, result.output) 

141 

142 def test_addElipsisToNargs(self): 

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

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

145 

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

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

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

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

150 

151 @click.command() 

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

153 def cmd(things): 

154 pass 

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

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

157 expectedOutut = f"""Options: 

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

159 self.assertIn(expectedOutut, result.output) 

160 

161 

162class MWArgumentDecoratorTest(unittest.TestCase): 

163 """Tests for the MWArgumentDecorator class.""" 

164 

165 things_argument = MWArgumentDecorator("things") 

166 otherHelpText = "Help text for OTHER." 

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

168 

169 def setUp(self): 

170 self.runner = LogCliRunner() 

171 

172 def test_help(self): 

173 """Verify expected help text output. 

174 

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

176 arguments are declared. 

177 

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

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

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

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

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

183 # number of arguments. 

184 

185 helpText = "Things help text." 

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

187 for required in (True, False): 

188 

189 @click.command() 

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

191 @self.other_argument() 

192 def cmd(things, other): 

193 """Cmd help text.""" 

194 pass 

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

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

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

198 

199 Cmd help text. 

200 

201 {helpText} 

202 

203 {self.otherHelpText} 

204""") 

205 self.assertIn(expectedOutut, result.output) 

206 

207 def testUse(self): 

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

209 mock = MagicMock() 

210 

211 @click.command() 

212 @self.things_argument() 

213 def cli(things): 

214 mock(things) 

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

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

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

218 mock.assert_called_with("foo") 

219 

220 

221class MWOptionDecoratorTest(unittest.TestCase): 

222 """Tests for the MWOptionDecorator class.""" 

223 

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

225 

226 def testGetName(self): 

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

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

229 

230 def testGetOpts(self): 

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

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

233 

234 def testUse(self): 

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

236 mock = MagicMock() 

237 

238 @click.command() 

239 @self.test_option() 

240 def cli(test): 

241 mock(test) 

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

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

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

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

246 

247 def testOverride(self): 

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

249 of the default values.""" 

250 mock = MagicMock() 

251 

252 @click.command() 

253 @self.test_option(multiple=False) 

254 def cli(test): 

255 mock(test) 

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

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

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

259 mock.assert_called_with("foo") 

260 

261 

262class SectionOptionTest(unittest.TestCase): 

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

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

265 

266 @staticmethod 

267 @click.command() 

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

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

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

271 def cli(foo, bar): 

272 pass 

273 

274 def setUp(self): 

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

276 

277 def test_section_help(self): 

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

279 expected location and with expected formatting.""" 

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

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

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

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

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

285 expected = """Options: 

286 --foo TEXT 

287\x20\x20 

288Section break between metasyntactic variables. 

289 --bar TEXT""" 

290 self.assertIn(expected, result.output) 

291 

292 def test_section_function(self): 

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

294 the command function. 

295 

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

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

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

299 missing argument error). 

300 """ 

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

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

303 

304 

305class MWPathTest(unittest.TestCase): 

306 

307 def getCmd(self, exists): 

308 

309 @click.command() 

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

311 def cmd(name): 

312 pass 

313 return cmd 

314 

315 def setUp(self): 

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

317 

318 def test_exist(self): 

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

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

321 or may not exist.""" 

322 with self.runner.isolated_filesystem(): 

323 mustExistCmd = self.getCmd(exists=True) 

324 mayExistCmd = self.getCmd(exists=None) 

325 mustNotExistCmd = self.getCmd(exists=False) 

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

327 

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

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

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

331 

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

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

334 

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

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

337 

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

339 # removed everything inside will be removed. 

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

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

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

343 

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

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

346 

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

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

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

350 

351 

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

353 unittest.main()