1 # Copyright (c) Twisted Matrix Laboratories.
2 # See LICENSE for details.
5 Test cases for twisted.python._shellcomp
9 from cStringIO import StringIO
11 from twisted.trial import unittest
12 from twisted.python import _shellcomp, usage, reflect
13 from twisted.python.usage import Completions, Completer, CompleteFiles
14 from twisted.python.usage import CompleteList
18 class ZshScriptTestMeta(type):
20 Metaclass of ZshScriptTestMixin.
22 def __new__(cls, name, bases, attrs):
23 def makeTest(cmdName, optionsFQPN):
25 return test_genZshFunction(self, cmdName, optionsFQPN)
28 # add test_ methods to the class for each script
30 if 'generateFor' in attrs:
31 for cmdName, optionsFQPN in attrs['generateFor']:
32 test = makeTest(cmdName, optionsFQPN)
33 attrs['test_genZshFunction_' + cmdName] = test
35 return type.__new__(cls, name, bases, attrs)
39 class ZshScriptTestMixin(object):
41 Integration test helper to show that C{usage.Options} classes can have zsh
42 completion functions generated for them without raising errors.
44 In your subclasses set a class variable like so:
46 # | cmd name | Fully Qualified Python Name of Options class |
48 generateFor = [('conch', 'twisted.conch.scripts.conch.ClientOptions'),
49 ('twistd', 'twisted.scripts.twistd.ServerOptions'),
52 Each package that contains Twisted scripts should contain one TestCase
53 subclass which also inherits from this mixin, and contains a C{generateFor}
54 list appropriate for the scripts in that package.
56 __metaclass__ = ZshScriptTestMeta
60 def test_genZshFunction(self, cmdName, optionsFQPN):
62 Generate completion functions for given twisted command - no errors
66 @param cmdName: The name of the command-line utility e.g. 'twistd'
68 @type optionsFQPN: C{str}
69 @param optionsFQPN: The Fully Qualified Python Name of the C{Options}
72 outputFile = StringIO()
73 self.patch(usage.Options, '_shellCompFile', outputFile)
75 # some scripts won't import or instantiate because of missing
76 # dependencies (PyCrypto, etc) so we have to skip them.
78 o = reflect.namedAny(optionsFQPN)()
80 raise unittest.SkipTest("Couldn't import or instantiate "
81 "Options class: %s" % (e,))
84 o.parseOptions(["", "--_shell-completion", "zsh:2"])
85 except ImportError, e:
86 # this can happen for commands which don't have all
87 # the necessary dependencies installed. skip test.
89 raise unittest.SkipTest("ImportError calling parseOptions(): %s", (e,))
93 self.fail('SystemExit not raised')
95 # test that we got some output
96 self.assertEqual(1, len(outputFile.read(1)))
100 # now, if it has sub commands, we have to test those too
101 if hasattr(o, 'subCommands'):
102 for (cmd, short, parser, doc) in o.subCommands:
104 o.parseOptions([cmd, "", "--_shell-completion",
106 except ImportError, e:
107 # this can happen for commands which don't have all
108 # the necessary dependencies installed. skip test.
109 raise unittest.SkipTest("ImportError calling parseOptions() "
110 "on subcommand: %s", (e,))
114 self.fail('SystemExit not raised')
117 # test that we got some output
118 self.assertEqual(1, len(outputFile.read(1)))
120 outputFile.truncate()
122 # flushed because we don't want DeprecationWarnings to be printed when
123 # running these test cases.
128 class ZshTestCase(unittest.TestCase):
130 Tests for zsh completion code
132 def test_accumulateMetadata(self):
134 Are `compData' attributes you can place on Options classes
137 opts = FighterAceExtendedOptions()
138 ag = _shellcomp.ZshArgumentsGenerator(opts, 'ace', 'dummy_value')
140 descriptions = FighterAceOptions.compData.descriptions.copy()
141 descriptions.update(FighterAceExtendedOptions.compData.descriptions)
143 self.assertEqual(ag.descriptions, descriptions)
144 self.assertEqual(ag.multiUse,
145 set(FighterAceOptions.compData.multiUse))
146 self.assertEqual(ag.mutuallyExclusive,
147 FighterAceOptions.compData.mutuallyExclusive)
149 optActions = FighterAceOptions.compData.optActions.copy()
150 optActions.update(FighterAceExtendedOptions.compData.optActions)
151 self.assertEqual(ag.optActions, optActions)
153 self.assertEqual(ag.extraActions,
154 FighterAceOptions.compData.extraActions)
157 def test_mutuallyExclusiveCornerCase(self):
159 Exercise a corner-case of ZshArgumentsGenerator.makeExcludesDict()
160 where the long option name already exists in the `excludes` dict being
163 class OddFighterAceOptions(FighterAceExtendedOptions):
164 # since "fokker", etc, are already defined as mutually-
165 # exclusive on the super-class, defining them again here forces
166 # the corner-case to be exercised.
167 optFlags = [['anatra', None,
168 'Select the Anatra DS as your dogfighter aircraft']]
169 compData = Completions(
170 mutuallyExclusive=[['anatra', 'fokker', 'albatros',
173 opts = OddFighterAceOptions()
174 ag = _shellcomp.ZshArgumentsGenerator(opts, 'ace', 'dummy_value')
177 'albatros': set(['anatra', 'b', 'bristol', 'f',
178 'fokker', 's', 'spad']),
179 'anatra': set(['a', 'albatros', 'b', 'bristol',
180 'f', 'fokker', 's', 'spad']),
181 'bristol': set(['a', 'albatros', 'anatra', 'f',
182 'fokker', 's', 'spad']),
183 'fokker': set(['a', 'albatros', 'anatra', 'b',
184 'bristol', 's', 'spad']),
185 'spad': set(['a', 'albatros', 'anatra', 'b',
186 'bristol', 'f', 'fokker'])}
188 self.assertEqual(ag.excludes, expected)
191 def test_accumulateAdditionalOptions(self):
193 We pick up options that are only defined by having an
194 appropriately named method on your Options class,
195 e.g. def opt_foo(self, foo)
197 opts = FighterAceExtendedOptions()
198 ag = _shellcomp.ZshArgumentsGenerator(opts, 'ace', 'dummy_value')
200 self.assertIn('nocrash', ag.flagNameToDefinition)
201 self.assertIn('nocrash', ag.allOptionsNameToDefinition)
203 self.assertIn('difficulty', ag.paramNameToDefinition)
204 self.assertIn('difficulty', ag.allOptionsNameToDefinition)
207 def test_verifyZshNames(self):
209 Using a parameter/flag name that doesn't exist
212 class TmpOptions(FighterAceExtendedOptions):
213 # Note typo of detail
214 compData = Completions(optActions={'detaill' : None})
216 self.assertRaises(ValueError, _shellcomp.ZshArgumentsGenerator,
217 TmpOptions(), 'ace', 'dummy_value')
219 class TmpOptions2(FighterAceExtendedOptions):
220 # Note that 'foo' and 'bar' are not real option
221 # names defined in this class
222 compData = Completions(
223 mutuallyExclusive=[("foo", "bar")])
225 self.assertRaises(ValueError, _shellcomp.ZshArgumentsGenerator,
226 TmpOptions2(), 'ace', 'dummy_value')
229 def test_zshCode(self):
231 Generate a completion function, and test the textual output
232 against a known correct output
234 outputFile = StringIO()
235 self.patch(usage.Options, '_shellCompFile', outputFile)
236 self.patch(sys, 'argv', ["silly", "", "--_shell-completion", "zsh:2"])
237 opts = SimpleProgOptions()
238 self.assertRaises(SystemExit, opts.parseOptions)
239 self.assertEqual(testOutput1, outputFile.getvalue())
242 def test_zshCodeWithSubs(self):
244 Generate a completion function with subcommands,
245 and test the textual output against a known correct output
247 outputFile = StringIO()
248 self.patch(usage.Options, '_shellCompFile', outputFile)
249 self.patch(sys, 'argv', ["silly2", "", "--_shell-completion", "zsh:2"])
250 opts = SimpleProgWithSubcommands()
251 self.assertRaises(SystemExit, opts.parseOptions)
252 self.assertEqual(testOutput2, outputFile.getvalue())
255 def test_incompleteCommandLine(self):
257 Completion still happens even if a command-line is given
258 that would normally throw UsageError.
260 outputFile = StringIO()
261 self.patch(usage.Options, '_shellCompFile', outputFile)
262 opts = FighterAceOptions()
264 self.assertRaises(SystemExit, opts.parseOptions,
265 ["--fokker", "server", "--unknown-option",
267 "--_shell-completion", "zsh:5"])
269 # test that we got some output
270 self.assertEqual(1, len(outputFile.read(1)))
273 def test_incompleteCommandLine_case2(self):
275 Completion still happens even if a command-line is given
276 that would normally throw UsageError.
278 The existance of --unknown-option prior to the subcommand
279 will break subcommand detection... but we complete anyway
281 outputFile = StringIO()
282 self.patch(usage.Options, '_shellCompFile', outputFile)
283 opts = FighterAceOptions()
285 self.assertRaises(SystemExit, opts.parseOptions,
286 ["--fokker", "--unknown-option", "server",
287 "--list-server", "--_shell-completion", "zsh:5"])
289 # test that we got some output
290 self.assertEqual(1, len(outputFile.read(1)))
293 outputFile.truncate()
296 def test_incompleteCommandLine_case3(self):
298 Completion still happens even if a command-line is given
299 that would normally throw UsageError.
301 Break subcommand detection in a different way by providing
302 an invalid subcommand name.
304 outputFile = StringIO()
305 self.patch(usage.Options, '_shellCompFile', outputFile)
306 opts = FighterAceOptions()
308 self.assertRaises(SystemExit, opts.parseOptions,
309 ["--fokker", "unknown-subcommand",
310 "--list-server", "--_shell-completion", "zsh:4"])
312 # test that we got some output
313 self.assertEqual(1, len(outputFile.read(1)))
316 def test_skipSubcommandList(self):
318 Ensure the optimization which skips building the subcommand list
319 under certain conditions isn't broken.
321 outputFile = StringIO()
322 self.patch(usage.Options, '_shellCompFile', outputFile)
323 opts = FighterAceOptions()
325 self.assertRaises(SystemExit, opts.parseOptions,
326 ["--alba", "--_shell-completion", "zsh:2"])
328 # test that we got some output
329 self.assertEqual(1, len(outputFile.read(1)))
332 def test_poorlyDescribedOptMethod(self):
334 Test corner case fetching an option description from a method docstring
336 opts = FighterAceOptions()
337 argGen = _shellcomp.ZshArgumentsGenerator(opts, 'ace', None)
339 descr = argGen.getDescription('silly')
341 # docstring for opt_silly is useless so it should just use the
342 # option name as the description
343 self.assertEqual(descr, 'silly')
346 def test_brokenActions(self):
348 A C{Completer} with repeat=True may only be used as the
349 last item in the extraActions list.
351 class BrokenActions(usage.Options):
352 compData = usage.Completions(
353 extraActions=[usage.Completer(repeat=True),
357 outputFile = StringIO()
358 opts = BrokenActions()
359 self.patch(opts, '_shellCompFile', outputFile)
360 self.assertRaises(ValueError, opts.parseOptions,
361 ["", "--_shell-completion", "zsh:2"])
364 def test_optMethodsDontOverride(self):
366 opt_* methods on Options classes should not override the
367 data provided in optFlags or optParameters.
369 class Options(usage.Options):
370 optFlags = [['flag', 'f', 'A flag']]
371 optParameters = [['param', 'p', None, 'A param']]
374 """ junk description """
376 def opt_param(self, param):
377 """ junk description """
380 argGen = _shellcomp.ZshArgumentsGenerator(opts, 'ace', None)
382 self.assertEqual(argGen.getDescription('flag'), 'A flag')
383 self.assertEqual(argGen.getDescription('param'), 'A param')
387 class EscapeTestCase(unittest.TestCase):
388 def test_escape(self):
390 Verify _shellcomp.escape() function
392 esc = _shellcomp.escape
395 self.assertEqual(esc(test), "'$'")
397 test = 'A--\'$"\\`--B'
398 self.assertEqual(esc(test), '"A--\'\\$\\"\\\\\\`--B"')
402 class CompleterNotImplementedTestCase(unittest.TestCase):
404 Test that using an unknown shell constant with SubcommandAction
405 raises NotImplementedError
407 The other Completer() subclasses are tested in test_usage.py
409 def test_unknownShell(self):
411 Using an unknown shellType should raise NotImplementedError
413 action = _shellcomp.SubcommandAction()
415 self.assertRaises(NotImplementedError, action._shellCode,
416 None, "bad_shell_type")
420 class FighterAceServerOptions(usage.Options):
422 Options for FighterAce 'server' subcommand
424 optFlags = [['list-server', None,
425 'List this server with the online FighterAce network']]
426 optParameters = [['packets-per-second', None,
427 'Number of update packets to send per second', '20']]
431 class FighterAceOptions(usage.Options):
433 Command-line options for an imaginary `Fighter Ace` game
435 optFlags = [['fokker', 'f',
436 'Select the Fokker Dr.I as your dogfighter aircraft'],
438 'Select the Albatros D-III as your dogfighter aircraft'],
440 'Select the SPAD S.VII as your dogfighter aircraft'],
442 'Select the Bristol Scout as your dogfighter aircraft'],
444 'Enable secret Twisted physics engine'],
446 'Enable a small chance that your machine guns will jam!'],
448 'Verbose logging (may be specified more than once)'],
451 optParameters = [['pilot-name', None, "What's your name, Ace?",
452 'Manfred von Richthofen'],
454 'Select the level of rendering detail (1-5)', '3'],
457 subCommands = [['server', None, FighterAceServerOptions,
458 'Start FighterAce game-server.'],
461 compData = Completions(
462 descriptions={'physics' : 'Twisted-Physics',
463 'detail' : 'Rendering detail level'},
464 multiUse=['verbose'],
465 mutuallyExclusive=[['fokker', 'albatros', 'spad',
467 optActions={'detail' : CompleteList(['1' '2' '3'
469 extraActions=[CompleteFiles(descr='saved game file to load')]
473 # A silly option which nobody can explain
478 class FighterAceExtendedOptions(FighterAceOptions):
480 Extend the options and zsh metadata provided by FighterAceOptions.
481 _shellcomp must accumulate options and metadata from all classes in the
482 hiearchy so this is important to test.
484 optFlags = [['no-stalls', None,
485 'Turn off the ability to stall your aircraft']]
486 optParameters = [['reality-level', None,
487 'Select the level of physics reality (1-5)', '5']]
489 compData = Completions(
490 descriptions={'no-stalls' : 'Can\'t stall your plane'},
491 optActions={'reality-level' :
492 Completer(descr='Physics reality level')}
495 def opt_nocrash(self):
497 Select that you can't crash your plane
501 def opt_difficulty(self, difficulty):
503 How tough are you? (1-10)
508 def _accuracyAction():
509 # add tick marks just to exercise quoting
510 return CompleteList(['1', '2', '3'], descr='Accuracy\'`?')
514 class SimpleProgOptions(usage.Options):
516 Command-line options for a `Silly` imaginary program
518 optFlags = [['color', 'c', 'Turn on color output'],
519 ['gray', 'g', 'Turn on gray-scale output'],
521 'Verbose logging (may be specified more than once)'],
524 optParameters = [['optimization', None, '5',
525 'Select the level of optimization (1-5)'],
526 ['accuracy', 'a', '3',
527 'Select the level of accuracy (1-3)'],
531 compData = Completions(
532 descriptions={'color' : 'Color on',
533 'optimization' : 'Optimization level'},
534 multiUse=['verbose'],
535 mutuallyExclusive=[['color', 'gray']],
536 optActions={'optimization' : CompleteList(['1', '2', '3', '4', '5'],
537 descr='Optimization?'),
538 'accuracy' : _accuracyAction},
539 extraActions=[CompleteFiles(descr='output file')]
544 usage.Options does not recognize single-letter opt_ methods
549 class SimpleProgSub1(usage.Options):
550 optFlags = [['sub-opt', 's', 'Sub Opt One']]
554 class SimpleProgSub2(usage.Options):
555 optFlags = [['sub-opt', 's', 'Sub Opt Two']]
559 class SimpleProgWithSubcommands(SimpleProgOptions):
560 optFlags = [['some-option'],
561 ['other-option', 'o']]
563 optParameters = [['some-param'],
564 ['other-param', 'p'],
565 ['another-param', 'P', 'Yet Another Param']]
567 subCommands = [ ['sub1', None, SimpleProgSub1, 'Sub Command 1'],
568 ['sub2', None, SimpleProgSub2, 'Sub Command 2']]
572 testOutput1 = """#compdef silly
574 _arguments -s -A "-*" \\
575 ':output file (*):_files -g "*"' \\
576 "(--accuracy)-a[Select the level of accuracy (1-3)]:Accuracy'\`?:(1 2 3)" \\
577 "(-a)--accuracy=[Select the level of accuracy (1-3)]:Accuracy'\`?:(1 2 3)" \\
578 '(--color --gray -g)-c[Color on]' \\
579 '(--gray -c -g)--color[Color on]' \\
580 '(--color --gray -c)-g[Turn on gray-scale output]' \\
581 '(--color -c -g)--gray[Turn on gray-scale output]' \\
582 '--help[Display this help and exit.]' \\
583 '--optimization=[Optimization level]:Optimization?:(1 2 3 4 5)' \\
584 '*-v[Verbose logging (may be specified more than once)]' \\
585 '*--verbose[Verbose logging (may be specified more than once)]' \\
586 '--version[Display Twisted version and exit.]' \\
591 testOutput2 = """#compdef silly2
593 _arguments -s -A "-*" \\
594 '*::subcmd:->subcmd' \\
595 ':output file (*):_files -g "*"' \\
596 "(--accuracy)-a[Select the level of accuracy (1-3)]:Accuracy'\`?:(1 2 3)" \\
597 "(-a)--accuracy=[Select the level of accuracy (1-3)]:Accuracy'\`?:(1 2 3)" \\
598 '(--another-param)-P[another-param]:another-param:_files' \\
599 '(-P)--another-param=[another-param]:another-param:_files' \\
600 '(--color --gray -g)-c[Color on]' \\
601 '(--gray -c -g)--color[Color on]' \\
602 '(--color --gray -c)-g[Turn on gray-scale output]' \\
603 '(--color -c -g)--gray[Turn on gray-scale output]' \\
604 '--help[Display this help and exit.]' \\
605 '--optimization=[Optimization level]:Optimization?:(1 2 3 4 5)' \\
606 '(--other-option)-o[other-option]' \\
607 '(-o)--other-option[other-option]' \\
608 '(--other-param)-p[other-param]:other-param:_files' \\
609 '(-p)--other-param=[other-param]:other-param:_files' \\
610 '--some-option[some-option]' \\
611 '--some-param=[some-param]:some-param:_files' \\
612 '*-v[Verbose logging (may be specified more than once)]' \\
613 '*--verbose[Verbose logging (may be specified more than once)]' \\
614 '--version[Display Twisted version and exit.]' \\
616 local _zsh_subcmds_array
622 _describe "sub-command" _zsh_subcmds_array