Correct the Bash completion script behaviour.
[dput.git] / test / test_configfile.py
blob0643552777869c834857f350455a6a3ebfe8650a
1 # -*- coding: utf-8; -*-
3 # test/test_configfile.py
4 # Part of ‘dput’, a Debian package upload toolkit.
6 # This is free software, and you are welcome to redistribute it under
7 # certain conditions; see the end of this file for copyright
8 # information, grant of license, and disclaimer of warranty.
10 """ Unit tests for config file behaviour. """
12 from __future__ import (absolute_import, unicode_literals)
14 import doctest
15 import os
16 import os.path
17 import sys
18 import tempfile
19 import textwrap
21 import testtools
22 import testtools.matchers
24 import dput.dput
26 from .helper import (
27 EXIT_STATUS_FAILURE,
28 FakeSystemExit,
29 FileDouble,
30 StringIO,
31 builtins,
32 configparser,
33 mock,
34 patch_system_interfaces,
35 setup_file_double_behaviour,
39 def make_config_from_stream(stream):
40 """ Make a ConfigParser parsed configuration from the stream content.
42 :param stream: Text stream content of a config file.
43 :return: The resulting config if the content parses correctly,
44 or ``None``.
46 """
47 config = configparser.ConfigParser(
48 defaults={
49 'allow_unsigned_uploads': "false",
53 config_file = StringIO(stream)
54 try:
55 config.readfp(config_file)
56 except configparser.ParsingError:
57 config = None
59 return config
62 def make_config_file_scenarios():
63 """ Make a collection of scenarios for testing with config files.
65 :return: A collection of scenarios for tests involving config files.
67 The collection is a mapping from scenario name to a dictionary of
68 scenario attributes.
70 """
72 runtime_config_file_path = tempfile.mktemp()
73 global_config_file_path = os.path.join(os.path.sep, "etc", "dput.cf")
74 user_config_file_path = os.path.join(os.path.expanduser("~"), ".dput.cf")
76 fake_file_empty = StringIO()
77 fake_file_bogus = StringIO("b0gUs")
78 fake_file_minimal = StringIO(textwrap.dedent("""\
79 [DEFAULT]
80 """))
81 fake_file_simple = StringIO(textwrap.dedent("""\
82 [DEFAULT]
83 hash = md5
84 [foo]
85 method = ftp
86 fqdn = quux.example.com
87 incoming = quux
88 check_version = false
89 allow_unsigned_uploads = false
90 allowed_distributions =
91 run_dinstall = false
92 """))
93 fake_file_simple_host_three = StringIO(textwrap.dedent("""\
94 [DEFAULT]
95 hash = md5
96 [foo]
97 method = ftp
98 fqdn = quux.example.com
99 incoming = quux
100 check_version = false
101 allow_unsigned_uploads = false
102 allowed_distributions =
103 run_dinstall = false
104 [bar]
105 fqdn = xyzzy.example.com
106 incoming = xyzzy
107 [baz]
108 fqdn = chmrr.example.com
109 incoming = chmrr
110 """))
111 fake_file_method_local = StringIO(textwrap.dedent("""\
112 [foo]
113 method = local
114 incoming = quux
115 """))
116 fake_file_missing_fqdn = StringIO(textwrap.dedent("""\
117 [foo]
118 method = ftp
119 incoming = quux
120 """))
121 fake_file_missing_incoming = StringIO(textwrap.dedent("""\
122 [foo]
123 method = ftp
124 fqdn = quux.example.com
125 """))
126 fake_file_default_not_unsigned = StringIO(textwrap.dedent("""\
127 [DEFAULT]
128 allow_unsigned_uploads = false
129 [foo]
130 method = ftp
131 fqdn = quux.example.com
132 """))
133 fake_file_default_distribution_only = StringIO(textwrap.dedent("""\
134 [DEFAULT]
135 default_host_main = consecteur
136 [ftp-master]
137 method = ftp
138 fqdn = quux.example.com
139 """))
140 fake_file_distribution_none = StringIO(textwrap.dedent("""\
141 [foo]
142 method = ftp
143 fqdn = quux.example.com
144 distributions =
145 """))
146 fake_file_distribution_one = StringIO(textwrap.dedent("""\
147 [foo]
148 method = ftp
149 fqdn = quux.example.com
150 distributions = spam
151 """))
152 fake_file_distribution_three = StringIO(textwrap.dedent("""\
153 [foo]
154 method = ftp
155 fqdn = quux.example.com
156 distributions = spam,eggs,beans
157 """))
159 default_scenario_params = {
160 'runtime': {
161 'file_double_params': dict(
162 path=runtime_config_file_path,
163 fake_file=fake_file_minimal),
164 'open_scenario_name': 'okay',
166 'global': {
167 'file_double_params': dict(
168 path=global_config_file_path,
169 fake_file=fake_file_minimal),
170 'open_scenario_name': 'okay',
172 'user': {
173 'file_double_params': dict(
174 path=user_config_file_path,
175 fake_file=fake_file_minimal),
176 'open_scenario_name': 'okay',
180 scenarios = {
181 'default': {
182 'configs_by_name': {
183 'runtime': None,
186 'not-exist': {
187 'configs_by_name': {
188 'runtime': {
189 'open_scenario_name': 'nonexist',
193 'exist-read-denied': {
194 'configs_by_name': {
195 'runtime': {
196 'open_scenario_name': 'read_denied',
200 'exist-empty': {
201 'configs_by_name': {
202 'runtime': {
203 'file_double_params': dict(
204 path=runtime_config_file_path,
205 fake_file=fake_file_empty),
209 'exist-invalid': {
210 'configs_by_name': {
211 'runtime': {
212 'file_double_params': dict(
213 path=runtime_config_file_path,
214 fake_file=fake_file_bogus),
218 'exist-minimal': {},
219 'exist-simple': {
220 'configs_by_name': {
221 'runtime': {
222 'file_double_params': dict(
223 path=runtime_config_file_path,
224 fake_file=fake_file_simple),
225 'test_section': "foo",
229 'exist-simple-host-three': {
230 'configs_by_name': {
231 'runtime': {
232 'file_double_params': dict(
233 path=runtime_config_file_path,
234 fake_file=fake_file_simple_host_three),
235 'test_section': "foo",
239 'exist-method-local': {
240 'configs_by_name': {
241 'runtime': {
242 'file_double_params': dict(
243 path=runtime_config_file_path,
244 fake_file=fake_file_method_local),
245 'test_section': "foo",
249 'exist-missing-fqdn': {
250 'configs_by_name': {
251 'runtime': {
252 'file_double_params': dict(
253 path=runtime_config_file_path,
254 fake_file=fake_file_missing_fqdn),
255 'test_section': "foo",
259 'exist-missing-incoming': {
260 'configs_by_name': {
261 'runtime': {
262 'file_double_params': dict(
263 path=runtime_config_file_path,
264 fake_file=fake_file_missing_incoming),
265 'test_section': "foo",
269 'exist-default-not-unsigned': {
270 'configs_by_name': {
271 'runtime': {
272 'file_double_params': dict(
273 path=runtime_config_file_path,
274 fake_file=fake_file_default_not_unsigned),
275 'test_section': "foo",
279 'exist-default-distribution-only': {
280 'configs_by_name': {
281 'runtime': {
282 'file_double_params': dict(
283 path=runtime_config_file_path,
284 fake_file=fake_file_default_distribution_only),
285 'test_section': "foo",
289 'exist-distribution-none': {
290 'configs_by_name': {
291 'runtime': {
292 'file_double_params': dict(
293 path=runtime_config_file_path,
294 fake_file=fake_file_distribution_none),
295 'test_section': "foo",
299 'exist-distribution-one': {
300 'configs_by_name': {
301 'runtime': {
302 'file_double_params': dict(
303 path=runtime_config_file_path,
304 fake_file=fake_file_distribution_one),
305 'test_section': "foo",
309 'exist-distribution-three': {
310 'configs_by_name': {
311 'runtime': {
312 'file_double_params': dict(
313 path=runtime_config_file_path,
314 fake_file=fake_file_distribution_three),
315 'test_section': "foo",
319 'global-config-not-exist': {
320 'configs_by_name': {
321 'global': {
322 'open_scenario_name': 'nonexist',
324 'runtime': None,
327 'global-config-read-denied': {
328 'configs_by_name': {
329 'global': {
330 'open_scenario_name': 'read_denied',
332 'runtime': None,
335 'user-config-not-exist': {
336 'configs_by_name': {
337 'user': {
338 'open_scenario_name': 'nonexist',
340 'runtime': None,
343 'all-not-exist': {
344 'configs_by_name': {
345 'global': {
346 'open_scenario_name': 'nonexist',
348 'user': {
349 'open_scenario_name': 'nonexist',
351 'runtime': None,
356 for scenario in scenarios.values():
357 scenario['empty_file'] = fake_file_empty
358 if 'configs_by_name' not in scenario:
359 scenario['configs_by_name'] = {}
360 for (config_name, default_params) in default_scenario_params.items():
361 if config_name not in scenario['configs_by_name']:
362 params = default_params
363 elif scenario['configs_by_name'][config_name] is None:
364 continue
365 else:
366 params = default_params.copy()
367 params.update(scenario['configs_by_name'][config_name])
368 params['file_double'] = FileDouble(**params['file_double_params'])
369 params['file_double'].set_open_scenario(
370 params['open_scenario_name'])
371 params['config'] = make_config_from_stream(
372 params['file_double'].fake_file.getvalue())
373 scenario['configs_by_name'][config_name] = params
375 return scenarios
378 def get_file_doubles_from_config_file_scenarios(scenarios):
379 """ Get the `FileDouble` instances from config file scenarios.
381 :param scenarios: Collection of config file scenarios.
382 :return: Collection of `FileDouble` instances.
385 doubles = set()
386 for scenario in scenarios:
387 configs_by_name = scenario['configs_by_name']
388 doubles.update(
389 configs_by_name[config_name]['file_double']
390 for config_name in ['global', 'user', 'runtime']
391 if configs_by_name[config_name] is not None)
393 return doubles
396 def setup_config_file_fixtures(testcase):
397 """ Set up fixtures for config file doubles. """
399 scenarios = make_config_file_scenarios()
400 testcase.config_file_scenarios = scenarios
402 setup_file_double_behaviour(
403 testcase,
404 get_file_doubles_from_config_file_scenarios(scenarios.values()))
407 def set_config(testcase, name):
408 """ Set the config scenario for a specific test case. """
409 scenarios = make_config_file_scenarios()
410 testcase.config_scenario = scenarios[name]
413 def patch_runtime_config_options(testcase):
414 """ Patch specific options in the runtime config. """
415 config_params_by_name = testcase.config_scenario['configs_by_name']
416 runtime_config_params = config_params_by_name['runtime']
417 testcase.runtime_config_parser = runtime_config_params['config']
419 def maybe_set_option(
420 parser, section_name, option_name, value, default=""):
421 if value is not None:
422 if value is NotImplemented:
423 # No specified value. Set a default.
424 value = default
425 parser.set(section_name, option_name, str(value))
426 else:
427 # Specifically requested *no* value for the option.
428 parser.remove_option(section_name, option_name)
430 if testcase.runtime_config_parser is not None:
431 testcase.test_host = runtime_config_params.get(
432 'test_section', None)
434 testcase.runtime_config_parser.set(
435 'DEFAULT', 'method',
436 getattr(testcase, 'config_default_method', "ftp"))
437 testcase.runtime_config_parser.set(
438 'DEFAULT', 'login',
439 getattr(testcase, 'config_default_login', "username"))
440 testcase.runtime_config_parser.set(
441 'DEFAULT', 'scp_compress',
442 str(getattr(testcase, 'config_default_scp_compress', False)))
443 testcase.runtime_config_parser.set(
444 'DEFAULT', 'ssh_config_options',
445 getattr(testcase, 'config_default_ssh_config_options', ""))
446 testcase.runtime_config_parser.set(
447 'DEFAULT', 'distributions',
448 getattr(testcase, 'config_default_distributions', ""))
449 testcase.runtime_config_parser.set(
450 'DEFAULT', 'incoming',
451 getattr(testcase, 'config_default_incoming', "quux"))
452 testcase.runtime_config_parser.set(
453 'DEFAULT', 'allow_dcut',
454 str(getattr(testcase, 'config_default_allow_dcut', True)))
456 config_default_default_host_main = getattr(
457 testcase, 'config_default_default_host_main', NotImplemented)
458 maybe_set_option(
459 testcase.runtime_config_parser,
460 'DEFAULT', 'default_host_main',
461 config_default_default_host_main,
462 default="")
463 config_default_delayed = getattr(
464 testcase, 'config_default_delayed', NotImplemented)
465 maybe_set_option(
466 testcase.runtime_config_parser,
467 'DEFAULT', 'delayed', config_default_delayed,
468 default=7)
470 for section_name in testcase.runtime_config_parser.sections():
471 testcase.runtime_config_parser.set(
472 section_name, 'method',
473 getattr(testcase, 'config_method', "ftp"))
474 testcase.runtime_config_parser.set(
475 section_name, 'fqdn',
476 getattr(testcase, 'config_fqdn', "quux.example.com"))
477 testcase.runtime_config_parser.set(
478 section_name, 'passive_ftp',
479 str(getattr(testcase, 'config_passive_ftp', False)))
480 testcase.runtime_config_parser.set(
481 section_name, 'run_lintian',
482 str(getattr(testcase, 'config_run_lintian', False)))
483 testcase.runtime_config_parser.set(
484 section_name, 'run_dinstall',
485 str(getattr(testcase, 'config_run_dinstall', False)))
486 testcase.runtime_config_parser.set(
487 section_name, 'pre_upload_command',
488 getattr(testcase, 'config_pre_upload_command', ""))
489 testcase.runtime_config_parser.set(
490 section_name, 'post_upload_command',
491 getattr(testcase, 'config_post_upload_command', ""))
492 testcase.runtime_config_parser.set(
493 section_name, 'progress_indicator',
494 str(getattr(testcase, 'config_progress_indicator', 0)))
495 testcase.runtime_config_parser.set(
496 section_name, 'allow_dcut',
497 str(getattr(testcase, 'config_allow_dcut', True)))
498 if hasattr(testcase, 'config_incoming'):
499 testcase.runtime_config_parser.set(
500 section_name, 'incoming', testcase.config_incoming)
501 config_delayed = getattr(
502 testcase, 'config_delayed', NotImplemented)
503 maybe_set_option(
504 testcase.runtime_config_parser,
505 section_name, 'delayed', config_delayed,
506 default=9)
508 for (section_type, options) in (
509 getattr(testcase, 'config_extras', {}).items()):
510 section_name = {
511 'default': "DEFAULT",
512 'host': testcase.test_host,
513 }[section_type]
514 for (option_name, option_value) in options.items():
515 testcase.runtime_config_parser.set(
516 section_name, option_name, option_value)
519 class read_configs_TestCase(testtools.TestCase):
520 """ Test cases for `read_config` function. """
522 def setUp(self):
523 """ Set up test fixtures. """
524 super(read_configs_TestCase, self).setUp()
525 patch_system_interfaces(self)
526 setup_config_file_fixtures(self)
528 self.test_configparser = configparser.ConfigParser()
529 self.mock_configparser_class = mock.Mock(
530 'ConfigParser',
531 return_value=self.test_configparser)
533 patcher_class_configparser = mock.patch.object(
534 configparser, "ConfigParser",
535 new=self.mock_configparser_class)
536 patcher_class_configparser.start()
537 self.addCleanup(patcher_class_configparser.stop)
539 self.set_config_file_scenario('exist-minimal')
540 self.set_test_args()
542 def set_config_file_scenario(self, name):
543 """ Set the configuration file scenario for this test case. """
544 self.config_file_scenario = self.config_file_scenarios[name]
545 self.configs_by_name = self.config_file_scenario['configs_by_name']
546 for config_params in self.configs_by_name.values():
547 if config_params is not None:
548 config_params['file_double'].register_for_testcase(self)
550 def get_path_for_runtime_config_file(self):
551 """ Get the path to specify for runtime config file. """
552 path = ""
553 runtime_config_params = self.configs_by_name['runtime']
554 if runtime_config_params is not None:
555 runtime_config_file_double = runtime_config_params['file_double']
556 path = runtime_config_file_double.path
557 return path
559 def set_test_args(self):
560 """ Set the arguments for the test call to the function. """
561 runtime_config_file_path = self.get_path_for_runtime_config_file()
562 self.test_args = dict(
563 extra_config=runtime_config_file_path,
564 debug=False,
567 def test_creates_new_parser(self):
568 """ Should invoke the `ConfigParser` constructor. """
569 dput.dput.read_configs(**self.test_args)
570 configparser.ConfigParser.assert_called_with()
572 def test_returns_expected_configparser(self):
573 """ Should return expected `ConfigParser` instance. """
574 result = dput.dput.read_configs(**self.test_args)
575 self.assertEqual(self.test_configparser, result)
577 def test_sets_default_option_values(self):
578 """ Should set values for options, in section 'DEFAULT'. """
579 option_names = set([
580 'login',
581 'method',
582 'hash',
583 'allow_unsigned_uploads',
584 'allow_dcut',
585 'distributions',
586 'allowed_distributions',
587 'run_lintian',
588 'run_dinstall',
589 'check_version',
590 'scp_compress',
591 'default_host_main',
592 'post_upload_command',
593 'pre_upload_command',
594 'ssh_config_options',
595 'passive_ftp',
596 'progress_indicator',
597 'delayed',
599 result = dput.dput.read_configs(**self.test_args)
600 self.assertTrue(option_names.issubset(set(result.defaults().keys())))
602 def test_opens_default_config_files(self):
603 """ Should open the default config files. """
604 self.set_config_file_scenario('default')
605 self.set_test_args()
606 dput.dput.read_configs(**self.test_args)
607 expected_calls = [
608 mock.call(
609 self.configs_by_name[config_name]['file_double'].path)
610 for config_name in ['global', 'user']]
611 builtins.open.assert_has_calls(expected_calls)
613 def test_opens_specified_config_file(self):
614 """ Should open the specified config file. """
615 dput.dput.read_configs(**self.test_args)
616 builtins.open.assert_called_with(
617 self.configs_by_name['runtime']['file_double'].path)
619 def test_emits_debug_message_on_opening_config_file(self):
620 """ Should emit a debug message when opening the config file. """
621 self.test_args['debug'] = True
622 config_file_double = self.configs_by_name['runtime']['file_double']
623 dput.dput.read_configs(**self.test_args)
624 expected_output = textwrap.dedent("""\
625 D: Parsing Configuration File {path}
626 """).format(path=config_file_double.path)
627 self.assertIn(expected_output, sys.stdout.getvalue())
629 def test_skips_file_if_not_exist(self):
630 """ Should skip a config file if it doesn't exist. """
631 self.set_config_file_scenario('global-config-not-exist')
632 self.set_test_args()
633 config_file_double = self.configs_by_name['global']['file_double']
634 self.test_args['debug'] = True
635 dput.dput.read_configs(**self.test_args)
636 expected_calls = [
637 mock.call(config_file_double.path)]
638 expected_output = textwrap.dedent("""\
639 No such file ...: {path}, skipping
640 """).format(path=config_file_double.path)
641 builtins.open.assert_has_calls(expected_calls)
642 self.assertThat(
643 sys.stderr.getvalue(),
644 testtools.matchers.DocTestMatches(
645 expected_output, flags=doctest.ELLIPSIS))
647 def test_skips_file_if_permission_denied(self):
648 """ Should skip a config file if read permission is denied.. """
649 self.set_config_file_scenario('global-config-read-denied')
650 self.set_test_args()
651 config_file_double = self.configs_by_name['global']['file_double']
652 self.test_args['debug'] = True
653 dput.dput.read_configs(**self.test_args)
654 expected_calls = [
655 mock.call(config_file_double.path)]
656 expected_output = textwrap.dedent("""\
657 Read denied on ...: {path}, skipping
658 """).format(path=config_file_double.path)
659 builtins.open.assert_has_calls(expected_calls)
660 self.assertThat(
661 sys.stderr.getvalue(),
662 testtools.matchers.DocTestMatches(
663 expected_output, flags=doctest.ELLIPSIS))
665 def test_calls_sys_exit_if_no_config_files(self):
666 """ Should call `sys.exit` if unable to open any config files. """
667 self.set_config_file_scenario('all-not-exist')
668 self.set_test_args()
669 with testtools.ExpectedException(FakeSystemExit):
670 dput.dput.read_configs(**self.test_args)
671 expected_output = textwrap.dedent("""\
672 Error: Could not open any configfile, tried ...
673 """)
674 self.assertThat(
675 sys.stderr.getvalue(),
676 testtools.matchers.DocTestMatches(
677 expected_output, flags=doctest.ELLIPSIS))
678 sys.exit.assert_called_with(EXIT_STATUS_FAILURE)
680 def test_calls_sys_exit_if_config_parsing_error(self):
681 """ Should call `sys.exit` if a parsing error occurs. """
682 self.set_config_file_scenario('exist-invalid')
683 self.set_test_args()
684 self.test_args['debug'] = True
685 with testtools.ExpectedException(FakeSystemExit):
686 dput.dput.read_configs(**self.test_args)
687 expected_output = textwrap.dedent("""\
688 Error parsing config file:
690 """)
691 self.assertThat(
692 sys.stderr.getvalue(),
693 testtools.matchers.DocTestMatches(
694 expected_output, flags=doctest.ELLIPSIS))
695 sys.exit.assert_called_with(EXIT_STATUS_FAILURE)
697 def test_sets_fqdn_option_if_local_method(self):
698 """ Should set “fqdn” option for “local” method. """
699 self.set_config_file_scenario('exist-method-local')
700 self.set_test_args()
701 result = dput.dput.read_configs(**self.test_args)
702 runtime_config_params = self.configs_by_name['runtime']
703 test_section = runtime_config_params['test_section']
704 self.assertTrue(result.has_option(test_section, "fqdn"))
706 def test_exits_with_error_if_missing_fqdn(self):
707 """ Should exit with error if config is missing 'fqdn'. """
708 self.set_config_file_scenario('exist-missing-fqdn')
709 self.set_test_args()
710 with testtools.ExpectedException(FakeSystemExit):
711 dput.dput.read_configs(**self.test_args)
712 expected_output = textwrap.dedent("""\
713 Config error: {host} must have a fqdn set
714 """).format(host="foo")
715 self.assertIn(expected_output, sys.stderr.getvalue())
716 sys.exit.assert_called_with(EXIT_STATUS_FAILURE)
718 def test_exits_with_error_if_missing_incoming(self):
719 """ Should exit with error if config is missing 'incoming'. """
720 self.set_config_file_scenario('exist-missing-incoming')
721 self.set_test_args()
722 with testtools.ExpectedException(FakeSystemExit):
723 dput.dput.read_configs(**self.test_args)
724 expected_output = textwrap.dedent("""\
725 Config error: {host} must have an incoming directory set
726 """).format(host="foo")
727 self.assertIn(expected_output, sys.stderr.getvalue())
728 sys.exit.assert_called_with(EXIT_STATUS_FAILURE)
731 class print_config_TestCase(testtools.TestCase):
732 """ Test cases for `print_config` function. """
734 def setUp(self):
735 """ Set up test fixtures. """
736 super(print_config_TestCase, self).setUp()
737 patch_system_interfaces(self)
739 def test_invokes_config_write_to_stdout(self):
740 """ Should invoke config's `write` method with `sys.stdout`. """
741 test_config = make_config_from_stream("")
742 mock_config = mock.Mock(test_config)
743 dput.dput.print_config(mock_config, debug=False)
744 mock_config.write.assert_called_with(sys.stdout)
747 # Copyright © 2015–2016 Ben Finney <bignose@debian.org>
749 # This is free software: you may copy, modify, and/or distribute this work
750 # under the terms of the GNU General Public License as published by the
751 # Free Software Foundation; version 3 of that license or any later version.
752 # No warranty expressed or implied. See the file ‘LICENSE.GPL-3’ for details.
755 # Local variables:
756 # coding: utf-8
757 # mode: python
758 # End:
759 # vim: fileencoding=utf-8 filetype=python :