Initial import to Tizen
[profile/ivi/python-twisted.git] / twisted / python / test / test_shellcomp.py
1 # Copyright (c) Twisted Matrix Laboratories.
2 # See LICENSE for details.
3
4 """
5 Test cases for twisted.python._shellcomp
6 """
7
8 import sys
9 from cStringIO import StringIO
10
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
15
16
17
18 class ZshScriptTestMeta(type):
19     """
20     Metaclass of ZshScriptTestMixin.
21     """
22     def __new__(cls, name, bases, attrs):
23         def makeTest(cmdName, optionsFQPN):
24             def runTest(self):
25                 return test_genZshFunction(self, cmdName, optionsFQPN)
26             return runTest
27
28         # add test_ methods to the class for each script
29         # we are testing.
30         if 'generateFor' in attrs:
31             for cmdName, optionsFQPN in attrs['generateFor']:
32                 test = makeTest(cmdName, optionsFQPN)
33                 attrs['test_genZshFunction_' + cmdName] = test
34
35         return type.__new__(cls, name, bases, attrs)
36
37
38
39 class ZshScriptTestMixin(object):
40     """
41     Integration test helper to show that C{usage.Options} classes can have zsh
42     completion functions generated for them without raising errors.
43
44     In your subclasses set a class variable like so:
45
46     #            | cmd name | Fully Qualified Python Name of Options class |
47     #
48     generateFor = [('conch',  'twisted.conch.scripts.conch.ClientOptions'),
49                    ('twistd', 'twisted.scripts.twistd.ServerOptions'),
50                    ]
51
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.
55     """
56     __metaclass__ = ZshScriptTestMeta
57
58
59
60 def test_genZshFunction(self, cmdName, optionsFQPN):
61     """
62     Generate completion functions for given twisted command - no errors
63     should be raised
64
65     @type cmdName: C{str}
66     @param cmdName: The name of the command-line utility e.g. 'twistd'
67
68     @type optionsFQPN: C{str}
69     @param optionsFQPN: The Fully Qualified Python Name of the C{Options}
70         class to be tested.
71     """
72     outputFile = StringIO()
73     self.patch(usage.Options, '_shellCompFile', outputFile)
74
75     # some scripts won't import or instantiate because of missing
76     # dependencies (PyCrypto, etc) so we have to skip them.
77     try:
78         o = reflect.namedAny(optionsFQPN)()
79     except Exception, e:
80         raise unittest.SkipTest("Couldn't import or instantiate "
81                                 "Options class: %s" % (e,))
82
83     try:
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.
88         # skip
89         raise unittest.SkipTest("ImportError calling parseOptions(): %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
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:
103             try:
104                 o.parseOptions([cmd, "", "--_shell-completion",
105                                 "zsh:3"])
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,))
111             except SystemExit:
112                 pass # expected
113             else:
114                 self.fail('SystemExit not raised')
115
116             outputFile.seek(0)
117             # test that we got some output
118             self.assertEqual(1, len(outputFile.read(1)))
119             outputFile.seek(0)
120             outputFile.truncate()
121
122     # flushed because we don't want DeprecationWarnings to be printed when
123     # running these test cases.
124     self.flushWarnings()
125
126
127
128 class ZshTestCase(unittest.TestCase):
129     """
130     Tests for zsh completion code
131     """
132     def test_accumulateMetadata(self):
133         """
134         Are `compData' attributes you can place on Options classes
135         picked up correctly?
136         """
137         opts = FighterAceExtendedOptions()
138         ag = _shellcomp.ZshArgumentsGenerator(opts, 'ace', 'dummy_value')
139
140         descriptions = FighterAceOptions.compData.descriptions.copy()
141         descriptions.update(FighterAceExtendedOptions.compData.descriptions)
142
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)
148
149         optActions = FighterAceOptions.compData.optActions.copy()
150         optActions.update(FighterAceExtendedOptions.compData.optActions)
151         self.assertEqual(ag.optActions, optActions)
152
153         self.assertEqual(ag.extraActions,
154                           FighterAceOptions.compData.extraActions)
155
156
157     def test_mutuallyExclusiveCornerCase(self):
158         """
159         Exercise a corner-case of ZshArgumentsGenerator.makeExcludesDict()
160         where the long option name already exists in the `excludes` dict being
161         built.
162         """
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',
171                                                 'spad', 'bristol']])
172
173         opts = OddFighterAceOptions()
174         ag = _shellcomp.ZshArgumentsGenerator(opts, 'ace', 'dummy_value')
175
176         expected = {
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'])}
187
188         self.assertEqual(ag.excludes, expected)
189
190
191     def test_accumulateAdditionalOptions(self):
192         """
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)
196         """
197         opts = FighterAceExtendedOptions()
198         ag = _shellcomp.ZshArgumentsGenerator(opts, 'ace', 'dummy_value')
199
200         self.assertIn('nocrash', ag.flagNameToDefinition)
201         self.assertIn('nocrash', ag.allOptionsNameToDefinition)
202
203         self.assertIn('difficulty', ag.paramNameToDefinition)
204         self.assertIn('difficulty', ag.allOptionsNameToDefinition)
205
206
207     def test_verifyZshNames(self):
208         """
209         Using a parameter/flag name that doesn't exist
210         will raise an error
211         """
212         class TmpOptions(FighterAceExtendedOptions):
213             # Note typo of detail
214             compData = Completions(optActions={'detaill' : None})
215
216         self.assertRaises(ValueError, _shellcomp.ZshArgumentsGenerator,
217                           TmpOptions(), 'ace', 'dummy_value')
218
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")])
224
225         self.assertRaises(ValueError, _shellcomp.ZshArgumentsGenerator,
226                           TmpOptions2(), 'ace', 'dummy_value')
227
228
229     def test_zshCode(self):
230         """
231         Generate a completion function, and test the textual output
232         against a known correct output
233         """
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())
240
241
242     def test_zshCodeWithSubs(self):
243         """
244         Generate a completion function with subcommands,
245         and test the textual output against a known correct output
246         """
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())
253
254
255     def test_incompleteCommandLine(self):
256         """
257         Completion still happens even if a command-line is given
258         that would normally throw UsageError.
259         """
260         outputFile = StringIO()
261         self.patch(usage.Options, '_shellCompFile', outputFile)
262         opts = FighterAceOptions()
263
264         self.assertRaises(SystemExit, opts.parseOptions,
265                           ["--fokker", "server", "--unknown-option",
266                            "--unknown-option2",
267                            "--_shell-completion", "zsh:5"])
268         outputFile.seek(0)
269         # test that we got some output
270         self.assertEqual(1, len(outputFile.read(1)))
271
272
273     def test_incompleteCommandLine_case2(self):
274         """
275         Completion still happens even if a command-line is given
276         that would normally throw UsageError.
277
278         The existance of --unknown-option prior to the subcommand
279         will break subcommand detection... but we complete anyway
280         """
281         outputFile = StringIO()
282         self.patch(usage.Options, '_shellCompFile', outputFile)
283         opts = FighterAceOptions()
284
285         self.assertRaises(SystemExit, opts.parseOptions,
286                           ["--fokker", "--unknown-option", "server",
287                            "--list-server", "--_shell-completion", "zsh:5"])
288         outputFile.seek(0)
289         # test that we got some output
290         self.assertEqual(1, len(outputFile.read(1)))
291
292         outputFile.seek(0)
293         outputFile.truncate()
294
295
296     def test_incompleteCommandLine_case3(self):
297         """
298         Completion still happens even if a command-line is given
299         that would normally throw UsageError.
300
301         Break subcommand detection in a different way by providing
302         an invalid subcommand name.
303         """
304         outputFile = StringIO()
305         self.patch(usage.Options, '_shellCompFile', outputFile)
306         opts = FighterAceOptions()
307
308         self.assertRaises(SystemExit, opts.parseOptions,
309                           ["--fokker", "unknown-subcommand",
310                            "--list-server", "--_shell-completion", "zsh:4"])
311         outputFile.seek(0)
312         # test that we got some output
313         self.assertEqual(1, len(outputFile.read(1)))
314
315
316     def test_skipSubcommandList(self):
317         """
318         Ensure the optimization which skips building the subcommand list
319         under certain conditions isn't broken.
320         """
321         outputFile = StringIO()
322         self.patch(usage.Options, '_shellCompFile', outputFile)
323         opts = FighterAceOptions()
324
325         self.assertRaises(SystemExit, opts.parseOptions,
326                           ["--alba", "--_shell-completion", "zsh:2"])
327         outputFile.seek(0)
328         # test that we got some output
329         self.assertEqual(1, len(outputFile.read(1)))
330
331
332     def test_poorlyDescribedOptMethod(self):
333         """
334         Test corner case fetching an option description from a method docstring
335         """
336         opts = FighterAceOptions()
337         argGen = _shellcomp.ZshArgumentsGenerator(opts, 'ace', None)
338
339         descr = argGen.getDescription('silly')
340
341         # docstring for opt_silly is useless so it should just use the
342         # option name as the description
343         self.assertEqual(descr, 'silly')
344
345
346     def test_brokenActions(self):
347         """
348         A C{Completer} with repeat=True may only be used as the
349         last item in the extraActions list.
350         """
351         class BrokenActions(usage.Options):
352             compData = usage.Completions(
353                 extraActions=[usage.Completer(repeat=True),
354                               usage.Completer()]
355                 )
356
357         outputFile = StringIO()
358         opts = BrokenActions()
359         self.patch(opts, '_shellCompFile', outputFile)
360         self.assertRaises(ValueError, opts.parseOptions,
361                           ["", "--_shell-completion", "zsh:2"])
362
363
364     def test_optMethodsDontOverride(self):
365         """
366         opt_* methods on Options classes should not override the
367         data provided in optFlags or optParameters.
368         """
369         class Options(usage.Options):
370             optFlags = [['flag', 'f', 'A flag']]
371             optParameters = [['param', 'p', None, 'A param']]
372
373             def opt_flag(self):
374                 """ junk description """
375
376             def opt_param(self, param):
377                 """ junk description """
378
379         opts = Options()
380         argGen = _shellcomp.ZshArgumentsGenerator(opts, 'ace', None)
381
382         self.assertEqual(argGen.getDescription('flag'), 'A flag')
383         self.assertEqual(argGen.getDescription('param'), 'A param')
384
385
386
387 class EscapeTestCase(unittest.TestCase):
388     def test_escape(self):
389         """
390         Verify _shellcomp.escape() function
391         """
392         esc = _shellcomp.escape
393
394         test = "$"
395         self.assertEqual(esc(test), "'$'")
396
397         test = 'A--\'$"\\`--B'
398         self.assertEqual(esc(test), '"A--\'\\$\\"\\\\\\`--B"')
399
400
401
402 class CompleterNotImplementedTestCase(unittest.TestCase):
403     """
404     Test that using an unknown shell constant with SubcommandAction
405     raises NotImplementedError
406
407     The other Completer() subclasses are tested in test_usage.py
408     """
409     def test_unknownShell(self):
410         """
411         Using an unknown shellType should raise NotImplementedError
412         """
413         action = _shellcomp.SubcommandAction()
414
415         self.assertRaises(NotImplementedError, action._shellCode,
416                           None, "bad_shell_type")
417
418
419
420 class FighterAceServerOptions(usage.Options):
421     """
422     Options for FighterAce 'server' subcommand
423     """
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']]
428
429
430
431 class FighterAceOptions(usage.Options):
432     """
433     Command-line options for an imaginary `Fighter Ace` game
434     """
435     optFlags = [['fokker', 'f',
436                  'Select the Fokker Dr.I as your dogfighter aircraft'],
437                 ['albatros', 'a',
438                  'Select the Albatros D-III as your dogfighter aircraft'],
439                 ['spad', 's',
440                  'Select the SPAD S.VII as your dogfighter aircraft'],
441                 ['bristol', 'b',
442                  'Select the Bristol Scout as your dogfighter aircraft'],
443                 ['physics', 'p',
444                  'Enable secret Twisted physics engine'],
445                 ['jam', 'j',
446                  'Enable a small chance that your machine guns will jam!'],
447                 ['verbose', 'v',
448                  'Verbose logging (may be specified more than once)'],
449                 ]
450
451     optParameters = [['pilot-name', None, "What's your name, Ace?",
452                       'Manfred von Richthofen'],
453                      ['detail', 'd',
454                       'Select the level of rendering detail (1-5)', '3'],
455             ]
456
457     subCommands = [['server', None, FighterAceServerOptions,
458                     'Start FighterAce game-server.'],
459                    ]
460
461     compData = Completions(
462         descriptions={'physics' : 'Twisted-Physics',
463                       'detail' : 'Rendering detail level'},
464         multiUse=['verbose'],
465         mutuallyExclusive=[['fokker', 'albatros', 'spad',
466                             'bristol']],
467         optActions={'detail' : CompleteList(['1' '2' '3'
468                                          '4' '5'])},
469         extraActions=[CompleteFiles(descr='saved game file to load')]
470         )
471                               
472     def opt_silly(self):
473         # A silly option which nobody can explain
474         """ """
475
476
477
478 class FighterAceExtendedOptions(FighterAceOptions):
479     """
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.
483     """
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']]
488
489     compData = Completions(
490         descriptions={'no-stalls' : 'Can\'t stall your plane'},
491         optActions={'reality-level' :
492                         Completer(descr='Physics reality level')}
493         )
494
495     def opt_nocrash(self):
496         """
497         Select that you can't crash your plane
498         """
499
500
501     def opt_difficulty(self, difficulty):
502         """
503         How tough are you? (1-10)
504         """
505
506
507
508 def _accuracyAction():
509                                     # add tick marks just to exercise quoting
510     return CompleteList(['1', '2', '3'], descr='Accuracy\'`?')
511
512
513
514 class SimpleProgOptions(usage.Options):
515     """
516     Command-line options for a `Silly` imaginary program
517     """
518     optFlags = [['color', 'c', 'Turn on color output'],
519                 ['gray', 'g', 'Turn on gray-scale output'],
520                 ['verbose', 'v',
521                  'Verbose logging (may be specified more than once)'],
522                 ]
523
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)'],
528                      ]
529
530
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')]
540         )
541
542     def opt_X(self):
543         """
544         usage.Options does not recognize single-letter opt_ methods
545         """
546
547
548
549 class SimpleProgSub1(usage.Options):
550     optFlags = [['sub-opt', 's', 'Sub Opt One']]
551
552
553
554 class SimpleProgSub2(usage.Options):
555     optFlags = [['sub-opt', 's', 'Sub Opt Two']]
556
557
558
559 class SimpleProgWithSubcommands(SimpleProgOptions):
560     optFlags = [['some-option'],
561                 ['other-option', 'o']]
562
563     optParameters = [['some-param'],
564                      ['other-param', 'p'],
565                      ['another-param', 'P', 'Yet Another Param']]
566
567     subCommands = [ ['sub1', None, SimpleProgSub1, 'Sub Command 1'],
568                     ['sub2', None, SimpleProgSub2, 'Sub Command 2']]
569
570
571
572 testOutput1 = """#compdef silly
573
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.]' \\
587 && return 0
588 """
589
590 # with sub-commands
591 testOutput2 = """#compdef silly2
592
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.]' \\
615 && return 0
616 local _zsh_subcmds_array
617 _zsh_subcmds_array=(
618 "sub1:Sub Command 1"
619 "sub2:Sub Command 2"
620 )
621
622 _describe "sub-command" _zsh_subcmds_array
623 """