Test ArgumentParseManager.parse
[kamaki] / kamaki / cli / argument / test.py
1 # Copyright 2013 GRNET S.A. All rights reserved.
2 #
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
5 # conditions are met:
6 #
7 #   1. Redistributions of source code must retain the above
8 #      copyright notice, this list of conditions and the following
9 #      disclaimer.
10 #
11 #   2. Redistributions in binary form must reproduce the above
12 #      copyright notice, this list of conditions and the following
13 #      disclaimer in the documentation and/or other materials
14 #      provided with the distribution.
15 #
16 # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23 # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24 # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 # POSSIBILITY OF SUCH DAMAGE.
28 #
29 # The views and conclusions contained in the software and
30 # documentation are those of the authors and should not be
31 # interpreted as representing official policies, either expressed
32 # or implied, of GRNET S.A.
33
34 from mock import patch, call, MagicMock
35 from unittest import TestCase
36 from StringIO import StringIO
37 from datetime import datetime
38 #from itertools import product
39
40 from kamaki.cli import argument, errors
41 from kamaki.cli.config import Config
42
43
44 def assert_dicts_are_equal(test_case, d1, d2):
45     for k, v in d1.items():
46         test_case.assertTrue(k in d2)
47         if isinstance(v, dict):
48             test_case.assert_dicts_are_equal(v, d2[k])
49         else:
50             test_case.assertEqual(unicode(v), unicode(d2[k]))
51
52
53 cnf_path = 'kamaki.cli.config.Config'
54 arg_path = 'kamaki.cli.argument'
55
56
57 class Argument(TestCase):
58
59     def test___init__(self):
60         self.assertRaises(ValueError, argument.Argument, 'non-integer')
61         self.assertRaises(AssertionError, argument.Argument, 1)
62         self.assertRaises(AssertionError, argument.Argument, 0, 'noname')
63         self.assertRaises(AssertionError, argument.Argument, 0, '--no name')
64         self.assertRaises(AssertionError, argument.Argument, 0, ['-n', 'n m'])
65         for arity, help, parsed_name, default in (
66                 (0, 'help 0', '--zero', None),
67                 (1, 'help 1', ['--one', '-o'], 'lala'),
68                 (-1, 'help -1', ['--help', '--or', '--more'], 0),
69                 (0, 'help 0 again', ['--again', ], True)):
70             a = argument.Argument(arity, help, parsed_name, default)
71             if arity:
72                 self.assertEqual(arity, a.arity)
73             self.assertEqual(help, a.help)
74
75             exp_name = parsed_name if (
76                 isinstance(parsed_name, list)) else [parsed_name, ]
77             self.assertEqual(exp_name, a.parsed_name)
78
79             exp_default = default if (default or arity) else False
80             self.assertEqual(exp_default, a.default)
81
82     def test_value(self):
83         a = argument.Argument(1, parsed_name='--value')
84         for value in (None, '', 0, 0.1, -12, [1, 'a', 2.8], (3, 'lala'), 'pi'):
85             a.value = value
86             self.assertEqual(value, a.value)
87
88     def test_update_parser(self):
89         for i, arity in enumerate((-1, 0, 1)):
90             arp = argument.ArgumentParser()
91             pname, aname = '--pname%s' % i, 'a_name_%s' % i
92             a = argument.Argument(arity, 'args', pname, 42)
93             a.update_parser(arp, aname)
94
95             f = StringIO()
96             arp.print_usage(file=f), f.seek(0)
97             usage, exp = f.readline(), '[%s%s]\n' % (
98                 pname, (' %s' % aname.upper()) if arity else '')
99             self.assertEqual(usage[-len(exp):], exp)
100             del arp
101
102
103 class ConfigArgument(TestCase):
104
105     def setUp(self):
106         argument._config_arg = argument.ConfigArgument('Recovered Path')
107
108     def test_value(self):
109         c = argument._config_arg
110         self.assertEqual(c.value, None)
111         exp = '/some/random/path'
112         c.value = exp
113         self.assertTrue(isinstance(c.value, Config))
114         self.assertEqual(c.file_path, exp)
115         self.assertEqual(c.value.path, exp)
116
117     def test_get(self):
118         c = argument._config_arg
119         c.value = None
120         with patch('%s.get' % cnf_path, return_value='config') as get:
121             self.assertEqual(c.value.get('global', 'config_cli'), 'config')
122             self.assertEqual(get.mock_calls[-1], call('global', 'config_cli'))
123
124     @patch('%s.keys' % cnf_path, return_value=(
125         'image_cli', 'config_cli', 'history_cli', 'file'))
126     def test_groups(self, keys):
127         c = argument._config_arg
128         c.value = None
129         cset = set(c.groups)
130         self.assertTrue(cset.issuperset(['image', 'config', 'history']))
131         self.assertEqual(keys.mock_calls[-1], call('global'))
132         self.assertFalse('file' in cset)
133         self.assertEqual(keys.mock_calls[-1], call('global'))
134
135     @patch('%s.items' % cnf_path, return_value=(
136         ('image_cli', 'image'), ('file', 'pithos'),
137         ('config_cli', 'config'), ('history_cli', 'history')))
138     def test_cli_specs(self, items):
139         c = argument._config_arg
140         c.value = None
141         cset = set(c.cli_specs)
142         self.assertTrue(cset.issuperset([
143             ('image', 'image'), ('config', 'config'), ('history', 'history')]))
144         self.assertEqual(items.mock_calls[-1], call('global'))
145         self.assertFalse(cset.issuperset([('file', 'pithos'), ]))
146         self.assertEqual(items.mock_calls[-1], call('global'))
147
148     def test_get_global(self):
149         c = argument._config_arg
150         c.value = None
151         for k, v in (
152                 ('config_cli', 'config'),
153                 ('image_cli', 'image'),
154                 ('history_cli', 'history')):
155             with patch('%s.get_global' % cnf_path, return_value=v) as gg:
156                 self.assertEqual(c.get_global(k), v)
157                 self.assertEqual(gg.mock_calls[-1], call(k))
158
159     def test_get_cloud(self):
160         c = argument._config_arg
161         c.value = None
162         with patch(
163                 '%s.get_cloud' % cnf_path,
164                 return_value='http://cloud') as get_cloud:
165             self.assertTrue(len(c.get_cloud('mycloud', 'url')) > 0)
166             self.assertEqual(get_cloud.mock_calls[-1],  call('mycloud', 'url'))
167         with patch(
168                 '%s.get_cloud' % cnf_path,
169                 side_effect=KeyError('no token')) as get_cloud:
170             self.assertRaises(KeyError, c.get_cloud, 'mycloud', 'token')
171         invalidcloud = 'PLEASE_DO_NOT_EVER_NAME_YOUR_CLOUD_LIKE_THIS111'
172         self.assertRaises(KeyError, c.get_cloud, invalidcloud, 'url')
173
174
175 class RuntimeConfigArgument(TestCase):
176
177     def setUp(self):
178         argument._config_arg = argument.ConfigArgument('Recovered Path')
179
180     @patch('%s.Argument.__init__' % arg_path)
181     def test___init__(self, arg):
182         config, help, pname, default = 'config', 'help', 'pname', 'default'
183         rca = argument.RuntimeConfigArgument(config, help, pname, default)
184         self.assertTrue(isinstance(rca, argument.RuntimeConfigArgument))
185         self.assertEqual(rca._config_arg, config)
186         self.assertEqual(arg.mock_calls[-1], call(1, help, pname, default))
187
188     @patch('%s.override' % cnf_path)
189     def test_value(self, override):
190         config, help, pname, default = argument._config_arg, 'help', '-n', 'df'
191         config.value = None
192         rca = argument.RuntimeConfigArgument(config, help, pname, default)
193         self.assertEqual(rca.value, default)
194
195         for options in ('grp', 'grp.opt', 'k v', '=nokey', 2.8, None, 42, ''):
196             self.assertRaises(TypeError, rca.value, options)
197
198         for options in ('key=val', 'grp.key=val', 'dotted.opt.key=val'):
199             rca.value = options
200             option, sep, val = options.partition('=')
201             grp, sep, key = option.partition('.')
202             grp, key = (grp, key) if key else ('global', grp)
203             self.assertEqual(override.mock_calls[-1], call(grp, key, val))
204
205
206 class FlagArgument(TestCase):
207
208     @patch('%s.Argument.__init__' % arg_path)
209     def test___init__(self, arg):
210         help, pname, default = 'help', 'pname', 'default'
211         fa = argument.FlagArgument(help, pname, default)
212         self.assertTrue(isinstance(fa, argument.FlagArgument))
213         arg.assert_called_once(0, help, pname, default)
214
215
216 class ValueArgument(TestCase):
217
218     @patch('%s.Argument.__init__' % arg_path)
219     def test___init__(self, arg):
220         help, pname, default = 'help', 'pname', 'default'
221         fa = argument.ValueArgument(help, pname, default)
222         self.assertTrue(isinstance(fa, argument.ValueArgument))
223         arg.assert_called_once(1, help, pname, default)
224
225
226 class IntArgument(TestCase):
227
228     def test_value(self):
229         ia = argument.IntArgument(parsed_name='--ia')
230         self.assertEqual(ia.value, None)
231         for v in (1, 0, -1, 923455555555555555555555555555555):
232             ia.value = v
233             self.assertEqual(ia.value, v)
234         for v in ('1', '-1', 2.8):
235             ia.value = v
236             self.assertEqual(ia.value, int(v))
237         for v, err in (
238                 ('invalid', errors.CLIError),
239                 (None, TypeError), (False, TypeError), ([1, 2, 3], TypeError)):
240             try:
241                 ia.value = v
242             except Exception as e:
243                 self.assertTrue(isinstance(e, err))
244
245
246 class DateArgument(TestCase):
247
248     def test_timestamp(self):
249         da = argument.DateArgument(parsed_name='--date')
250         self.assertEqual(da.timestamp, None)
251         date, format, exp = '24-10-1917', '%d-%m-%Y', -1646964000.0
252         da._value = argument.dtm.strptime(date, format)
253         self.assertEqual(da.timestamp, exp)
254
255     def test_formated(self):
256         da = argument.DateArgument(parsed_name='--date')
257         self.assertEqual(da.formated, None)
258         date, format, exp = (
259             '24-10-1917', '%d-%m-%Y', 'Wed Oct 24 00:00:00 1917')
260         da._value = argument.dtm.strptime(date, format)
261         self.assertEqual(da.formated, exp)
262
263     @patch('%s.DateArgument.timestamp' % arg_path)
264     @patch('%s.DateArgument.format_date' % arg_path)
265     def test_value(self, format_date, timestamp):
266         da = argument.DateArgument(parsed_name='--date')
267         self.assertTrue(isinstance(da.value, MagicMock))
268         da.value = 'Something'
269         format_date.assert_called_once(call('Something'))
270
271     def test_format_date(self):
272         da = argument.DateArgument(parsed_name='--date')
273         for datestr, exp in (
274                 ('Wed Oct 24 01:02:03 1917', datetime(1917, 10, 24, 1, 2, 3)),
275                 ('24-10-1917', datetime(1917, 10, 24)),
276                 ('01:02:03 24-10-1917', datetime(1917, 10, 24, 1, 2, 3))):
277             self.assertEqual(da.format_date(datestr), exp)
278         for datestr, err in (
279                 ('32-40-20134', errors.CLIError),
280                 ('Wednesday, 24 Oct 2017', errors.CLIError),
281                 (None, TypeError), (0.8, TypeError)):
282             self.assertRaises(err, da.format_date, datestr)
283
284
285 class VersionArgument(TestCase):
286
287     def test_value(self):
288         va = argument.VersionArgument(parsed_name='--version')
289         self.assertTrue(va, argument.VersionArgument)
290         va.value = 'some value'
291         self.assertEqual(va.value, 'some value')
292
293
294 class KeyValueArgument(TestCase):
295
296     @patch('%s.Argument.__init__' % arg_path)
297     def test___init__(self, init):
298         help, pname, default = 'help', 'pname', 'default'
299         kva = argument.KeyValueArgument(help, pname, default)
300         self.assertTrue(isinstance(kva, argument.KeyValueArgument))
301         self.assertEqual(init.mock_calls[-1], call(-1, help, pname, default))
302
303     def test_value(self):
304         kva = argument.KeyValueArgument(parsed_name='--keyval')
305         self.assertEqual(kva.value, {})
306         for kvpairs in (
307                 'strval', 'key=val', 2.8, 42, None,
308                 ('key', 'val'), ('key val'), ['=val', 'key=val'],
309                 ['key1=val1', 'key2 val2'], ('key1 = val1', )):
310             try:
311                 kva.value = kvpairs
312             except Exception as e:
313                 self.assertTrue(isinstance(e, errors.CLIError))
314         for kvpairs, exp in (
315                 (('key=val', ), {'key': 'val'}),
316                 (['key1=val1', 'key2=val2'], {'key1': 'val1', 'key2': 'val2'}),
317                 (
318                     ('k1=v1 v2', 'k3=', 'k 4=v4'),
319                     {'k1': 'v1 v2', 'k3': '', 'k 4': 'v4'}),
320                 (('k=v1', 'k=v2', 'k=v3'), {'k': 'v3'})
321             ):
322             kva.value = kvpairs
323             assert_dicts_are_equal(self, kva.value, exp)
324
325
326 class ProgressBarArgument(TestCase):
327
328     class PseudoBar(object):
329             message = ''
330             suffix = ''
331
332             def start():
333                 pass
334
335     @patch('%s.FlagArgument.__init__' % arg_path)
336     def test___init__(self, init):
337         help, pname, default = 'help', '--progress', 'default'
338         pba = argument.ProgressBarArgument(help, pname, default)
339         self.assertTrue(isinstance(pba, argument.ProgressBarArgument))
340         self.assertEqual(pba.suffix, '%(percent)d%%')
341         init.assert_called_once(help, pname, default)
342
343     def test_clone(self):
344         pba = argument.ProgressBarArgument(parsed_name='--progress')
345         pba.value = None
346         pba_clone = pba.clone()
347         self.assertTrue(isinstance(pba, argument.ProgressBarArgument))
348         self.assertTrue(isinstance(pba_clone, argument.ProgressBarArgument))
349         self.assertNotEqual(pba, pba_clone)
350         self.assertEqual(pba.parsed_name, pba_clone.parsed_name)
351
352     def test_get_generator(self):
353         pba = argument.ProgressBarArgument(parsed_name='--progress')
354         pba.value = None
355         msg, msg_len = 'message', 40
356         with patch('%s.KamakiProgressBar.start' % arg_path) as start:
357             pba.get_generator(msg, msg_len)
358             self.assertTrue(isinstance(pba.bar, argument.KamakiProgressBar))
359             self.assertNotEqual(pba.bar.message, msg)
360             self.assertEqual(
361                 pba.bar.message, '%s%s' % (msg, ' ' * (msg_len - len(msg))))
362             self.assertEqual(pba.bar.suffix, '%(percent)d%% - %(eta)ds')
363             start.assert_called_once()
364
365     def test_finish(self):
366         pba = argument.ProgressBarArgument(parsed_name='--progress')
367         pba.value = None
368         self.assertEqual(pba.finish(), None)
369         pba.bar = argument.KamakiProgressBar()
370         with patch('%s.KamakiProgressBar.finish' % arg_path) as finish:
371             pba.finish()
372             finish.assert_called_once()
373
374
375 class ArgumentParseManager(TestCase):
376
377     @patch('%s.ArgumentParseManager.parse' % arg_path)
378     @patch('%s.ArgumentParseManager.update_parser' % arg_path)
379     def test___init__(self, parse, update_parser):
380         for arguments in (None, {'k1': 'v1', 'k2': 'v2'}):
381             apm = argument.ArgumentParseManager('exe', arguments)
382             self.assertTrue(isinstance(apm, argument.ArgumentParseManager))
383
384             self.assertTrue(isinstance(apm.parser, argument.ArgumentParser))
385             self.assertFalse(apm.parser.add_help)
386             self.assertEqual(
387                 apm.parser.formatter_class,
388                 argument.RawDescriptionHelpFormatter)
389
390             self.assertEqual(
391                 apm.syntax, 'exe <cmd_group> [<cmd_subbroup> ...] <cmd>')
392             assert_dicts_are_equal(
393                 self, apm.arguments,
394                 arguments or argument._arguments)
395             self.assertFalse(apm._parser_modified)
396             self.assertEqual(apm._parsed, None)
397             self.assertEqual(apm._unparsed, None)
398             self.assertEqual(parse.mock_calls[-1], call())
399             if arguments:
400                 update_parser.assert_called_once()
401
402     def test_syntax(self):
403         apm = argument.ArgumentParseManager('exe')
404         self.assertEqual(
405             apm.syntax, 'exe <cmd_group> [<cmd_subbroup> ...] <cmd>')
406         apm.syntax = 'some syntax'
407         self.assertEqual(apm.syntax, 'some syntax')
408
409     @patch('%s.ArgumentParseManager.update_parser' % arg_path)
410     def test_arguments(self, update_parser):
411         apm = argument.ArgumentParseManager('exe')
412         assert_dicts_are_equal(self, apm.arguments, argument._arguments)
413         update_parser.assert_called_once()
414         exp = {'k1': 'v1', 'k2': 'v2'}
415         apm.arguments = exp
416         assert_dicts_are_equal(self, apm.arguments, exp)
417         self.assertEqual(update_parser.mock_calls[-1], call())
418         try:
419             apm.arguments = None
420         except Exception as e:
421             self.assertTrue(isinstance(e, AssertionError))
422
423     @patch('%s.ArgumentParseManager.parse' % arg_path)
424     def test_parsed(self, parse):
425         apm = argument.ArgumentParseManager('exe')
426         self.assertEqual(apm.parsed, None)
427         exp = 'you have been parsed'
428         apm._parsed = exp
429         self.assertEqual(apm.parsed, exp)
430         apm._parser_modified = True
431         apm._parsed = exp + ' v2'
432         self.assertEqual(apm.parsed, exp + ' v2')
433         self.assertEqual(parse.mock_calls, [call(), call()])
434
435     @patch('%s.ArgumentParseManager.parse' % arg_path)
436     def test_unparsed(self, parse):
437         apm = argument.ArgumentParseManager('exe')
438         self.assertEqual(apm.unparsed, None)
439         exp = 'you have been unparsed'
440         apm._unparsed = exp
441         self.assertEqual(apm.unparsed, exp)
442         apm._parser_modified = True
443         apm._unparsed = exp + ' v2'
444         self.assertEqual(apm.unparsed, exp + ' v2')
445         self.assertEqual(parse.mock_calls, [call(), call()])
446
447     @patch('%s.Argument.update_parser' % arg_path
448         )
449     def test_update_parser(self, update_parser):
450         apm = argument.ArgumentParseManager('exe')
451         body_count = len(update_parser.mock_calls)
452         exp = len(argument._arguments)
453         self.assertEqual(body_count, exp)
454         apm.update_parser()
455         exp = len(apm.arguments) + body_count
456         body_count = len(update_parser.mock_calls)
457         self.assertEqual(body_count, exp)
458         expd = dict(
459             k1=argument.Argument(0, parsed_name='-a'),
460             k2=argument.Argument(0, parsed_name='-b'))
461         apm.update_parser(expd)
462         body_count = len(update_parser.mock_calls)
463         self.assertEqual(body_count, exp + 2)
464
465     def test_update_arguments(self):
466         (inp, cor, exp) = (
467             {'k1': 'v1', 'k2': 'v3'}, {'k2': 'v2'}, {'k1': 'v1', 'k2': 'v2'})
468         apm = argument.ArgumentParseManager('exe')
469         with patch(
470                 '%s.ArgumentParseManager.update_parser' % arg_path) as UP:
471             apm.update_arguments(None)
472             self.assertEqual(len(UP.mock_calls), 0)
473             apm._arguments = inp
474             apm.update_arguments(cor)
475             assert_dicts_are_equal(self, apm.arguments, exp)
476             UP.assert_called_once_with()
477
478     def test_parse(self):
479         apm = argument.ArgumentParseManager('exe')
480         parsed, unparsed = apm.parser.parse_known_args()
481         apm.parse()
482         self.assertEqual(apm._parsed, parsed)
483         self.assertEqual(apm.unparsed, unparsed)
484
485
486 if __name__ == '__main__':
487     from sys import argv
488     from kamaki.cli.test import runTestCase
489     runTestCase(Argument, 'Argument', argv[1:])
490     runTestCase(ConfigArgument, 'ConfigArgument', argv[1:])
491     runTestCase(RuntimeConfigArgument, 'RuntimeConfigArgument', argv[1:])
492     runTestCase(FlagArgument, 'FlagArgument', argv[1:])
493     runTestCase(FlagArgument, 'ValueArgument', argv[1:])
494     runTestCase(IntArgument, 'IntArgument', argv[1:])
495     runTestCase(DateArgument, 'DateArgument', argv[1:])
496     runTestCase(VersionArgument, 'VersionArgument', argv[1:])
497     runTestCase(KeyValueArgument, 'KeyValueArgument', argv[1:])
498     runTestCase(ProgressBarArgument, 'ProgressBarArgument', argv[1:])
499     runTestCase(ArgumentParseManager, 'ArgumentParseManager', argv[1:])