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
)
25 import testtools
.matchers
27 __package__
= str("test")
28 __import__(__package__
)
29 sys
.path
.insert(1, os
.path
.dirname(os
.path
.dirname(__file__
)))
39 patch_system_interfaces
,
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,
53 config
= configparser
.ConfigParser(
55 'allow_unsigned_uploads': "false",
59 config_file
= StringIO(stream
)
61 config
.readfp(config_file
)
62 except configparser
.ParsingError
:
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
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("""\
87 fake_file_simple
= StringIO(textwrap
.dedent("""\
92 fqdn = quux.example.com
95 allow_unsigned_uploads = false
96 allowed_distributions =
99 fake_file_simple_host_three
= StringIO(textwrap
.dedent("""\
104 fqdn = quux.example.com
106 check_version = false
107 allow_unsigned_uploads = false
108 allowed_distributions =
111 fqdn = xyzzy.example.com
114 fqdn = chmrr.example.com
117 fake_file_method_local
= StringIO(textwrap
.dedent("""\
122 fake_file_missing_fqdn
= StringIO(textwrap
.dedent("""\
127 fake_file_missing_incoming
= StringIO(textwrap
.dedent("""\
130 fqdn = quux.example.com
132 fake_file_default_not_unsigned
= StringIO(textwrap
.dedent("""\
134 allow_unsigned_uploads = false
137 fqdn = quux.example.com
139 fake_file_default_distribution_only
= StringIO(textwrap
.dedent("""\
141 default_host_main = consecteur
144 fqdn = quux.example.com
146 fake_file_distribution_none
= StringIO(textwrap
.dedent("""\
149 fqdn = quux.example.com
152 fake_file_distribution_one
= StringIO(textwrap
.dedent("""\
155 fqdn = quux.example.com
158 fake_file_distribution_three
= StringIO(textwrap
.dedent("""\
161 fqdn = quux.example.com
162 distributions = spam,eggs,beans
165 default_scenario_params
= {
167 'file_double_params': dict(
168 path
=runtime_config_file_path
,
169 fake_file
=fake_file_minimal
),
170 'open_scenario_name': 'okay',
173 'file_double_params': dict(
174 path
=global_config_file_path
,
175 fake_file
=fake_file_minimal
),
176 'open_scenario_name': 'okay',
179 'file_double_params': dict(
180 path
=user_config_file_path
,
181 fake_file
=fake_file_minimal
),
182 'open_scenario_name': 'okay',
195 'open_scenario_name': 'nonexist',
199 'exist-read-denied': {
202 'open_scenario_name': 'read_denied',
209 'file_double_params': dict(
210 path
=runtime_config_file_path
,
211 fake_file
=fake_file_empty
),
218 'file_double_params': dict(
219 path
=runtime_config_file_path
,
220 fake_file
=fake_file_bogus
),
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': {
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': {
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': {
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': {
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': {
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': {
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': {
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': {
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': {
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': {
328 'open_scenario_name': 'nonexist',
333 'global-config-read-denied': {
336 'open_scenario_name': 'read_denied',
341 'user-config-not-exist': {
344 'open_scenario_name': 'nonexist',
352 'open_scenario_name': 'nonexist',
355 'open_scenario_name': 'nonexist',
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:
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
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.
392 for scenario
in scenarios
:
393 configs_by_name
= scenario
['configs_by_name']
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)
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(
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.
431 parser
.set(section_name
, option_name
, str(value
))
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(
442 getattr(testcase
, 'config_default_method', "ftp"))
443 testcase
.runtime_config_parser
.set(
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)
465 testcase
.runtime_config_parser
,
466 'DEFAULT', 'default_host_main',
467 config_default_default_host_main
,
469 config_default_delayed
= getattr(
470 testcase
, 'config_default_delayed', NotImplemented)
472 testcase
.runtime_config_parser
,
473 'DEFAULT', 'delayed', config_default_delayed
,
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)
510 testcase
.runtime_config_parser
,
511 section_name
, 'delayed', config_delayed
,
514 for (section_type
, options
) in (
515 getattr(testcase
, 'config_extras', {}).items()):
517 'default': "DEFAULT",
518 'host': testcase
.test_host
,
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. """
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(
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')
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. """
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
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
,
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'. """
589 'allow_unsigned_uploads',
592 'allowed_distributions',
598 'post_upload_command',
599 'pre_upload_command',
600 'ssh_config_options',
602 'progress_indicator',
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')
612 dput
.dput
.read_configs(**self
.test_args
)
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')
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
)
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
)
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')
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
)
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
)
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')
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 ...
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')
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:
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')
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')
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')
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. """
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
)
757 # vim: fileencoding=utf-8 filetype=python :