Add autopkgtest smoke tests to verify the commands will run.
[dput.git] / test / test_configfile.py
blobafd420009135743ba8e7a132b6451179ac9ea8bd
1 # -*- coding: utf-8; -*-
3 # test/test_configfile.py
4 # Part of ‘dput’, a Debian package upload toolkit.
6 # Copyright © 2015–2016 Ben Finney <ben+python@benfinney.id.au>
8 # This is free software: you may copy, modify, and/or distribute this work
9 # under the terms of the GNU General Public License as published by the
10 # Free Software Foundation; version 3 of that license or any later version.
11 # No warranty expressed or implied. See the file ‘LICENSE.GPL-3’ for details.
13 """ Unit tests for config file behaviour. """
15 from __future__ import (absolute_import, unicode_literals)
17 import sys
18 import os
19 import os.path
20 import tempfile
21 import textwrap
22 import doctest
24 import testtools
25 import testtools.matchers
27 __package__ = str("test")
28 __import__(__package__)
29 sys.path.insert(1, os.path.dirname(os.path.dirname(__file__)))
30 import dput.dput
32 from .helper import (
33 builtins,
34 StringIO,
35 configparser,
36 mock,
37 FakeSystemExit,
38 EXIT_STATUS_FAILURE,
39 patch_system_interfaces,
40 FileDouble,
41 setup_file_double_behaviour,
45 def make_config_from_stream(stream):
46 """ Make a ConfigParser parsed configuration from the stream content.
48 :param stream: Text stream content of a config file.
49 :return: The resulting config if the content parses correctly,
50 or ``None``.
52 """
53 config = configparser.ConfigParser(
54 defaults={
55 'allow_unsigned_uploads': "false",
59 config_file = StringIO(stream)
60 try:
61 config.readfp(config_file)
62 except configparser.ParsingError:
63 config = None
65 return config
68 def make_config_file_scenarios():
69 """ Make a collection of scenarios for testing with config files.
71 :return: A collection of scenarios for tests involving config files.
73 The collection is a mapping from scenario name to a dictionary of
74 scenario attributes.
76 """
78 runtime_config_file_path = tempfile.mktemp()
79 global_config_file_path = os.path.join(os.path.sep, "etc", "dput.cf")
80 user_config_file_path = os.path.join(os.path.expanduser("~"), ".dput.cf")
82 fake_file_empty = StringIO()
83 fake_file_bogus = StringIO("b0gUs")
84 fake_file_minimal = StringIO(textwrap.dedent("""\
85 [DEFAULT]
86 """))
87 fake_file_simple = StringIO(textwrap.dedent("""\
88 [DEFAULT]
89 hash = md5
90 [foo]
91 method = ftp
92 fqdn = quux.example.com
93 incoming = quux
94 check_version = false
95 allow_unsigned_uploads = false
96 allowed_distributions =
97 run_dinstall = false
98 """))
99 fake_file_simple_host_three = StringIO(textwrap.dedent("""\
100 [DEFAULT]
101 hash = md5
102 [foo]
103 method = ftp
104 fqdn = quux.example.com
105 incoming = quux
106 check_version = false
107 allow_unsigned_uploads = false
108 allowed_distributions =
109 run_dinstall = false
110 [bar]
111 fqdn = xyzzy.example.com
112 incoming = xyzzy
113 [baz]
114 fqdn = chmrr.example.com
115 incoming = chmrr
116 """))
117 fake_file_method_local = StringIO(textwrap.dedent("""\
118 [foo]
119 method = local
120 incoming = quux
121 """))
122 fake_file_missing_fqdn = StringIO(textwrap.dedent("""\
123 [foo]
124 method = ftp
125 incoming = quux
126 """))
127 fake_file_missing_incoming = StringIO(textwrap.dedent("""\
128 [foo]
129 method = ftp
130 fqdn = quux.example.com
131 """))
132 fake_file_default_not_unsigned = StringIO(textwrap.dedent("""\
133 [DEFAULT]
134 allow_unsigned_uploads = false
135 [foo]
136 method = ftp
137 fqdn = quux.example.com
138 """))
139 fake_file_default_distribution_only = StringIO(textwrap.dedent("""\
140 [DEFAULT]
141 default_host_main = consecteur
142 [ftp-master]
143 method = ftp
144 fqdn = quux.example.com
145 """))
146 fake_file_distribution_none = StringIO(textwrap.dedent("""\
147 [foo]
148 method = ftp
149 fqdn = quux.example.com
150 distributions =
151 """))
152 fake_file_distribution_one = StringIO(textwrap.dedent("""\
153 [foo]
154 method = ftp
155 fqdn = quux.example.com
156 distributions = spam
157 """))
158 fake_file_distribution_three = StringIO(textwrap.dedent("""\
159 [foo]
160 method = ftp
161 fqdn = quux.example.com
162 distributions = spam,eggs,beans
163 """))
165 default_scenario_params = {
166 'runtime': {
167 'file_double_params': dict(
168 path=runtime_config_file_path,
169 fake_file=fake_file_minimal),
170 'open_scenario_name': 'okay',
172 'global': {
173 'file_double_params': dict(
174 path=global_config_file_path,
175 fake_file=fake_file_minimal),
176 'open_scenario_name': 'okay',
178 'user': {
179 'file_double_params': dict(
180 path=user_config_file_path,
181 fake_file=fake_file_minimal),
182 'open_scenario_name': 'okay',
186 scenarios = {
187 'default': {
188 'configs_by_name': {
189 'runtime': None,
192 'not-exist': {
193 'configs_by_name': {
194 'runtime': {
195 'open_scenario_name': 'nonexist',
199 'exist-read-denied': {
200 'configs_by_name': {
201 'runtime': {
202 'open_scenario_name': 'read_denied',
206 'exist-empty': {
207 'configs_by_name': {
208 'runtime': {
209 'file_double_params': dict(
210 path=runtime_config_file_path,
211 fake_file=fake_file_empty),
215 'exist-invalid': {
216 'configs_by_name': {
217 'runtime': {
218 'file_double_params': dict(
219 path=runtime_config_file_path,
220 fake_file=fake_file_bogus),
224 'exist-minimal': {},
225 'exist-simple': {
226 'configs_by_name': {
227 'runtime': {
228 'file_double_params': dict(
229 path=runtime_config_file_path,
230 fake_file=fake_file_simple),
231 'test_section': "foo",
235 'exist-simple-host-three': {
236 'configs_by_name': {
237 'runtime': {
238 'file_double_params': dict(
239 path=runtime_config_file_path,
240 fake_file=fake_file_simple_host_three),
241 'test_section': "foo",
245 'exist-method-local': {
246 'configs_by_name': {
247 'runtime': {
248 'file_double_params': dict(
249 path=runtime_config_file_path,
250 fake_file=fake_file_method_local),
251 'test_section': "foo",
255 'exist-missing-fqdn': {
256 'configs_by_name': {
257 'runtime': {
258 'file_double_params': dict(
259 path=runtime_config_file_path,
260 fake_file=fake_file_missing_fqdn),
261 'test_section': "foo",
265 'exist-missing-incoming': {
266 'configs_by_name': {
267 'runtime': {
268 'file_double_params': dict(
269 path=runtime_config_file_path,
270 fake_file=fake_file_missing_incoming),
271 'test_section': "foo",
275 'exist-default-not-unsigned': {
276 'configs_by_name': {
277 'runtime': {
278 'file_double_params': dict(
279 path=runtime_config_file_path,
280 fake_file=fake_file_default_not_unsigned),
281 'test_section': "foo",
285 'exist-default-distribution-only': {
286 'configs_by_name': {
287 'runtime': {
288 'file_double_params': dict(
289 path=runtime_config_file_path,
290 fake_file=fake_file_default_distribution_only),
291 'test_section': "foo",
295 'exist-distribution-none': {
296 'configs_by_name': {
297 'runtime': {
298 'file_double_params': dict(
299 path=runtime_config_file_path,
300 fake_file=fake_file_distribution_none),
301 'test_section': "foo",
305 'exist-distribution-one': {
306 'configs_by_name': {
307 'runtime': {
308 'file_double_params': dict(
309 path=runtime_config_file_path,
310 fake_file=fake_file_distribution_one),
311 'test_section': "foo",
315 'exist-distribution-three': {
316 'configs_by_name': {
317 'runtime': {
318 'file_double_params': dict(
319 path=runtime_config_file_path,
320 fake_file=fake_file_distribution_three),
321 'test_section': "foo",
325 'global-config-not-exist': {
326 'configs_by_name': {
327 'global': {
328 'open_scenario_name': 'nonexist',
330 'runtime': None,
333 'global-config-read-denied': {
334 'configs_by_name': {
335 'global': {
336 'open_scenario_name': 'read_denied',
338 'runtime': None,
341 'user-config-not-exist': {
342 'configs_by_name': {
343 'user': {
344 'open_scenario_name': 'nonexist',
346 'runtime': None,
349 'all-not-exist': {
350 'configs_by_name': {
351 'global': {
352 'open_scenario_name': 'nonexist',
354 'user': {
355 'open_scenario_name': 'nonexist',
357 'runtime': None,
362 for scenario in scenarios.values():
363 scenario['empty_file'] = fake_file_empty
364 if 'configs_by_name' not in scenario:
365 scenario['configs_by_name'] = {}
366 for (config_name, default_params) in default_scenario_params.items():
367 if config_name not in scenario['configs_by_name']:
368 params = default_params
369 elif scenario['configs_by_name'][config_name] is None:
370 continue
371 else:
372 params = default_params.copy()
373 params.update(scenario['configs_by_name'][config_name])
374 params['file_double'] = FileDouble(**params['file_double_params'])
375 params['file_double'].set_open_scenario(
376 params['open_scenario_name'])
377 params['config'] = make_config_from_stream(
378 params['file_double'].fake_file.getvalue())
379 scenario['configs_by_name'][config_name] = params
381 return scenarios
384 def get_file_doubles_from_config_file_scenarios(scenarios):
385 """ Get the `FileDouble` instances from config file scenarios.
387 :param scenarios: Collection of config file scenarios.
388 :return: Collection of `FileDouble` instances.
391 doubles = set()
392 for scenario in scenarios:
393 configs_by_name = scenario['configs_by_name']
394 doubles.update(
395 configs_by_name[config_name]['file_double']
396 for config_name in ['global', 'user', 'runtime']
397 if configs_by_name[config_name] is not None)
399 return doubles
402 def setup_config_file_fixtures(testcase):
403 """ Set up fixtures for config file doubles. """
405 scenarios = make_config_file_scenarios()
406 testcase.config_file_scenarios = scenarios
408 setup_file_double_behaviour(
409 testcase,
410 get_file_doubles_from_config_file_scenarios(scenarios.values()))
413 def set_config(testcase, name):
414 """ Set the config scenario for a specific test case. """
415 scenarios = make_config_file_scenarios()
416 testcase.config_scenario = scenarios[name]
419 def patch_runtime_config_options(testcase):
420 """ Patch specific options in the runtime config. """
421 config_params_by_name = testcase.config_scenario['configs_by_name']
422 runtime_config_params = config_params_by_name['runtime']
423 testcase.runtime_config_parser = runtime_config_params['config']
425 def maybe_set_option(
426 parser, section_name, option_name, value, default=""):
427 if value is not None:
428 if value is NotImplemented:
429 # No specified value. Set a default.
430 value = default
431 parser.set(section_name, option_name, str(value))
432 else:
433 # Specifically requested *no* value for the option.
434 parser.remove_option(section_name, option_name)
436 if testcase.runtime_config_parser is not None:
437 testcase.test_host = runtime_config_params.get(
438 'test_section', None)
440 testcase.runtime_config_parser.set(
441 'DEFAULT', 'method',
442 getattr(testcase, 'config_default_method', "ftp"))
443 testcase.runtime_config_parser.set(
444 'DEFAULT', 'login',
445 getattr(testcase, 'config_default_login', "username"))
446 testcase.runtime_config_parser.set(
447 'DEFAULT', 'scp_compress',
448 str(getattr(testcase, 'config_default_scp_compress', False)))
449 testcase.runtime_config_parser.set(
450 'DEFAULT', 'ssh_config_options',
451 getattr(testcase, 'config_default_ssh_config_options', ""))
452 testcase.runtime_config_parser.set(
453 'DEFAULT', 'distributions',
454 getattr(testcase, 'config_default_distributions', ""))
455 testcase.runtime_config_parser.set(
456 'DEFAULT', 'incoming',
457 getattr(testcase, 'config_default_incoming', "quux"))
458 testcase.runtime_config_parser.set(
459 'DEFAULT', 'allow_dcut',
460 str(getattr(testcase, 'config_default_allow_dcut', True)))
462 config_default_default_host_main = getattr(
463 testcase, 'config_default_default_host_main', NotImplemented)
464 maybe_set_option(
465 testcase.runtime_config_parser,
466 'DEFAULT', 'default_host_main',
467 config_default_default_host_main,
468 default="")
469 config_default_delayed = getattr(
470 testcase, 'config_default_delayed', NotImplemented)
471 maybe_set_option(
472 testcase.runtime_config_parser,
473 'DEFAULT', 'delayed', config_default_delayed,
474 default=7)
476 for section_name in testcase.runtime_config_parser.sections():
477 testcase.runtime_config_parser.set(
478 section_name, 'method',
479 getattr(testcase, 'config_method', "ftp"))
480 testcase.runtime_config_parser.set(
481 section_name, 'fqdn',
482 getattr(testcase, 'config_fqdn', "quux.example.com"))
483 testcase.runtime_config_parser.set(
484 section_name, 'passive_ftp',
485 str(getattr(testcase, 'config_passive_ftp', False)))
486 testcase.runtime_config_parser.set(
487 section_name, 'run_lintian',
488 str(getattr(testcase, 'config_run_lintian', False)))
489 testcase.runtime_config_parser.set(
490 section_name, 'run_dinstall',
491 str(getattr(testcase, 'config_run_dinstall', False)))
492 testcase.runtime_config_parser.set(
493 section_name, 'pre_upload_command',
494 getattr(testcase, 'config_pre_upload_command', ""))
495 testcase.runtime_config_parser.set(
496 section_name, 'post_upload_command',
497 getattr(testcase, 'config_post_upload_command', ""))
498 testcase.runtime_config_parser.set(
499 section_name, 'progress_indicator',
500 str(getattr(testcase, 'config_progress_indicator', 0)))
501 testcase.runtime_config_parser.set(
502 section_name, 'allow_dcut',
503 str(getattr(testcase, 'config_allow_dcut', True)))
504 if hasattr(testcase, 'config_incoming'):
505 testcase.runtime_config_parser.set(
506 section_name, 'incoming', testcase.config_incoming)
507 config_delayed = getattr(
508 testcase, 'config_delayed', NotImplemented)
509 maybe_set_option(
510 testcase.runtime_config_parser,
511 section_name, 'delayed', config_delayed,
512 default=9)
514 for (section_type, options) in (
515 getattr(testcase, 'config_extras', {}).items()):
516 section_name = {
517 'default': "DEFAULT",
518 'host': testcase.test_host,
519 }[section_type]
520 for (option_name, option_value) in options.items():
521 testcase.runtime_config_parser.set(
522 section_name, option_name, option_value)
525 class read_configs_TestCase(testtools.TestCase):
526 """ Test cases for `read_config` function. """
528 def setUp(self):
529 """ Set up test fixtures. """
530 super(read_configs_TestCase, self).setUp()
531 patch_system_interfaces(self)
532 setup_config_file_fixtures(self)
534 self.test_configparser = configparser.ConfigParser()
535 self.mock_configparser_class = mock.Mock(
536 'ConfigParser',
537 return_value=self.test_configparser)
539 patcher_class_configparser = mock.patch.object(
540 configparser, "ConfigParser",
541 new=self.mock_configparser_class)
542 patcher_class_configparser.start()
543 self.addCleanup(patcher_class_configparser.stop)
545 self.set_config_file_scenario('exist-minimal')
546 self.set_test_args()
548 def set_config_file_scenario(self, name):
549 """ Set the configuration file scenario for this test case. """
550 self.config_file_scenario = self.config_file_scenarios[name]
551 self.configs_by_name = self.config_file_scenario['configs_by_name']
552 for config_params in self.configs_by_name.values():
553 if config_params is not None:
554 config_params['file_double'].register_for_testcase(self)
556 def get_path_for_runtime_config_file(self):
557 """ Get the path to specify for runtime config file. """
558 path = ""
559 runtime_config_params = self.configs_by_name['runtime']
560 if runtime_config_params is not None:
561 runtime_config_file_double = runtime_config_params['file_double']
562 path = runtime_config_file_double.path
563 return path
565 def set_test_args(self):
566 """ Set the arguments for the test call to the function. """
567 runtime_config_file_path = self.get_path_for_runtime_config_file()
568 self.test_args = dict(
569 extra_config=runtime_config_file_path,
570 debug=False,
573 def test_creates_new_parser(self):
574 """ Should invoke the `ConfigParser` constructor. """
575 dput.dput.read_configs(**self.test_args)
576 configparser.ConfigParser.assert_called_with()
578 def test_returns_expected_configparser(self):
579 """ Should return expected `ConfigParser` instance. """
580 result = dput.dput.read_configs(**self.test_args)
581 self.assertEqual(self.test_configparser, result)
583 def test_sets_default_option_values(self):
584 """ Should set values for options, in section 'DEFAULT'. """
585 option_names = set([
586 'login',
587 'method',
588 'hash',
589 'allow_unsigned_uploads',
590 'allow_dcut',
591 'distributions',
592 'allowed_distributions',
593 'run_lintian',
594 'run_dinstall',
595 'check_version',
596 'scp_compress',
597 'default_host_main',
598 'post_upload_command',
599 'pre_upload_command',
600 'ssh_config_options',
601 'passive_ftp',
602 'progress_indicator',
603 'delayed',
605 result = dput.dput.read_configs(**self.test_args)
606 self.assertTrue(option_names.issubset(set(result.defaults().keys())))
608 def test_opens_default_config_files(self):
609 """ Should open the default config files. """
610 self.set_config_file_scenario('default')
611 self.set_test_args()
612 dput.dput.read_configs(**self.test_args)
613 expected_calls = [
614 mock.call(
615 self.configs_by_name[config_name]['file_double'].path)
616 for config_name in ['global', 'user']]
617 builtins.open.assert_has_calls(expected_calls)
619 def test_opens_specified_config_file(self):
620 """ Should open the specified config file. """
621 dput.dput.read_configs(**self.test_args)
622 builtins.open.assert_called_with(
623 self.configs_by_name['runtime']['file_double'].path)
625 def test_emits_debug_message_on_opening_config_file(self):
626 """ Should emit a debug message when opening the config file. """
627 self.test_args['debug'] = True
628 config_file_double = self.configs_by_name['runtime']['file_double']
629 dput.dput.read_configs(**self.test_args)
630 expected_output = textwrap.dedent("""\
631 D: Parsing Configuration File {path}
632 """).format(path=config_file_double.path)
633 self.assertIn(expected_output, sys.stdout.getvalue())
635 def test_skips_file_if_not_exist(self):
636 """ Should skip a config file if it doesn't exist. """
637 self.set_config_file_scenario('global-config-not-exist')
638 self.set_test_args()
639 config_file_double = self.configs_by_name['global']['file_double']
640 self.test_args['debug'] = True
641 dput.dput.read_configs(**self.test_args)
642 expected_calls = [
643 mock.call(config_file_double.path)]
644 expected_output = textwrap.dedent("""\
645 No such file ...: {path}, skipping
646 """).format(path=config_file_double.path)
647 builtins.open.assert_has_calls(expected_calls)
648 self.assertThat(
649 sys.stderr.getvalue(),
650 testtools.matchers.DocTestMatches(
651 expected_output, flags=doctest.ELLIPSIS))
653 def test_skips_file_if_permission_denied(self):
654 """ Should skip a config file if read permission is denied.. """
655 self.set_config_file_scenario('global-config-read-denied')
656 self.set_test_args()
657 config_file_double = self.configs_by_name['global']['file_double']
658 self.test_args['debug'] = True
659 dput.dput.read_configs(**self.test_args)
660 expected_calls = [
661 mock.call(config_file_double.path)]
662 expected_output = textwrap.dedent("""\
663 Read denied on ...: {path}, skipping
664 """).format(path=config_file_double.path)
665 builtins.open.assert_has_calls(expected_calls)
666 self.assertThat(
667 sys.stderr.getvalue(),
668 testtools.matchers.DocTestMatches(
669 expected_output, flags=doctest.ELLIPSIS))
671 def test_calls_sys_exit_if_no_config_files(self):
672 """ Should call `sys.exit` if unable to open any config files. """
673 self.set_config_file_scenario('all-not-exist')
674 self.set_test_args()
675 with testtools.ExpectedException(FakeSystemExit):
676 dput.dput.read_configs(**self.test_args)
677 expected_output = textwrap.dedent("""\
678 Error: Could not open any configfile, tried ...
679 """)
680 self.assertThat(
681 sys.stderr.getvalue(),
682 testtools.matchers.DocTestMatches(
683 expected_output, flags=doctest.ELLIPSIS))
684 sys.exit.assert_called_with(EXIT_STATUS_FAILURE)
686 def test_calls_sys_exit_if_config_parsing_error(self):
687 """ Should call `sys.exit` if a parsing error occurs. """
688 self.set_config_file_scenario('exist-invalid')
689 self.set_test_args()
690 self.test_args['debug'] = True
691 with testtools.ExpectedException(FakeSystemExit):
692 dput.dput.read_configs(**self.test_args)
693 expected_output = textwrap.dedent("""\
694 Error parsing config file:
696 """)
697 self.assertThat(
698 sys.stderr.getvalue(),
699 testtools.matchers.DocTestMatches(
700 expected_output, flags=doctest.ELLIPSIS))
701 sys.exit.assert_called_with(EXIT_STATUS_FAILURE)
703 def test_sets_fqdn_option_if_local_method(self):
704 """ Should set “fqdn” option for “local” method. """
705 self.set_config_file_scenario('exist-method-local')
706 self.set_test_args()
707 result = dput.dput.read_configs(**self.test_args)
708 runtime_config_params = self.configs_by_name['runtime']
709 test_section = runtime_config_params['test_section']
710 self.assertTrue(result.has_option(test_section, "fqdn"))
712 def test_exits_with_error_if_missing_fqdn(self):
713 """ Should exit with error if config is missing 'fqdn'. """
714 self.set_config_file_scenario('exist-missing-fqdn')
715 self.set_test_args()
716 with testtools.ExpectedException(FakeSystemExit):
717 dput.dput.read_configs(**self.test_args)
718 expected_output = textwrap.dedent("""\
719 Config error: {host} must have a fqdn set
720 """).format(host="foo")
721 self.assertIn(expected_output, sys.stderr.getvalue())
722 sys.exit.assert_called_with(EXIT_STATUS_FAILURE)
724 def test_exits_with_error_if_missing_incoming(self):
725 """ Should exit with error if config is missing 'incoming'. """
726 self.set_config_file_scenario('exist-missing-incoming')
727 self.set_test_args()
728 with testtools.ExpectedException(FakeSystemExit):
729 dput.dput.read_configs(**self.test_args)
730 expected_output = textwrap.dedent("""\
731 Config error: {host} must have an incoming directory set
732 """).format(host="foo")
733 self.assertIn(expected_output, sys.stderr.getvalue())
734 sys.exit.assert_called_with(EXIT_STATUS_FAILURE)
737 class print_config_TestCase(testtools.TestCase):
738 """ Test cases for `print_config` function. """
740 def setUp(self):
741 """ Set up test fixtures. """
742 super(print_config_TestCase, self).setUp()
743 patch_system_interfaces(self)
745 def test_invokes_config_write_to_stdout(self):
746 """ Should invoke config's `write` method with `sys.stdout`. """
747 test_config = make_config_from_stream("")
748 mock_config = mock.Mock(test_config)
749 dput.dput.print_config(mock_config, debug=False)
750 mock_config.write.assert_called_with(sys.stdout)
753 # Local variables:
754 # coding: utf-8
755 # mode: python
756 # End:
757 # vim: fileencoding=utf-8 filetype=python :