test_shellcomp.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625
  1. # Copyright (c) Twisted Matrix Laboratories.
  2. # See LICENSE for details.
  3. """
  4. Test cases for twisted.python._shellcomp
  5. """
  6. from __future__ import division, absolute_import
  7. import sys
  8. from io import BytesIO
  9. from twisted.trial import unittest
  10. from twisted.python import _shellcomp, usage, reflect
  11. from twisted.python.usage import Completions, Completer, CompleteFiles
  12. from twisted.python.usage import CompleteList
  13. class ZshScriptTestMeta(type):
  14. """
  15. Metaclass of ZshScriptTestMixin.
  16. """
  17. def __new__(cls, name, bases, attrs):
  18. def makeTest(cmdName, optionsFQPN):
  19. def runTest(self):
  20. return test_genZshFunction(self, cmdName, optionsFQPN)
  21. return runTest
  22. # add test_ methods to the class for each script
  23. # we are testing.
  24. if 'generateFor' in attrs:
  25. for cmdName, optionsFQPN in attrs['generateFor']:
  26. test = makeTest(cmdName, optionsFQPN)
  27. attrs['test_genZshFunction_' + cmdName] = test
  28. return type.__new__(cls, name, bases, attrs)
  29. class ZshScriptTestMixin(object):
  30. """
  31. Integration test helper to show that C{usage.Options} classes can have zsh
  32. completion functions generated for them without raising errors.
  33. In your subclasses set a class variable like so:
  34. # | cmd name | Fully Qualified Python Name of Options class |
  35. #
  36. generateFor = [('conch', 'twisted.conch.scripts.conch.ClientOptions'),
  37. ('twistd', 'twisted.scripts.twistd.ServerOptions'),
  38. ]
  39. Each package that contains Twisted scripts should contain one TestCase
  40. subclass which also inherits from this mixin, and contains a C{generateFor}
  41. list appropriate for the scripts in that package.
  42. """
  43. __metaclass__ = ZshScriptTestMeta
  44. def test_genZshFunction(self, cmdName, optionsFQPN):
  45. """
  46. Generate completion functions for given twisted command - no errors
  47. should be raised
  48. @type cmdName: C{str}
  49. @param cmdName: The name of the command-line utility e.g. 'twistd'
  50. @type optionsFQPN: C{str}
  51. @param optionsFQPN: The Fully Qualified Python Name of the C{Options}
  52. class to be tested.
  53. """
  54. outputFile = BytesIO()
  55. self.patch(usage.Options, '_shellCompFile', outputFile)
  56. # some scripts won't import or instantiate because of missing
  57. # dependencies (pyOpenSSL, etc) so we have to skip them.
  58. try:
  59. o = reflect.namedAny(optionsFQPN)()
  60. except Exception as e:
  61. raise unittest.SkipTest("Couldn't import or instantiate "
  62. "Options class: %s" % (e,))
  63. try:
  64. o.parseOptions(["", "--_shell-completion", "zsh:2"])
  65. except ImportError as e:
  66. # this can happen for commands which don't have all
  67. # the necessary dependencies installed. skip test.
  68. # skip
  69. raise unittest.SkipTest("ImportError calling parseOptions(): %s", (e,))
  70. except SystemExit:
  71. pass # expected
  72. else:
  73. self.fail('SystemExit not raised')
  74. outputFile.seek(0)
  75. # test that we got some output
  76. self.assertEqual(1, len(outputFile.read(1)))
  77. outputFile.seek(0)
  78. outputFile.truncate()
  79. # now, if it has sub commands, we have to test those too
  80. if hasattr(o, 'subCommands'):
  81. for (cmd, short, parser, doc) in o.subCommands:
  82. try:
  83. o.parseOptions([cmd, "", "--_shell-completion",
  84. "zsh:3"])
  85. except ImportError as e:
  86. # this can happen for commands which don't have all
  87. # the necessary dependencies installed. skip test.
  88. raise unittest.SkipTest("ImportError calling parseOptions() "
  89. "on subcommand: %s", (e,))
  90. except SystemExit:
  91. pass # expected
  92. else:
  93. self.fail('SystemExit not raised')
  94. outputFile.seek(0)
  95. # test that we got some output
  96. self.assertEqual(1, len(outputFile.read(1)))
  97. outputFile.seek(0)
  98. outputFile.truncate()
  99. # flushed because we don't want DeprecationWarnings to be printed when
  100. # running these test cases.
  101. self.flushWarnings()
  102. class ZshTests(unittest.TestCase):
  103. """
  104. Tests for zsh completion code
  105. """
  106. def test_accumulateMetadata(self):
  107. """
  108. Are `compData' attributes you can place on Options classes
  109. picked up correctly?
  110. """
  111. opts = FighterAceExtendedOptions()
  112. ag = _shellcomp.ZshArgumentsGenerator(opts, 'ace', 'dummy_value')
  113. descriptions = FighterAceOptions.compData.descriptions.copy()
  114. descriptions.update(FighterAceExtendedOptions.compData.descriptions)
  115. self.assertEqual(ag.descriptions, descriptions)
  116. self.assertEqual(ag.multiUse,
  117. set(FighterAceOptions.compData.multiUse))
  118. self.assertEqual(ag.mutuallyExclusive,
  119. FighterAceOptions.compData.mutuallyExclusive)
  120. optActions = FighterAceOptions.compData.optActions.copy()
  121. optActions.update(FighterAceExtendedOptions.compData.optActions)
  122. self.assertEqual(ag.optActions, optActions)
  123. self.assertEqual(ag.extraActions,
  124. FighterAceOptions.compData.extraActions)
  125. def test_mutuallyExclusiveCornerCase(self):
  126. """
  127. Exercise a corner-case of ZshArgumentsGenerator.makeExcludesDict()
  128. where the long option name already exists in the `excludes` dict being
  129. built.
  130. """
  131. class OddFighterAceOptions(FighterAceExtendedOptions):
  132. # since "fokker", etc, are already defined as mutually-
  133. # exclusive on the super-class, defining them again here forces
  134. # the corner-case to be exercised.
  135. optFlags = [['anatra', None,
  136. 'Select the Anatra DS as your dogfighter aircraft']]
  137. compData = Completions(
  138. mutuallyExclusive=[['anatra', 'fokker', 'albatros',
  139. 'spad', 'bristol']])
  140. opts = OddFighterAceOptions()
  141. ag = _shellcomp.ZshArgumentsGenerator(opts, 'ace', 'dummy_value')
  142. expected = {
  143. 'albatros': set(['anatra', 'b', 'bristol', 'f',
  144. 'fokker', 's', 'spad']),
  145. 'anatra': set(['a', 'albatros', 'b', 'bristol',
  146. 'f', 'fokker', 's', 'spad']),
  147. 'bristol': set(['a', 'albatros', 'anatra', 'f',
  148. 'fokker', 's', 'spad']),
  149. 'fokker': set(['a', 'albatros', 'anatra', 'b',
  150. 'bristol', 's', 'spad']),
  151. 'spad': set(['a', 'albatros', 'anatra', 'b',
  152. 'bristol', 'f', 'fokker'])}
  153. self.assertEqual(ag.excludes, expected)
  154. def test_accumulateAdditionalOptions(self):
  155. """
  156. We pick up options that are only defined by having an
  157. appropriately named method on your Options class,
  158. e.g. def opt_foo(self, foo)
  159. """
  160. opts = FighterAceExtendedOptions()
  161. ag = _shellcomp.ZshArgumentsGenerator(opts, 'ace', 'dummy_value')
  162. self.assertIn('nocrash', ag.flagNameToDefinition)
  163. self.assertIn('nocrash', ag.allOptionsNameToDefinition)
  164. self.assertIn('difficulty', ag.paramNameToDefinition)
  165. self.assertIn('difficulty', ag.allOptionsNameToDefinition)
  166. def test_verifyZshNames(self):
  167. """
  168. Using a parameter/flag name that doesn't exist
  169. will raise an error
  170. """
  171. class TmpOptions(FighterAceExtendedOptions):
  172. # Note typo of detail
  173. compData = Completions(optActions={'detaill' : None})
  174. self.assertRaises(ValueError, _shellcomp.ZshArgumentsGenerator,
  175. TmpOptions(), 'ace', 'dummy_value')
  176. class TmpOptions2(FighterAceExtendedOptions):
  177. # Note that 'foo' and 'bar' are not real option
  178. # names defined in this class
  179. compData = Completions(
  180. mutuallyExclusive=[("foo", "bar")])
  181. self.assertRaises(ValueError, _shellcomp.ZshArgumentsGenerator,
  182. TmpOptions2(), 'ace', 'dummy_value')
  183. def test_zshCode(self):
  184. """
  185. Generate a completion function, and test the textual output
  186. against a known correct output
  187. """
  188. outputFile = BytesIO()
  189. self.patch(usage.Options, '_shellCompFile', outputFile)
  190. self.patch(sys, 'argv', ["silly", "", "--_shell-completion", "zsh:2"])
  191. opts = SimpleProgOptions()
  192. self.assertRaises(SystemExit, opts.parseOptions)
  193. self.assertEqual(testOutput1, outputFile.getvalue())
  194. def test_zshCodeWithSubs(self):
  195. """
  196. Generate a completion function with subcommands,
  197. and test the textual output against a known correct output
  198. """
  199. outputFile = BytesIO()
  200. self.patch(usage.Options, '_shellCompFile', outputFile)
  201. self.patch(sys, 'argv', ["silly2", "", "--_shell-completion", "zsh:2"])
  202. opts = SimpleProgWithSubcommands()
  203. self.assertRaises(SystemExit, opts.parseOptions)
  204. self.assertEqual(testOutput2, outputFile.getvalue())
  205. def test_incompleteCommandLine(self):
  206. """
  207. Completion still happens even if a command-line is given
  208. that would normally throw UsageError.
  209. """
  210. outputFile = BytesIO()
  211. self.patch(usage.Options, '_shellCompFile', outputFile)
  212. opts = FighterAceOptions()
  213. self.assertRaises(SystemExit, opts.parseOptions,
  214. ["--fokker", "server", "--unknown-option",
  215. "--unknown-option2",
  216. "--_shell-completion", "zsh:5"])
  217. outputFile.seek(0)
  218. # test that we got some output
  219. self.assertEqual(1, len(outputFile.read(1)))
  220. def test_incompleteCommandLine_case2(self):
  221. """
  222. Completion still happens even if a command-line is given
  223. that would normally throw UsageError.
  224. The existence of --unknown-option prior to the subcommand
  225. will break subcommand detection... but we complete anyway
  226. """
  227. outputFile = BytesIO()
  228. self.patch(usage.Options, '_shellCompFile', outputFile)
  229. opts = FighterAceOptions()
  230. self.assertRaises(SystemExit, opts.parseOptions,
  231. ["--fokker", "--unknown-option", "server",
  232. "--list-server", "--_shell-completion", "zsh:5"])
  233. outputFile.seek(0)
  234. # test that we got some output
  235. self.assertEqual(1, len(outputFile.read(1)))
  236. outputFile.seek(0)
  237. outputFile.truncate()
  238. def test_incompleteCommandLine_case3(self):
  239. """
  240. Completion still happens even if a command-line is given
  241. that would normally throw UsageError.
  242. Break subcommand detection in a different way by providing
  243. an invalid subcommand name.
  244. """
  245. outputFile = BytesIO()
  246. self.patch(usage.Options, '_shellCompFile', outputFile)
  247. opts = FighterAceOptions()
  248. self.assertRaises(SystemExit, opts.parseOptions,
  249. ["--fokker", "unknown-subcommand",
  250. "--list-server", "--_shell-completion", "zsh:4"])
  251. outputFile.seek(0)
  252. # test that we got some output
  253. self.assertEqual(1, len(outputFile.read(1)))
  254. def test_skipSubcommandList(self):
  255. """
  256. Ensure the optimization which skips building the subcommand list
  257. under certain conditions isn't broken.
  258. """
  259. outputFile = BytesIO()
  260. self.patch(usage.Options, '_shellCompFile', outputFile)
  261. opts = FighterAceOptions()
  262. self.assertRaises(SystemExit, opts.parseOptions,
  263. ["--alba", "--_shell-completion", "zsh:2"])
  264. outputFile.seek(0)
  265. # test that we got some output
  266. self.assertEqual(1, len(outputFile.read(1)))
  267. def test_poorlyDescribedOptMethod(self):
  268. """
  269. Test corner case fetching an option description from a method docstring
  270. """
  271. opts = FighterAceOptions()
  272. argGen = _shellcomp.ZshArgumentsGenerator(opts, 'ace', None)
  273. descr = argGen.getDescription('silly')
  274. # docstring for opt_silly is useless so it should just use the
  275. # option name as the description
  276. self.assertEqual(descr, 'silly')
  277. def test_brokenActions(self):
  278. """
  279. A C{Completer} with repeat=True may only be used as the
  280. last item in the extraActions list.
  281. """
  282. class BrokenActions(usage.Options):
  283. compData = usage.Completions(
  284. extraActions=[usage.Completer(repeat=True),
  285. usage.Completer()]
  286. )
  287. outputFile = BytesIO()
  288. opts = BrokenActions()
  289. self.patch(opts, '_shellCompFile', outputFile)
  290. self.assertRaises(ValueError, opts.parseOptions,
  291. ["", "--_shell-completion", "zsh:2"])
  292. def test_optMethodsDontOverride(self):
  293. """
  294. opt_* methods on Options classes should not override the
  295. data provided in optFlags or optParameters.
  296. """
  297. class Options(usage.Options):
  298. optFlags = [['flag', 'f', 'A flag']]
  299. optParameters = [['param', 'p', None, 'A param']]
  300. def opt_flag(self):
  301. """ junk description """
  302. def opt_param(self, param):
  303. """ junk description """
  304. opts = Options()
  305. argGen = _shellcomp.ZshArgumentsGenerator(opts, 'ace', None)
  306. self.assertEqual(argGen.getDescription('flag'), 'A flag')
  307. self.assertEqual(argGen.getDescription('param'), 'A param')
  308. class EscapeTests(unittest.TestCase):
  309. def test_escape(self):
  310. """
  311. Verify _shellcomp.escape() function
  312. """
  313. esc = _shellcomp.escape
  314. test = "$"
  315. self.assertEqual(esc(test), "'$'")
  316. test = 'A--\'$"\\`--B'
  317. self.assertEqual(esc(test), '"A--\'\\$\\"\\\\\\`--B"')
  318. class CompleterNotImplementedTests(unittest.TestCase):
  319. """
  320. Test that using an unknown shell constant with SubcommandAction
  321. raises NotImplementedError
  322. The other Completer() subclasses are tested in test_usage.py
  323. """
  324. def test_unknownShell(self):
  325. """
  326. Using an unknown shellType should raise NotImplementedError
  327. """
  328. action = _shellcomp.SubcommandAction()
  329. self.assertRaises(NotImplementedError, action._shellCode,
  330. None, "bad_shell_type")
  331. class FighterAceServerOptions(usage.Options):
  332. """
  333. Options for FighterAce 'server' subcommand
  334. """
  335. optFlags = [['list-server', None,
  336. 'List this server with the online FighterAce network']]
  337. optParameters = [['packets-per-second', None,
  338. 'Number of update packets to send per second', '20']]
  339. class FighterAceOptions(usage.Options):
  340. """
  341. Command-line options for an imaginary `Fighter Ace` game
  342. """
  343. optFlags = [['fokker', 'f',
  344. 'Select the Fokker Dr.I as your dogfighter aircraft'],
  345. ['albatros', 'a',
  346. 'Select the Albatros D-III as your dogfighter aircraft'],
  347. ['spad', 's',
  348. 'Select the SPAD S.VII as your dogfighter aircraft'],
  349. ['bristol', 'b',
  350. 'Select the Bristol Scout as your dogfighter aircraft'],
  351. ['physics', 'p',
  352. 'Enable secret Twisted physics engine'],
  353. ['jam', 'j',
  354. 'Enable a small chance that your machine guns will jam!'],
  355. ['verbose', 'v',
  356. 'Verbose logging (may be specified more than once)'],
  357. ]
  358. optParameters = [['pilot-name', None, "What's your name, Ace?",
  359. 'Manfred von Richthofen'],
  360. ['detail', 'd',
  361. 'Select the level of rendering detail (1-5)', '3'],
  362. ]
  363. subCommands = [['server', None, FighterAceServerOptions,
  364. 'Start FighterAce game-server.'],
  365. ]
  366. compData = Completions(
  367. descriptions={'physics' : 'Twisted-Physics',
  368. 'detail' : 'Rendering detail level'},
  369. multiUse=['verbose'],
  370. mutuallyExclusive=[['fokker', 'albatros', 'spad',
  371. 'bristol']],
  372. optActions={'detail' : CompleteList(['1' '2' '3'
  373. '4' '5'])},
  374. extraActions=[CompleteFiles(descr='saved game file to load')]
  375. )
  376. def opt_silly(self):
  377. # A silly option which nobody can explain
  378. """ """
  379. class FighterAceExtendedOptions(FighterAceOptions):
  380. """
  381. Extend the options and zsh metadata provided by FighterAceOptions.
  382. _shellcomp must accumulate options and metadata from all classes in the
  383. hiearchy so this is important to test.
  384. """
  385. optFlags = [['no-stalls', None,
  386. 'Turn off the ability to stall your aircraft']]
  387. optParameters = [['reality-level', None,
  388. 'Select the level of physics reality (1-5)', '5']]
  389. compData = Completions(
  390. descriptions={'no-stalls' : 'Can\'t stall your plane'},
  391. optActions={'reality-level' :
  392. Completer(descr='Physics reality level')}
  393. )
  394. def opt_nocrash(self):
  395. """
  396. Select that you can't crash your plane
  397. """
  398. def opt_difficulty(self, difficulty):
  399. """
  400. How tough are you? (1-10)
  401. """
  402. def _accuracyAction():
  403. # add tick marks just to exercise quoting
  404. return CompleteList(['1', '2', '3'], descr='Accuracy\'`?')
  405. class SimpleProgOptions(usage.Options):
  406. """
  407. Command-line options for a `Silly` imaginary program
  408. """
  409. optFlags = [['color', 'c', 'Turn on color output'],
  410. ['gray', 'g', 'Turn on gray-scale output'],
  411. ['verbose', 'v',
  412. 'Verbose logging (may be specified more than once)'],
  413. ]
  414. optParameters = [['optimization', None, '5',
  415. 'Select the level of optimization (1-5)'],
  416. ['accuracy', 'a', '3',
  417. 'Select the level of accuracy (1-3)'],
  418. ]
  419. compData = Completions(
  420. descriptions={'color' : 'Color on',
  421. 'optimization' : 'Optimization level'},
  422. multiUse=['verbose'],
  423. mutuallyExclusive=[['color', 'gray']],
  424. optActions={'optimization' : CompleteList(['1', '2', '3', '4', '5'],
  425. descr='Optimization?'),
  426. 'accuracy' : _accuracyAction},
  427. extraActions=[CompleteFiles(descr='output file')]
  428. )
  429. def opt_X(self):
  430. """
  431. usage.Options does not recognize single-letter opt_ methods
  432. """
  433. class SimpleProgSub1(usage.Options):
  434. optFlags = [['sub-opt', 's', 'Sub Opt One']]
  435. class SimpleProgSub2(usage.Options):
  436. optFlags = [['sub-opt', 's', 'Sub Opt Two']]
  437. class SimpleProgWithSubcommands(SimpleProgOptions):
  438. optFlags = [['some-option'],
  439. ['other-option', 'o']]
  440. optParameters = [['some-param'],
  441. ['other-param', 'p'],
  442. ['another-param', 'P', 'Yet Another Param']]
  443. subCommands = [ ['sub1', None, SimpleProgSub1, 'Sub Command 1'],
  444. ['sub2', None, SimpleProgSub2, 'Sub Command 2']]
  445. testOutput1 = b"""#compdef silly
  446. _arguments -s -A "-*" \\
  447. ':output file (*):_files -g "*"' \\
  448. "(--accuracy)-a[Select the level of accuracy (1-3)]:Accuracy'\`?:(1 2 3)" \\
  449. "(-a)--accuracy=[Select the level of accuracy (1-3)]:Accuracy'\`?:(1 2 3)" \\
  450. '(--color --gray -g)-c[Color on]' \\
  451. '(--gray -c -g)--color[Color on]' \\
  452. '(--color --gray -c)-g[Turn on gray-scale output]' \\
  453. '(--color -c -g)--gray[Turn on gray-scale output]' \\
  454. '--help[Display this help and exit.]' \\
  455. '--optimization=[Optimization level]:Optimization?:(1 2 3 4 5)' \\
  456. '*-v[Verbose logging (may be specified more than once)]' \\
  457. '*--verbose[Verbose logging (may be specified more than once)]' \\
  458. '--version[Display Twisted version and exit.]' \\
  459. && return 0
  460. """
  461. # with sub-commands
  462. testOutput2 = b"""#compdef silly2
  463. _arguments -s -A "-*" \\
  464. '*::subcmd:->subcmd' \\
  465. ':output file (*):_files -g "*"' \\
  466. "(--accuracy)-a[Select the level of accuracy (1-3)]:Accuracy'\`?:(1 2 3)" \\
  467. "(-a)--accuracy=[Select the level of accuracy (1-3)]:Accuracy'\`?:(1 2 3)" \\
  468. '(--another-param)-P[another-param]:another-param:_files' \\
  469. '(-P)--another-param=[another-param]:another-param:_files' \\
  470. '(--color --gray -g)-c[Color on]' \\
  471. '(--gray -c -g)--color[Color on]' \\
  472. '(--color --gray -c)-g[Turn on gray-scale output]' \\
  473. '(--color -c -g)--gray[Turn on gray-scale output]' \\
  474. '--help[Display this help and exit.]' \\
  475. '--optimization=[Optimization level]:Optimization?:(1 2 3 4 5)' \\
  476. '(--other-option)-o[other-option]' \\
  477. '(-o)--other-option[other-option]' \\
  478. '(--other-param)-p[other-param]:other-param:_files' \\
  479. '(-p)--other-param=[other-param]:other-param:_files' \\
  480. '--some-option[some-option]' \\
  481. '--some-param=[some-param]:some-param:_files' \\
  482. '*-v[Verbose logging (may be specified more than once)]' \\
  483. '*--verbose[Verbose logging (may be specified more than once)]' \\
  484. '--version[Display Twisted version and exit.]' \\
  485. && return 0
  486. local _zsh_subcmds_array
  487. _zsh_subcmds_array=(
  488. "sub1:Sub Command 1"
  489. "sub2:Sub Command 2"
  490. )
  491. _describe "sub-command" _zsh_subcmds_array
  492. """