Viewing file: test_shellcomp.py (20.67 KB) -rw-r--r-- Select action/file-type: (+) | (+) | (+) | Code (+) | Session (+) | (+) | SDB (+) | (+) | (+) | (+) | (+) | (+) |
# Copyright (c) Twisted Matrix Laboratories. # See LICENSE for details.
""" Test cases for twisted.python._shellcomp """
import sys from io import BytesIO from typing import List, Optional
from twisted.python import _shellcomp, reflect, usage from twisted.python.usage import CompleteFiles, CompleteList, Completer, Completions from twisted.trial import unittest
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(metaclass=ZshScriptTestMeta): """ 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. """
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", BytesIO())
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", BytesIO())
expected = { "albatros": {"anatra", "b", "bristol", "f", "fokker", "s", "spad"}, "anatra": {"a", "albatros", "b", "bristol", "f", "fokker", "s", "spad"}, "bristol": {"a", "albatros", "anatra", "f", "fokker", "s", "spad"}, "fokker": {"a", "albatros", "anatra", "b", "bristol", "s", "spad"}, "spad": {"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", BytesIO())
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", BytesIO() )
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", BytesIO(), )
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: List[List[Optional[str]]] = [ ["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: List[List[Optional[str]]] = [ ["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 """
|