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
)
22 import testtools
.matchers
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,
47 config
= configparser
.ConfigParser(
49 'allow_unsigned_uploads': "false",
53 config_file
= StringIO(stream
)
55 config
.readfp(config_file
)
56 except configparser
.ParsingError
:
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
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("""\
81 fake_file_simple
= StringIO(textwrap
.dedent("""\
86 fqdn = quux.example.com
89 allow_unsigned_uploads = false
90 allowed_distributions =
93 fake_file_simple_host_three
= StringIO(textwrap
.dedent("""\
98 fqdn = quux.example.com
100 check_version = false
101 allow_unsigned_uploads = false
102 allowed_distributions =
105 fqdn = xyzzy.example.com
108 fqdn = chmrr.example.com
111 fake_file_method_local
= StringIO(textwrap
.dedent("""\
116 fake_file_missing_fqdn
= StringIO(textwrap
.dedent("""\
121 fake_file_missing_incoming
= StringIO(textwrap
.dedent("""\
124 fqdn = quux.example.com
126 fake_file_default_not_unsigned
= StringIO(textwrap
.dedent("""\
128 allow_unsigned_uploads = false
131 fqdn = quux.example.com
133 fake_file_default_distribution_only
= StringIO(textwrap
.dedent("""\
135 default_host_main = consecteur
138 fqdn = quux.example.com
140 fake_file_distribution_none
= StringIO(textwrap
.dedent("""\
143 fqdn = quux.example.com
146 fake_file_distribution_one
= StringIO(textwrap
.dedent("""\
149 fqdn = quux.example.com
152 fake_file_distribution_three
= StringIO(textwrap
.dedent("""\
155 fqdn = quux.example.com
156 distributions = spam,eggs,beans
159 default_scenario_params
= {
161 'file_double_params': dict(
162 path
=runtime_config_file_path
,
163 fake_file
=fake_file_minimal
),
164 'open_scenario_name': 'okay',
167 'file_double_params': dict(
168 path
=global_config_file_path
,
169 fake_file
=fake_file_minimal
),
170 'open_scenario_name': 'okay',
173 'file_double_params': dict(
174 path
=user_config_file_path
,
175 fake_file
=fake_file_minimal
),
176 'open_scenario_name': 'okay',
189 'open_scenario_name': 'nonexist',
193 'exist-read-denied': {
196 'open_scenario_name': 'read_denied',
203 'file_double_params': dict(
204 path
=runtime_config_file_path
,
205 fake_file
=fake_file_empty
),
212 'file_double_params': dict(
213 path
=runtime_config_file_path
,
214 fake_file
=fake_file_bogus
),
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': {
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': {
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': {
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': {
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': {
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': {
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': {
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': {
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': {
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': {
322 'open_scenario_name': 'nonexist',
327 'global-config-read-denied': {
330 'open_scenario_name': 'read_denied',
335 'user-config-not-exist': {
338 'open_scenario_name': 'nonexist',
346 'open_scenario_name': 'nonexist',
349 'open_scenario_name': 'nonexist',
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:
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
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.
386 for scenario
in scenarios
:
387 configs_by_name
= scenario
['configs_by_name']
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)
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(
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.
425 parser
.set(section_name
, option_name
, str(value
))
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(
436 getattr(testcase
, 'config_default_method', "ftp"))
437 testcase
.runtime_config_parser
.set(
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)
459 testcase
.runtime_config_parser
,
460 'DEFAULT', 'default_host_main',
461 config_default_default_host_main
,
463 config_default_delayed
= getattr(
464 testcase
, 'config_default_delayed', NotImplemented)
466 testcase
.runtime_config_parser
,
467 'DEFAULT', 'delayed', config_default_delayed
,
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)
504 testcase
.runtime_config_parser
,
505 section_name
, 'delayed', config_delayed
,
508 for (section_type
, options
) in (
509 getattr(testcase
, 'config_extras', {}).items()):
511 'default': "DEFAULT",
512 'host': testcase
.test_host
,
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. """
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(
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')
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. """
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
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
,
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'. """
583 'allow_unsigned_uploads',
586 'allowed_distributions',
592 'post_upload_command',
593 'pre_upload_command',
594 'ssh_config_options',
596 'progress_indicator',
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')
606 dput
.dput
.read_configs(**self
.test_args
)
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')
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
)
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
)
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')
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
)
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
)
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')
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 ...
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')
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:
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')
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')
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')
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. """
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.
759 # vim: fileencoding=utf-8 filetype=python :