123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625 |
- # Copyright (c) Twisted Matrix Laboratories.
- # See LICENSE for details.
- """
- Test cases for twisted.python._shellcomp
- """
- from __future__ import division, absolute_import
- import sys
- from io import BytesIO
- from twisted.trial import unittest
- from twisted.python import _shellcomp, usage, reflect
- from twisted.python.usage import Completions, Completer, CompleteFiles
- from twisted.python.usage import CompleteList
- class ZshScriptTestMeta(type):
- """
- Metaclass of ZshScriptTestMixin.
- """
- def __new__(cls, name, bases, attrs):
- def makeTest(cmdName, optionsFQPN):
- def runTest(self):
- return test_genZshFunction(self, cmdName, optionsFQPN)
- return runTest
- # add test_ methods to the class for each script
- # we are testing.
- if 'generateFor' in attrs:
- for cmdName, optionsFQPN in attrs['generateFor']:
- test = makeTest(cmdName, optionsFQPN)
- attrs['test_genZshFunction_' + cmdName] = test
- return type.__new__(cls, name, bases, attrs)
- class ZshScriptTestMixin(object):
- """
- Integration test helper to show that C{usage.Options} classes can have zsh
- completion functions generated for them without raising errors.
- In your subclasses set a class variable like so:
- # | cmd name | Fully Qualified Python Name of Options class |
- #
- generateFor = [('conch', 'twisted.conch.scripts.conch.ClientOptions'),
- ('twistd', 'twisted.scripts.twistd.ServerOptions'),
- ]
- Each package that contains Twisted scripts should contain one TestCase
- subclass which also inherits from this mixin, and contains a C{generateFor}
- list appropriate for the scripts in that package.
- """
- __metaclass__ = ZshScriptTestMeta
- def test_genZshFunction(self, cmdName, optionsFQPN):
- """
- Generate completion functions for given twisted command - no errors
- should be raised
- @type cmdName: C{str}
- @param cmdName: The name of the command-line utility e.g. 'twistd'
- @type optionsFQPN: C{str}
- @param optionsFQPN: The Fully Qualified Python Name of the C{Options}
- class to be tested.
- """
- outputFile = BytesIO()
- self.patch(usage.Options, '_shellCompFile', outputFile)
- # some scripts won't import or instantiate because of missing
- # dependencies (pyOpenSSL, etc) so we have to skip them.
- try:
- o = reflect.namedAny(optionsFQPN)()
- except Exception as e:
- raise unittest.SkipTest("Couldn't import or instantiate "
- "Options class: %s" % (e,))
- try:
- o.parseOptions(["", "--_shell-completion", "zsh:2"])
- except ImportError as e:
- # this can happen for commands which don't have all
- # the necessary dependencies installed. skip test.
- # skip
- raise unittest.SkipTest("ImportError calling parseOptions(): %s", (e,))
- except SystemExit:
- pass # expected
- else:
- self.fail('SystemExit not raised')
- outputFile.seek(0)
- # test that we got some output
- self.assertEqual(1, len(outputFile.read(1)))
- outputFile.seek(0)
- outputFile.truncate()
- # now, if it has sub commands, we have to test those too
- if hasattr(o, 'subCommands'):
- for (cmd, short, parser, doc) in o.subCommands:
- try:
- o.parseOptions([cmd, "", "--_shell-completion",
- "zsh:3"])
- except ImportError as e:
- # this can happen for commands which don't have all
- # the necessary dependencies installed. skip test.
- raise unittest.SkipTest("ImportError calling parseOptions() "
- "on subcommand: %s", (e,))
- except SystemExit:
- pass # expected
- else:
- self.fail('SystemExit not raised')
- outputFile.seek(0)
- # test that we got some output
- self.assertEqual(1, len(outputFile.read(1)))
- outputFile.seek(0)
- outputFile.truncate()
- # flushed because we don't want DeprecationWarnings to be printed when
- # running these test cases.
- self.flushWarnings()
- class ZshTests(unittest.TestCase):
- """
- Tests for zsh completion code
- """
- def test_accumulateMetadata(self):
- """
- Are `compData' attributes you can place on Options classes
- picked up correctly?
- """
- opts = FighterAceExtendedOptions()
- ag = _shellcomp.ZshArgumentsGenerator(opts, 'ace', 'dummy_value')
- descriptions = FighterAceOptions.compData.descriptions.copy()
- descriptions.update(FighterAceExtendedOptions.compData.descriptions)
- self.assertEqual(ag.descriptions, descriptions)
- self.assertEqual(ag.multiUse,
- set(FighterAceOptions.compData.multiUse))
- self.assertEqual(ag.mutuallyExclusive,
- FighterAceOptions.compData.mutuallyExclusive)
- optActions = FighterAceOptions.compData.optActions.copy()
- optActions.update(FighterAceExtendedOptions.compData.optActions)
- self.assertEqual(ag.optActions, optActions)
- self.assertEqual(ag.extraActions,
- FighterAceOptions.compData.extraActions)
- def test_mutuallyExclusiveCornerCase(self):
- """
- Exercise a corner-case of ZshArgumentsGenerator.makeExcludesDict()
- where the long option name already exists in the `excludes` dict being
- built.
- """
- class OddFighterAceOptions(FighterAceExtendedOptions):
- # since "fokker", etc, are already defined as mutually-
- # exclusive on the super-class, defining them again here forces
- # the corner-case to be exercised.
- optFlags = [['anatra', None,
- 'Select the Anatra DS as your dogfighter aircraft']]
- compData = Completions(
- mutuallyExclusive=[['anatra', 'fokker', 'albatros',
- 'spad', 'bristol']])
- opts = OddFighterAceOptions()
- ag = _shellcomp.ZshArgumentsGenerator(opts, 'ace', 'dummy_value')
- expected = {
- 'albatros': set(['anatra', 'b', 'bristol', 'f',
- 'fokker', 's', 'spad']),
- 'anatra': set(['a', 'albatros', 'b', 'bristol',
- 'f', 'fokker', 's', 'spad']),
- 'bristol': set(['a', 'albatros', 'anatra', 'f',
- 'fokker', 's', 'spad']),
- 'fokker': set(['a', 'albatros', 'anatra', 'b',
- 'bristol', 's', 'spad']),
- 'spad': set(['a', 'albatros', 'anatra', 'b',
- 'bristol', 'f', 'fokker'])}
- self.assertEqual(ag.excludes, expected)
- def test_accumulateAdditionalOptions(self):
- """
- We pick up options that are only defined by having an
- appropriately named method on your Options class,
- e.g. def opt_foo(self, foo)
- """
- opts = FighterAceExtendedOptions()
- ag = _shellcomp.ZshArgumentsGenerator(opts, 'ace', 'dummy_value')
- self.assertIn('nocrash', ag.flagNameToDefinition)
- self.assertIn('nocrash', ag.allOptionsNameToDefinition)
- self.assertIn('difficulty', ag.paramNameToDefinition)
- self.assertIn('difficulty', ag.allOptionsNameToDefinition)
- def test_verifyZshNames(self):
- """
- Using a parameter/flag name that doesn't exist
- will raise an error
- """
- class TmpOptions(FighterAceExtendedOptions):
- # Note typo of detail
- compData = Completions(optActions={'detaill' : None})
- self.assertRaises(ValueError, _shellcomp.ZshArgumentsGenerator,
- TmpOptions(), 'ace', 'dummy_value')
- class TmpOptions2(FighterAceExtendedOptions):
- # Note that 'foo' and 'bar' are not real option
- # names defined in this class
- compData = Completions(
- mutuallyExclusive=[("foo", "bar")])
- self.assertRaises(ValueError, _shellcomp.ZshArgumentsGenerator,
- TmpOptions2(), 'ace', 'dummy_value')
- def test_zshCode(self):
- """
- Generate a completion function, and test the textual output
- against a known correct output
- """
- outputFile = BytesIO()
- self.patch(usage.Options, '_shellCompFile', outputFile)
- self.patch(sys, 'argv', ["silly", "", "--_shell-completion", "zsh:2"])
- opts = SimpleProgOptions()
- self.assertRaises(SystemExit, opts.parseOptions)
- self.assertEqual(testOutput1, outputFile.getvalue())
- def test_zshCodeWithSubs(self):
- """
- Generate a completion function with subcommands,
- and test the textual output against a known correct output
- """
- outputFile = BytesIO()
- self.patch(usage.Options, '_shellCompFile', outputFile)
- self.patch(sys, 'argv', ["silly2", "", "--_shell-completion", "zsh:2"])
- opts = SimpleProgWithSubcommands()
- self.assertRaises(SystemExit, opts.parseOptions)
- self.assertEqual(testOutput2, outputFile.getvalue())
- def test_incompleteCommandLine(self):
- """
- Completion still happens even if a command-line is given
- that would normally throw UsageError.
- """
- outputFile = BytesIO()
- self.patch(usage.Options, '_shellCompFile', outputFile)
- opts = FighterAceOptions()
- self.assertRaises(SystemExit, opts.parseOptions,
- ["--fokker", "server", "--unknown-option",
- "--unknown-option2",
- "--_shell-completion", "zsh:5"])
- outputFile.seek(0)
- # test that we got some output
- self.assertEqual(1, len(outputFile.read(1)))
- def test_incompleteCommandLine_case2(self):
- """
- Completion still happens even if a command-line is given
- that would normally throw UsageError.
- The existence of --unknown-option prior to the subcommand
- will break subcommand detection... but we complete anyway
- """
- outputFile = BytesIO()
- self.patch(usage.Options, '_shellCompFile', outputFile)
- opts = FighterAceOptions()
- self.assertRaises(SystemExit, opts.parseOptions,
- ["--fokker", "--unknown-option", "server",
- "--list-server", "--_shell-completion", "zsh:5"])
- outputFile.seek(0)
- # test that we got some output
- self.assertEqual(1, len(outputFile.read(1)))
- outputFile.seek(0)
- outputFile.truncate()
- def test_incompleteCommandLine_case3(self):
- """
- Completion still happens even if a command-line is given
- that would normally throw UsageError.
- Break subcommand detection in a different way by providing
- an invalid subcommand name.
- """
- outputFile = BytesIO()
- self.patch(usage.Options, '_shellCompFile', outputFile)
- opts = FighterAceOptions()
- self.assertRaises(SystemExit, opts.parseOptions,
- ["--fokker", "unknown-subcommand",
- "--list-server", "--_shell-completion", "zsh:4"])
- outputFile.seek(0)
- # test that we got some output
- self.assertEqual(1, len(outputFile.read(1)))
- def test_skipSubcommandList(self):
- """
- Ensure the optimization which skips building the subcommand list
- under certain conditions isn't broken.
- """
- outputFile = BytesIO()
- self.patch(usage.Options, '_shellCompFile', outputFile)
- opts = FighterAceOptions()
- self.assertRaises(SystemExit, opts.parseOptions,
- ["--alba", "--_shell-completion", "zsh:2"])
- outputFile.seek(0)
- # test that we got some output
- self.assertEqual(1, len(outputFile.read(1)))
- def test_poorlyDescribedOptMethod(self):
- """
- Test corner case fetching an option description from a method docstring
- """
- opts = FighterAceOptions()
- argGen = _shellcomp.ZshArgumentsGenerator(opts, 'ace', None)
- descr = argGen.getDescription('silly')
- # docstring for opt_silly is useless so it should just use the
- # option name as the description
- self.assertEqual(descr, 'silly')
- def test_brokenActions(self):
- """
- A C{Completer} with repeat=True may only be used as the
- last item in the extraActions list.
- """
- class BrokenActions(usage.Options):
- compData = usage.Completions(
- extraActions=[usage.Completer(repeat=True),
- usage.Completer()]
- )
- outputFile = BytesIO()
- opts = BrokenActions()
- self.patch(opts, '_shellCompFile', outputFile)
- self.assertRaises(ValueError, opts.parseOptions,
- ["", "--_shell-completion", "zsh:2"])
- def test_optMethodsDontOverride(self):
- """
- opt_* methods on Options classes should not override the
- data provided in optFlags or optParameters.
- """
- class Options(usage.Options):
- optFlags = [['flag', 'f', 'A flag']]
- optParameters = [['param', 'p', None, 'A param']]
- def opt_flag(self):
- """ junk description """
- def opt_param(self, param):
- """ junk description """
- opts = Options()
- argGen = _shellcomp.ZshArgumentsGenerator(opts, 'ace', None)
- self.assertEqual(argGen.getDescription('flag'), 'A flag')
- self.assertEqual(argGen.getDescription('param'), 'A param')
- class EscapeTests(unittest.TestCase):
- def test_escape(self):
- """
- Verify _shellcomp.escape() function
- """
- esc = _shellcomp.escape
- test = "$"
- self.assertEqual(esc(test), "'$'")
- test = 'A--\'$"\\`--B'
- self.assertEqual(esc(test), '"A--\'\\$\\"\\\\\\`--B"')
- class CompleterNotImplementedTests(unittest.TestCase):
- """
- Test that using an unknown shell constant with SubcommandAction
- raises NotImplementedError
- The other Completer() subclasses are tested in test_usage.py
- """
- def test_unknownShell(self):
- """
- Using an unknown shellType should raise NotImplementedError
- """
- action = _shellcomp.SubcommandAction()
- self.assertRaises(NotImplementedError, action._shellCode,
- None, "bad_shell_type")
- class FighterAceServerOptions(usage.Options):
- """
- Options for FighterAce 'server' subcommand
- """
- optFlags = [['list-server', None,
- 'List this server with the online FighterAce network']]
- optParameters = [['packets-per-second', None,
- 'Number of update packets to send per second', '20']]
- class FighterAceOptions(usage.Options):
- """
- Command-line options for an imaginary `Fighter Ace` game
- """
- optFlags = [['fokker', 'f',
- 'Select the Fokker Dr.I as your dogfighter aircraft'],
- ['albatros', 'a',
- 'Select the Albatros D-III as your dogfighter aircraft'],
- ['spad', 's',
- 'Select the SPAD S.VII as your dogfighter aircraft'],
- ['bristol', 'b',
- 'Select the Bristol Scout as your dogfighter aircraft'],
- ['physics', 'p',
- 'Enable secret Twisted physics engine'],
- ['jam', 'j',
- 'Enable a small chance that your machine guns will jam!'],
- ['verbose', 'v',
- 'Verbose logging (may be specified more than once)'],
- ]
- optParameters = [['pilot-name', None, "What's your name, Ace?",
- 'Manfred von Richthofen'],
- ['detail', 'd',
- 'Select the level of rendering detail (1-5)', '3'],
- ]
- subCommands = [['server', None, FighterAceServerOptions,
- 'Start FighterAce game-server.'],
- ]
- compData = Completions(
- descriptions={'physics' : 'Twisted-Physics',
- 'detail' : 'Rendering detail level'},
- multiUse=['verbose'],
- mutuallyExclusive=[['fokker', 'albatros', 'spad',
- 'bristol']],
- optActions={'detail' : CompleteList(['1' '2' '3'
- '4' '5'])},
- extraActions=[CompleteFiles(descr='saved game file to load')]
- )
-
- def opt_silly(self):
- # A silly option which nobody can explain
- """ """
- class FighterAceExtendedOptions(FighterAceOptions):
- """
- Extend the options and zsh metadata provided by FighterAceOptions.
- _shellcomp must accumulate options and metadata from all classes in the
- hiearchy so this is important to test.
- """
- optFlags = [['no-stalls', None,
- 'Turn off the ability to stall your aircraft']]
- optParameters = [['reality-level', None,
- 'Select the level of physics reality (1-5)', '5']]
- compData = Completions(
- descriptions={'no-stalls' : 'Can\'t stall your plane'},
- optActions={'reality-level' :
- Completer(descr='Physics reality level')}
- )
- def opt_nocrash(self):
- """
- Select that you can't crash your plane
- """
- def opt_difficulty(self, difficulty):
- """
- How tough are you? (1-10)
- """
- def _accuracyAction():
- # add tick marks just to exercise quoting
- return CompleteList(['1', '2', '3'], descr='Accuracy\'`?')
- class SimpleProgOptions(usage.Options):
- """
- Command-line options for a `Silly` imaginary program
- """
- optFlags = [['color', 'c', 'Turn on color output'],
- ['gray', 'g', 'Turn on gray-scale output'],
- ['verbose', 'v',
- 'Verbose logging (may be specified more than once)'],
- ]
- optParameters = [['optimization', None, '5',
- 'Select the level of optimization (1-5)'],
- ['accuracy', 'a', '3',
- 'Select the level of accuracy (1-3)'],
- ]
- compData = Completions(
- descriptions={'color' : 'Color on',
- 'optimization' : 'Optimization level'},
- multiUse=['verbose'],
- mutuallyExclusive=[['color', 'gray']],
- optActions={'optimization' : CompleteList(['1', '2', '3', '4', '5'],
- descr='Optimization?'),
- 'accuracy' : _accuracyAction},
- extraActions=[CompleteFiles(descr='output file')]
- )
- def opt_X(self):
- """
- usage.Options does not recognize single-letter opt_ methods
- """
- class SimpleProgSub1(usage.Options):
- optFlags = [['sub-opt', 's', 'Sub Opt One']]
- class SimpleProgSub2(usage.Options):
- optFlags = [['sub-opt', 's', 'Sub Opt Two']]
- class SimpleProgWithSubcommands(SimpleProgOptions):
- optFlags = [['some-option'],
- ['other-option', 'o']]
- optParameters = [['some-param'],
- ['other-param', 'p'],
- ['another-param', 'P', 'Yet Another Param']]
- subCommands = [ ['sub1', None, SimpleProgSub1, 'Sub Command 1'],
- ['sub2', None, SimpleProgSub2, 'Sub Command 2']]
- testOutput1 = b"""#compdef silly
- _arguments -s -A "-*" \\
- ':output file (*):_files -g "*"' \\
- "(--accuracy)-a[Select the level of accuracy (1-3)]:Accuracy'\`?:(1 2 3)" \\
- "(-a)--accuracy=[Select the level of accuracy (1-3)]:Accuracy'\`?:(1 2 3)" \\
- '(--color --gray -g)-c[Color on]' \\
- '(--gray -c -g)--color[Color on]' \\
- '(--color --gray -c)-g[Turn on gray-scale output]' \\
- '(--color -c -g)--gray[Turn on gray-scale output]' \\
- '--help[Display this help and exit.]' \\
- '--optimization=[Optimization level]:Optimization?:(1 2 3 4 5)' \\
- '*-v[Verbose logging (may be specified more than once)]' \\
- '*--verbose[Verbose logging (may be specified more than once)]' \\
- '--version[Display Twisted version and exit.]' \\
- && return 0
- """
- # with sub-commands
- testOutput2 = b"""#compdef silly2
- _arguments -s -A "-*" \\
- '*::subcmd:->subcmd' \\
- ':output file (*):_files -g "*"' \\
- "(--accuracy)-a[Select the level of accuracy (1-3)]:Accuracy'\`?:(1 2 3)" \\
- "(-a)--accuracy=[Select the level of accuracy (1-3)]:Accuracy'\`?:(1 2 3)" \\
- '(--another-param)-P[another-param]:another-param:_files' \\
- '(-P)--another-param=[another-param]:another-param:_files' \\
- '(--color --gray -g)-c[Color on]' \\
- '(--gray -c -g)--color[Color on]' \\
- '(--color --gray -c)-g[Turn on gray-scale output]' \\
- '(--color -c -g)--gray[Turn on gray-scale output]' \\
- '--help[Display this help and exit.]' \\
- '--optimization=[Optimization level]:Optimization?:(1 2 3 4 5)' \\
- '(--other-option)-o[other-option]' \\
- '(-o)--other-option[other-option]' \\
- '(--other-param)-p[other-param]:other-param:_files' \\
- '(-p)--other-param=[other-param]:other-param:_files' \\
- '--some-option[some-option]' \\
- '--some-param=[some-param]:some-param:_files' \\
- '*-v[Verbose logging (may be specified more than once)]' \\
- '*--verbose[Verbose logging (may be specified more than once)]' \\
- '--version[Display Twisted version and exit.]' \\
- && return 0
- local _zsh_subcmds_array
- _zsh_subcmds_array=(
- "sub1:Sub Command 1"
- "sub2:Sub Command 2"
- )
- _describe "sub-command" _zsh_subcmds_array
- """
|