From 5c0b8c2b0229992671e076e74c1256a880381d62 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Tue, 16 Sep 2014 15:50:04 -0700 Subject: testsuite: Added unit tests for new option parsing --- .../Testlib/TestClient/TestTools/Test_init.py | 19 +- testsuite/Testsrc/Testlib/TestOptions/One.py | 6 + .../Testsrc/Testlib/TestOptions/TestComponents.py | 210 +++++++++ .../Testsrc/Testlib/TestOptions/TestConfigFiles.py | 48 +++ .../Testlib/TestOptions/TestOptionGroups.py | 145 +++++++ .../Testsrc/Testlib/TestOptions/TestOptions.py | 469 +++++++++++++++++++++ .../Testsrc/Testlib/TestOptions/TestSubcommands.py | 141 +++++++ testsuite/Testsrc/Testlib/TestOptions/TestTypes.py | 111 +++++ .../Testsrc/Testlib/TestOptions/TestWildcards.py | 47 +++ testsuite/Testsrc/Testlib/TestOptions/Two.py | 6 + testsuite/Testsrc/Testlib/TestOptions/__init__.py | 85 ++++ .../Testlib/TestServer/TestPlugin/Testbase.py | 1 + .../Testlib/TestServer/TestPlugin/Testhelpers.py | 1 + testsuite/Testsrc/Testlib/TestUtils.py | 4 - testsuite/Testsrc/__init__.py | 0 15 files changed, 1279 insertions(+), 14 deletions(-) create mode 100644 testsuite/Testsrc/Testlib/TestOptions/One.py create mode 100644 testsuite/Testsrc/Testlib/TestOptions/TestComponents.py create mode 100644 testsuite/Testsrc/Testlib/TestOptions/TestConfigFiles.py create mode 100644 testsuite/Testsrc/Testlib/TestOptions/TestOptionGroups.py create mode 100644 testsuite/Testsrc/Testlib/TestOptions/TestOptions.py create mode 100644 testsuite/Testsrc/Testlib/TestOptions/TestSubcommands.py create mode 100644 testsuite/Testsrc/Testlib/TestOptions/TestTypes.py create mode 100644 testsuite/Testsrc/Testlib/TestOptions/TestWildcards.py create mode 100644 testsuite/Testsrc/Testlib/TestOptions/Two.py create mode 100644 testsuite/Testsrc/Testlib/TestOptions/__init__.py create mode 100644 testsuite/Testsrc/__init__.py (limited to 'testsuite') diff --git a/testsuite/Testsrc/Testlib/TestClient/TestTools/Test_init.py b/testsuite/Testsrc/Testlib/TestClient/TestTools/Test_init.py index 0e9e3a141..b9de703ff 100644 --- a/testsuite/Testsrc/Testlib/TestClient/TestTools/Test_init.py +++ b/testsuite/Testsrc/Testlib/TestClient/TestTools/Test_init.py @@ -17,18 +17,17 @@ while path != "/": path = os.path.dirname(path) from common import * -# try to find true -if os.path.exists("/bin/true"): - TRUE = "/bin/true" -elif os.path.exists("/usr/bin/true"): - TRUE = "/usr/bin/true" -else: - TRUE = None - class TestTool(Bcfg2TestCase): test_obj = Tool + if os.path.exists("/bin/true"): + true = "/bin/true" + elif os.path.exists("/usr/bin/true"): + true = "/usr/bin/true" + else: + true = None + def setUp(self): set_setup_default('command_timeout') set_setup_default('interactive', False) @@ -77,11 +76,11 @@ class TestTool(Bcfg2TestCase): ["/test"] + [e.get("name") for e in important]) t.getSupportedEntries.assert_called_with() - @skipIf(TRUE is None, "/bin/true or equivalent not found") + @skipIf(true is None, "/bin/true or equivalent not found") def test__check_execs(self): t = self.get_obj() if t.__execs__ == []: - t.__execs__.append(TRUE) + t.__execs__.append(self.true) @patch("os.stat") def inner(mock_stat): diff --git a/testsuite/Testsrc/Testlib/TestOptions/One.py b/testsuite/Testsrc/Testlib/TestOptions/One.py new file mode 100644 index 000000000..dac7f4558 --- /dev/null +++ b/testsuite/Testsrc/Testlib/TestOptions/One.py @@ -0,0 +1,6 @@ +"""Test module for component loading.""" + + +class One(object): + """Test class for component loading.""" + pass diff --git a/testsuite/Testsrc/Testlib/TestOptions/TestComponents.py b/testsuite/Testsrc/Testlib/TestOptions/TestComponents.py new file mode 100644 index 000000000..d637d34c5 --- /dev/null +++ b/testsuite/Testsrc/Testlib/TestOptions/TestComponents.py @@ -0,0 +1,210 @@ +"""test component loading.""" + +import argparse +import os + +from Bcfg2.Options import Option, BooleanOption, ComponentAction, get_parser, \ + new_parser, Types, ConfigFileAction + +from testsuite.Testsrc.Testlib.TestOptions import make_config, One, Two, \ + OptionTestCase + + +# create a bunch of fake components for testing component loading options + +class ChildOne(object): + """fake component for testing component loading.""" + options = [Option("--child-one")] + + +class ChildTwo(object): + """fake component for testing component loading.""" + options = [Option("--child-two")] + + +class ChildComponentAction(ComponentAction): + """child component loader action.""" + islist = False + mapping = {"one": ChildOne, + "two": ChildTwo} + + +class ComponentOne(object): + """fake component for testing component loading.""" + options = [BooleanOption("--one")] + + +class ComponentTwo(object): + """fake component for testing component loading.""" + options = [Option("--child", default="one", action=ChildComponentAction)] + + +class ComponentThree(object): + """fake component for testing component loading.""" + options = [BooleanOption("--three")] + + +class ConfigFileComponent(object): + """fake component for testing component loading.""" + options = [Option("--config2", action=ConfigFileAction), + Option(cf=("config", "test"), dest="config2_test", + default="bar")] + + +class ParentComponentAction(ComponentAction): + """parent component loader action.""" + mapping = {"one": ComponentOne, + "two": ComponentTwo, + "three": ComponentThree, + "config": ConfigFileComponent} + + +class TestComponentOptions(OptionTestCase): + """test cases for component loading.""" + + def setUp(self): + self.options = [ + Option("--parent", type=Types.comma_list, + default=["one", "two"], action=ParentComponentAction)] + + self.result = argparse.Namespace() + new_parser() + self.parser = get_parser(components=[self], namespace=self.result, + description="component testing parser") + + @make_config() + def test_loading_components(self, config_file): + """load a single component during option parsing.""" + self.parser.parse(["-C", config_file, "--parent", "one"]) + self.assertEqual(self.result.parent, [ComponentOne]) + + @make_config() + def test_component_option(self, config_file): + """use options from a component loaded during option parsing.""" + self.parser.parse(["--one", "-C", config_file, "--parent", "one"]) + self.assertEqual(self.result.parent, [ComponentOne]) + self.assertTrue(self.result.one) + + @make_config() + def test_multi_component_load(self, config_file): + """load multiple components during option parsing.""" + self.parser.parse(["-C", config_file, "--parent", "one,three"]) + self.assertEqual(self.result.parent, [ComponentOne, ComponentThree]) + + @make_config() + def test_multi_component_options(self, config_file): + """use options from multiple components during option parsing.""" + self.parser.parse(["-C", config_file, "--three", + "--parent", "one,three", "--one"]) + self.assertEqual(self.result.parent, [ComponentOne, ComponentThree]) + self.assertTrue(self.result.one) + self.assertTrue(self.result.three) + + @make_config() + def test_component_default_not_loaded(self, config_file): + """options from default but unused components not available.""" + self.assertRaises( + SystemExit, + self.parser.parse, + ["-C", config_file, "--child", "one", "--parent", "one"]) + + @make_config() + def test_tiered_components(self, config_file): + """load child component.""" + self.parser.parse(["-C", config_file, "--parent", "two", + "--child", "one"]) + self.assertEqual(self.result.parent, [ComponentTwo]) + self.assertEqual(self.result.child, ChildOne) + + @make_config() + def test_options_tiered_components(self, config_file): + """use options from child component.""" + self.parser.parse(["--child-one", "foo", "-C", config_file, "--parent", + "two", "--child", "one"]) + self.assertEqual(self.result.parent, [ComponentTwo]) + self.assertEqual(self.result.child, ChildOne) + self.assertEqual(self.result.child_one, "foo") + + @make_config() + def test_bogus_component(self, config_file): + """error out with bad component name.""" + self.assertRaises(SystemExit, + self.parser.parse, + ["-C", config_file, "--parent", "blargle"]) + + @make_config() + @make_config({"config": {"test": "foo"}}) + def test_config_component(self, config1, config2): + """load component with alternative config file.""" + self.parser.parse(["-C", config1, "--config2", config2, + "--parent", "config"]) + self.assertEqual(self.result.config2, config2) + self.assertEqual(self.result.config2_test, "foo") + + @make_config() + def test_config_component_no_file(self, config_file): + """load component with missing alternative config file.""" + self.parser.parse(["-C", config_file, "--parent", "config"]) + self.assertEqual(self.result.config2, None) + + +class ImportComponentAction(ComponentAction): + """action that imports real classes for testing.""" + islist = False + bases = ["testsuite.Testsrc.Testlib.TestOptions"] + + +class ImportModuleAction(ImportComponentAction): + """action that only imports modules for testing.""" + module = True + + +class TestImportComponentOptions(OptionTestCase): + """test cases for component loading.""" + + def setUp(self): + self.options = [Option("--cls", action=ImportComponentAction), + Option("--module", action=ImportModuleAction)] + + self.result = argparse.Namespace() + new_parser() + self.parser = get_parser(components=[self], namespace=self.result) + + @make_config() + def test_import_component(self, config_file): + """load class components by importing.""" + self.parser.parse(["-C", config_file, "--cls", "One"]) + self.assertEqual(self.result.cls, One.One) + + @make_config() + def test_import_module(self, config_file): + """load module components by importing.""" + self.parser.parse(["-C", config_file, "--module", "One"]) + self.assertEqual(self.result.module, One) + + @make_config() + def test_import_full_path(self, config_file): + """load components by importing the full path.""" + self.parser.parse(["-C", config_file, "--cls", "os.path"]) + self.assertEqual(self.result.cls, os.path) + + @make_config() + def test_import_bogus_class(self, config_file): + """fail to load class component that cannot be imported.""" + self.assertRaises(SystemExit, + self.parser.parse, + ["-C", config_file, "--cls", "Three"]) + + @make_config() + def test_import_bogus_module(self, config_file): + """fail to load module component that cannot be imported.""" + self.assertRaises(SystemExit, + self.parser.parse, + ["-C", config_file, "--module", "Three"]) + + @make_config() + def test_import_bogus_path(self, config_file): + """fail to load component that cannot be imported by full path.""" + self.assertRaises(SystemExit, + self.parser.parse, + ["-C", config_file, "--cls", "Bcfg2.No.Such.Thing"]) diff --git a/testsuite/Testsrc/Testlib/TestOptions/TestConfigFiles.py b/testsuite/Testsrc/Testlib/TestOptions/TestConfigFiles.py new file mode 100644 index 000000000..aee2ff666 --- /dev/null +++ b/testsuite/Testsrc/Testlib/TestOptions/TestConfigFiles.py @@ -0,0 +1,48 @@ +"""test reading multiple config files.""" + +import argparse + +from Bcfg2.Options import Option, PathOption, ConfigFileAction, get_parser, \ + new_parser + +from testsuite.Testsrc.Testlib.TestOptions import make_config, OptionTestCase + + +class TestConfigFiles(OptionTestCase): + def setUp(self): + self.options = [ + PathOption(cf=("test", "config2"), action=ConfigFileAction), + PathOption(cf=("test", "config3"), action=ConfigFileAction), + Option(cf=("test", "foo")), + Option(cf=("test", "bar")), + Option(cf=("test", "baz"))] + self.results = argparse.Namespace() + new_parser() + self.parser = get_parser(components=[self], namespace=self.results) + + @make_config({"test": {"baz": "baz"}}) + def test_config_files(self, config3): + """read multiple config files.""" + # Because make_config() generates temporary files for the + # configuration, we have to work backwards here. first we + # generate config3, then we generate config2 (which includes a + # reference to config3), then we finally generate the main + # config file, which contains a reference to config2. oh how + # I wish we could use context managers here... + + @make_config({"test": {"bar": "bar", "config3": config3}}) + def inner1(config2): + @make_config({"test": {"foo": "foo", "config2": config2}}) + def inner2(config): + self.parser.parse(["-C", config]) + self.assertEqual(self.results.foo, "foo") + self.assertEqual(self.results.bar, "bar") + self.assertEqual(self.results.baz, "baz") + + inner2() + + inner1() + + def test_no_config_file(self): + """fail to read config file.""" + self.assertRaises(SystemExit, self.parser.parse, []) diff --git a/testsuite/Testsrc/Testlib/TestOptions/TestOptionGroups.py b/testsuite/Testsrc/Testlib/TestOptions/TestOptionGroups.py new file mode 100644 index 000000000..de1abbb1b --- /dev/null +++ b/testsuite/Testsrc/Testlib/TestOptions/TestOptionGroups.py @@ -0,0 +1,145 @@ +"""test reading multiple config files.""" + +import argparse + +from Bcfg2.Options import Option, BooleanOption, Parser, OptionGroup, \ + ExclusiveOptionGroup, WildcardSectionGroup, new_parser, get_parser + +from testsuite.common import Bcfg2TestCase +from testsuite.Testsrc.Testlib.TestOptions import make_config, OptionTestCase + + +class TestOptionGroups(Bcfg2TestCase): + def setUp(self): + self.options = None + + def _test_options(self, options): + """test helper.""" + result = argparse.Namespace() + parser = Parser(components=[self], namespace=result) + parser.parse(options) + return result + + def test_option_group(self): + """basic option group functionality.""" + self.options = [OptionGroup(BooleanOption("--foo"), + BooleanOption("--bar"), + BooleanOption("--baz"), + title="group")] + result = self._test_options(["--foo", "--bar"]) + self.assertTrue(result.foo) + self.assertTrue(result.bar) + self.assertFalse(result.baz) + + def test_exclusive_option_group(self): + """parse options from exclusive option group.""" + self.options = [ + ExclusiveOptionGroup(BooleanOption("--foo"), + BooleanOption("--bar"), + BooleanOption("--baz"))] + result = self._test_options(["--foo"]) + self.assertTrue(result.foo) + self.assertFalse(result.bar) + self.assertFalse(result.baz) + + self.assertRaises(SystemExit, + self._test_options, ["--foo", "--bar"]) + + def test_required_exclusive_option_group(self): + """parse options from required exclusive option group.""" + self.options = [ + ExclusiveOptionGroup(BooleanOption("--foo"), + BooleanOption("--bar"), + BooleanOption("--baz"), + required=True)] + result = self._test_options(["--foo"]) + self.assertTrue(result.foo) + self.assertFalse(result.bar) + self.assertFalse(result.baz) + + self.assertRaises(SystemExit, self._test_options, []) + + def test_option_group(self): + """nest option groups.""" + self.options = [ + OptionGroup( + BooleanOption("--foo"), + BooleanOption("--bar"), + OptionGroup( + BooleanOption("--baz"), + BooleanOption("--quux"), + ExclusiveOptionGroup( + BooleanOption("--test1"), + BooleanOption("--test2")), + title="inner"), + title="outer")] + result = self._test_options(["--foo", "--baz", "--test1"]) + self.assertTrue(result.foo) + self.assertFalse(result.bar) + self.assertTrue(result.baz) + self.assertFalse(result.quux) + self.assertTrue(result.test1) + self.assertFalse(result.test2) + + self.assertRaises(SystemExit, + self._test_options, ["--test1", "--test2"]) + + +class TestWildcardSectionGroups(OptionTestCase): + config = { + "four:one": { + "foo": "foo one", + "bar": "bar one", + "baz": "baz one" + }, + "four:two": { + "foo": "foo two", + "bar": "bar two" + }, + "five:one": { + "foo": "foo one", + "bar": "bar one" + }, + "five:two": { + "foo": "foo two", + "bar": "bar two" + }, + "five:three": { + "foo": "foo three", + "bar": "bar three" + } + } + + def setUp(self): + self.options = [ + WildcardSectionGroup( + Option(cf=("four:*", "foo")), + Option(cf=("four:*", "bar"))), + WildcardSectionGroup( + Option(cf=("five:*", "foo")), + Option(cf=("five:*", "bar")), + prefix="", + dest="sections")] + self.results = argparse.Namespace() + new_parser() + self.parser = get_parser(components=[self], namespace=self.results) + + @make_config(config) + def test_wildcard_section_groups(self, config_file): + """parse options from wildcard section groups.""" + self.parser.parse(["-C", config_file]) + self.assertEqual(self.results.four_four_one_foo, "foo one") + self.assertEqual(self.results.four_four_one_bar, "bar one") + self.assertEqual(self.results.four_four_two_foo, "foo two") + self.assertEqual(self.results.four_four_two_bar, "bar two") + self.assertItemsEqual(self.results.four_sections, + ["four:one", "four:two"]) + + self.assertEqual(self.results.five_one_foo, "foo one") + self.assertEqual(self.results.five_one_bar, "bar one") + self.assertEqual(self.results.five_two_foo, "foo two") + self.assertEqual(self.results.five_two_bar, "bar two") + self.assertEqual(self.results.five_three_foo, "foo three") + self.assertEqual(self.results.five_three_bar, "bar three") + self.assertItemsEqual(self.results.sections, + ["five:one", "five:two", "five:three"]) diff --git a/testsuite/Testsrc/Testlib/TestOptions/TestOptions.py b/testsuite/Testsrc/Testlib/TestOptions/TestOptions.py new file mode 100644 index 000000000..b76cd6d3a --- /dev/null +++ b/testsuite/Testsrc/Testlib/TestOptions/TestOptions.py @@ -0,0 +1,469 @@ +"""basic option parsing tests.""" + +import argparse +import os +import tempfile + +import mock + +from Bcfg2.Compat import ConfigParser +from Bcfg2.Options import Option, PathOption, BooleanOption, Parser, \ + PositionalArgument, OptionParserException, Common, new_parser, get_parser +from testsuite.Testsrc.Testlib.TestOptions import OptionTestCase, \ + make_config, clean_environment + + +class TestBasicOptions(OptionTestCase): + """test basic option parsing.""" + def setUp(self): + # parsing options can modify the Option objects themselves. + # that's probably bad -- and it's definitely bad if we ever + # want to do real on-the-fly config changes -- but it's easier + # to leave it as is and set the options on each test. + self.options = [ + BooleanOption("--test-true-boolean", env="TEST_TRUE_BOOLEAN", + cf=("test", "true_boolean"), default=True), + BooleanOption("--test-false-boolean", env="TEST_FALSE_BOOLEAN", + cf=("test", "false_boolean"), default=False), + BooleanOption(cf=("test", "true_config_boolean"), + default=True), + BooleanOption(cf=("test", "false_config_boolean"), + default=False), + Option("--test-option", env="TEST_OPTION", cf=("test", "option"), + default="foo"), + PathOption("--test-path-option", env="TEST_PATH_OPTION", + cf=("test", "path"), default="/test")] + + @clean_environment + def _test_options(self, options=None, env=None, config=None): + """helper to test a set of options. + + returns the namespace from parsing the given CLI options with + the given config and environment. + """ + if config is not None: + config = {"test": config} + if options is None: + options = [] + + @make_config(config) + def inner(config_file): + """do the actual tests, since py2.4 lacks context managers.""" + result = argparse.Namespace() + parser = Parser(components=[self], namespace=result) + parser.parse(argv=["-C", config_file] + options) + return result + + if env is not None: + for name, value in env.items(): + os.environ[name] = value + + return inner() + + def test_expand_path(self): + """expand ~ in path option.""" + options = self._test_options(options=["--test-path-option", + "~/test"]) + self.assertEqual(options.test_path_option, + os.path.expanduser("~/test")) + + def test_canonicalize_path(self): + """get absolute path from path option.""" + options = self._test_options(options=["--test-path-option", + "./test"]) + self.assertEqual(options.test_path_option, + os.path.abspath("./test")) + + def test_default_bool(self): + """use the default value of boolean options.""" + options = self._test_options() + self.assertTrue(options.test_true_boolean) + self.assertFalse(options.test_false_boolean) + self.assertTrue(options.true_config_boolean) + self.assertFalse(options.false_config_boolean) + + def test_default(self): + """use the default value of an option.""" + options = self._test_options() + self.assertEqual(options.test_option, "foo") + + def test_default_path(self): + """use the default value of a path option.""" + options = self._test_options() + self.assertEqual(options.test_path_option, "/test") + + def test_invalid_boolean(self): + """set boolean to invalid values.""" + self.assertRaises(ValueError, + self._test_options, + config={"true_boolean": "you betcha"}) + self.assertRaises(ValueError, + self._test_options, + env={"TEST_TRUE_BOOLEAN": "hell no"}) + + def test_set_boolean_in_config(self): + """set boolean options in config files.""" + set_to_defaults = {"true_boolean": "1", + "false_boolean": "0", + "true_config_boolean": "yes", + "false_config_boolean": "no"} + options = self._test_options(config=set_to_defaults) + self.assertTrue(options.test_true_boolean) + self.assertFalse(options.test_false_boolean) + self.assertTrue(options.true_config_boolean) + self.assertFalse(options.false_config_boolean) + + set_to_other = {"true_boolean": "false", + "false_boolean": "true", + "true_config_boolean": "off", + "false_config_boolean": "on"} + options = self._test_options(config=set_to_other) + self.assertFalse(options.test_true_boolean) + self.assertTrue(options.test_false_boolean) + self.assertFalse(options.true_config_boolean) + self.assertTrue(options.false_config_boolean) + + def test_set_in_config(self): + """set options in config files.""" + options = self._test_options(config={"option": "foo"}) + self.assertEqual(options.test_option, "foo") + + options = self._test_options(config={"option": "bar"}) + self.assertEqual(options.test_option, "bar") + + def test_set_path_in_config(self): + """set path options in config files.""" + options = self._test_options(config={"path": "/test"}) + self.assertEqual(options.test_path_option, "/test") + + options = self._test_options(config={"path": "/foo"}) + self.assertEqual(options.test_path_option, "/foo") + + def test_set_boolean_in_env(self): + """set boolean options in environment.""" + set_to_defaults = {"TEST_TRUE_BOOLEAN": "1", + "TEST_FALSE_BOOLEAN": "0"} + options = self._test_options(env=set_to_defaults) + self.assertTrue(options.test_true_boolean) + self.assertFalse(options.test_false_boolean) + + set_to_other = {"TEST_TRUE_BOOLEAN": "false", + "TEST_FALSE_BOOLEAN": "true"} + options = self._test_options(env=set_to_other) + self.assertFalse(options.test_true_boolean) + self.assertTrue(options.test_false_boolean) + + def test_set_in_env(self): + """set options in environment.""" + options = self._test_options(env={"TEST_OPTION": "foo"}) + self.assertEqual(options.test_option, "foo") + + options = self._test_options(env={"TEST_OPTION": "bar"}) + self.assertEqual(options.test_option, "bar") + + def test_set_path_in_env(self): + """set path options in environment.""" + options = self._test_options(env={"TEST_PATH_OPTION": "/test"}) + self.assertEqual(options.test_path_option, "/test") + + options = self._test_options(env={"TEST_PATH_OPTION": "/foo"}) + self.assertEqual(options.test_path_option, "/foo") + + def test_set_boolean_in_cli(self): + """set boolean options in CLI options.""" + # passing the option yields the reverse of the default, no + # matter the default + options = self._test_options(options=["--test-true-boolean", + "--test-false-boolean"]) + self.assertFalse(options.test_true_boolean) + self.assertTrue(options.test_false_boolean) + + def test_set_in_cli(self): + """set options in CLI options.""" + options = self._test_options(options=["--test-option", "foo"]) + self.assertEqual(options.test_option, "foo") + + options = self._test_options(options=["--test-option", "bar"]) + self.assertEqual(options.test_option, "bar") + + def test_set_path_in_cli(self): + """set path options in CLI options.""" + options = self._test_options(options=["--test-path-option", "/test"]) + self.assertEqual(options.test_path_option, "/test") + + options = self._test_options(options=["--test-path-option", "/foo"]) + self.assertEqual(options.test_path_option, "/foo") + + def test_env_overrides_config_bool(self): + """setting boolean option in the environment overrides config file.""" + config = {"true_boolean": "false", + "false_boolean": "true"} + env = {"TEST_TRUE_BOOLEAN": "yes", + "TEST_FALSE_BOOLEAN": "no"} + options = self._test_options(config=config, env=env) + self.assertTrue(options.test_true_boolean) + self.assertFalse(options.test_false_boolean) + + def test_env_overrides_config(self): + """setting option in the environment overrides config file.""" + options = self._test_options(config={"option": "bar"}, + env={"TEST_OPTION": "baz"}) + self.assertEqual(options.test_option, "baz") + + def test_env_overrides_config_path(self): + """setting path option in the environment overrides config file.""" + options = self._test_options(config={"path": "/foo"}, + env={"TEST_PATH_OPTION": "/bar"}) + self.assertEqual(options.test_path_option, "/bar") + + def test_cli_overrides_config_bool(self): + """setting boolean option in the CLI overrides config file.""" + config = {"true_boolean": "on", + "false_boolean": "off"} + options = ["--test-true-boolean", "--test-false-boolean"] + options = self._test_options(options=options, config=config) + self.assertFalse(options.test_true_boolean) + self.assertTrue(options.test_false_boolean) + + def test_cli_overrides_config(self): + """setting option in the CLI overrides config file.""" + options = self._test_options(options=["--test-option", "baz"], + config={"option": "bar"}) + self.assertEqual(options.test_option, "baz") + + def test_cli_overrides_config_path(self): + """setting path option in the CLI overrides config file.""" + options = self._test_options(options=["--test-path-option", "/bar"], + config={"path": "/foo"}) + self.assertEqual(options.test_path_option, "/bar") + + def test_cli_overrides_env_bool(self): + """setting boolean option in the CLI overrides environment.""" + env = {"TEST_TRUE_BOOLEAN": "0", + "TEST_FALSE_BOOLEAN": "1"} + options = ["--test-true-boolean", "--test-false-boolean"] + options = self._test_options(options=options, env=env) + self.assertFalse(options.test_true_boolean) + self.assertTrue(options.test_false_boolean) + + def test_cli_overrides_env(self): + """setting option in the CLI overrides environment.""" + options = self._test_options(options=["--test-option", "baz"], + env={"TEST_OPTION": "bar"}) + self.assertEqual(options.test_option, "baz") + + def test_cli_overrides_env_path(self): + """setting path option in the CLI overrides environment.""" + options = self._test_options(options=["--test-path-option", "/bar"], + env={"TEST_PATH_OPTION": "/foo"}) + self.assertEqual(options.test_path_option, "/bar") + + def test_cli_overrides_all_bool(self): + """setting boolean option in the CLI overrides everything else.""" + config = {"true_boolean": "no", + "false_boolean": "yes"} + env = {"TEST_TRUE_BOOLEAN": "0", + "TEST_FALSE_BOOLEAN": "1"} + options = ["--test-true-boolean", "--test-false-boolean"] + options = self._test_options(options=options, env=env) + self.assertFalse(options.test_true_boolean) + self.assertTrue(options.test_false_boolean) + + def test_cli_overrides_all(self): + """setting option in the CLI overrides everything else.""" + options = self._test_options(options=["--test-option", "baz"], + env={"TEST_OPTION": "bar"}, + config={"test": "quux"}) + self.assertEqual(options.test_option, "baz") + + def test_cli_overrides_all_path(self): + """setting path option in the CLI overrides everything else.""" + options = self._test_options(options=["--test-path-option", "/bar"], + env={"TEST_PATH_OPTION": "/foo"}, + config={"path": "/baz"}) + self.assertEqual(options.test_path_option, "/bar") + + @make_config() + def _test_dest(self, *args, **kwargs): + """helper to test that ``dest`` is set properly.""" + args = list(args) + expected = args.pop(0) + config_file = args.pop() + + sentinel = object() + kwargs["default"] = sentinel + + result = argparse.Namespace() + parser = Parser(namespace=result) + parser.add_options([Option(*args, **kwargs)]) + parser.parse(["-C", config_file]) + + self.assertTrue(hasattr(result, expected)) + self.assertEqual(getattr(result, expected), sentinel) + + def test_explicit_dest(self): + """set the ``dest`` of an option explicitly.""" + self._test_dest("bar", dest="bar") + + def test_dest_from_env_var(self): + """set the ``dest`` of an option from the env var name.""" + self._test_dest("foo", env="FOO") + + def test_dest_from_cf(self): + """set the ``dest`` of an option from the config option.""" + self._test_dest("foo_bar", cf=("test", "foo-bar")) + + def test_dest_from_cli(self): + """set the ``dest`` of an option from the CLI option.""" + self._test_dest("test_foo", "--test-foo") + + def test_dest_from_all(self): + """set the ``dest`` of an option from the best of multiple sources.""" + self._test_dest("foo_baz", cf=("test", "foo-bar"), env="FOO_BAZ") + self._test_dest("xyzzy", + "--xyzzy", cf=("test", "foo-bar"), env="FOO_BAZ") + self._test_dest("quux", + "--xyzzy", cf=("test", "foo-bar"), env="FOO_BAZ", + dest="quux") + + @make_config() + def test_positional_args(self, config_file): + """get values from positional arguments.""" + result = argparse.Namespace() + parser = Parser(namespace=result) + parser.add_options([PositionalArgument("single")]) + parser.parse(["-C", config_file, "single"]) + self.assertEqual(result.single, "single") + + result = argparse.Namespace() + parser = Parser(namespace=result) + parser.add_options([PositionalArgument("one"), + PositionalArgument("two")]) + parser.parse(["-C", config_file, "one", "two"]) + self.assertEqual(result.one, "one") + self.assertEqual(result.two, "two") + + def test_duplicate_cli_option(self): + """add duplicate CLI option.""" + parser = Parser(components=[self]) + self.assertRaises( + argparse.ArgumentError, + parser.add_options, + [Option("--test-option")]) + + def test_duplicate_env_option(self): + """add duplicate environment option.""" + parser = Parser(components=[self]) + self.assertRaises( + OptionParserException, + parser.add_options, + [Option(env="TEST_OPTION")]) + + def test_duplicate_cf_option(self): + """add duplicate config file option.""" + parser = Parser(components=[self]) + self.assertRaises( + OptionParserException, + parser.add_options, + [Option(cf=("test", "option"))]) + + @make_config() + def test_repository_macro(self, config_file): + """fix up macros.""" + result = argparse.Namespace() + parser = Parser(namespace=result) + parser.add_options([PathOption("--test1"), + PathOption("--test2"), + Common.repository]) + parser.parse(["-C", config_file, "-Q", "/foo/bar", + "--test1", "/test1", + "--test2", ""]) + self.assertEqual(result.repository, "/foo/bar") + self.assertEqual(result.test1, "/foo/bar/test1") + self.assertEqual(result.test2, "/foo/bar/foo/bar") + + @make_config() + def test_file_like_path_option(self, config_file): + """get file-like object from PathOption.""" + result = argparse.Namespace() + parser = Parser(namespace=result) + parser.add_options([PathOption("--test", type=argparse.FileType('r'))]) + + fd, name = tempfile.mkstemp() + fh = os.fdopen(fd, "w") + fh.write("test") + fh.close() + + parser.parse(["-C", config_file, "--test", name]) + self.assertEqual(result.test.name, name) + self.assertEqual(result.test.read(), "test") + + @clean_environment + @make_config() + def test_unknown_options(self, config_file): + """error on unknown options.""" + parser = Parser(components=[self]) + self.assertRaises(SystemExit, + parser.parse, + ["-C", config_file, "--not-a-real-option"]) + + @clean_environment + @make_config() + def test_reparse(self, config_file): + """reparse options.""" + result = argparse.Namespace() + parser = Parser(components=[self], namespace=result) + parser.parse(["-C", config_file]) + self.assertFalse(result.test_false_boolean) + + parser.parse(["-C", config_file]) + self.assertFalse(result.test_false_boolean) + + parser.reparse() + self.assertFalse(result.test_false_boolean) + + parser.reparse(["-C", config_file, "--test-false-boolean"]) + self.assertTrue(result.test_false_boolean) + + cfp = ConfigParser.ConfigParser() + cfp.add_section("test") + cfp.set("test", "false_boolean", "on") + parser.parse(["-C", config_file]) + cfp.write(open(config_file, "w")) + self.assertTrue(result.test_false_boolean) + + +class TestParsingHooks(OptionTestCase): + """test option parsing hooks.""" + def setUp(self): + self.options_parsed_hook = mock.Mock() + self.options = [BooleanOption("--test", default=False)] + self.results = argparse.Namespace() + new_parser() + self.parser = get_parser(components=[self], namespace=self.results) + + @make_config() + def test_parsing_hooks(self, config_file): + """option parsing hooks are called.""" + self.parser.parse(["-C", config_file]) + self.options_parsed_hook.assert_called_with() + + +class TestEarlyParsingHooks(OptionTestCase): + """test early option parsing hooks.""" + parse_first = True + + def setUp(self): + self.component_parsed_hook = mock.Mock() + self.options = [BooleanOption("--early-test", default=False)] + self.results = argparse.Namespace() + new_parser() + self.parser = get_parser(components=[self], namespace=self.results) + + @make_config() + def test_parsing_hooks(self, config_file): + """early option parsing hooks are called.""" + self.parser.parse(["-C", config_file, "--early-test"]) + self.assertEqual(self.component_parsed_hook.call_count, 1) + early_opts = self.component_parsed_hook.call_args[0][0] + self.assertTrue(early_opts.early_test) diff --git a/testsuite/Testsrc/Testlib/TestOptions/TestSubcommands.py b/testsuite/Testsrc/Testlib/TestOptions/TestSubcommands.py new file mode 100644 index 000000000..35da909cb --- /dev/null +++ b/testsuite/Testsrc/Testlib/TestOptions/TestSubcommands.py @@ -0,0 +1,141 @@ +"""test subcommand option parsing.""" + +import argparse +import sys + +from Bcfg2.Compat import StringIO +from Bcfg2.Options import Option, get_parser, new_parser, Subcommand, \ + Subparser, CommandRegistry +import Bcfg2.Options.Subcommands + +from testsuite.Testsrc.Testlib.TestOptions import make_config, OptionTestCase + + +class MockSubcommand(Subcommand): + """fake subcommand that just records the options it was called with.""" + run_options = None + + def run(self, setup): + self.__class__.run_options = setup + + +class One(MockSubcommand): + """fake subcommand for testing.""" + options = [Option("--test-one")] + + +class Two(MockSubcommand): + """fake subcommand for testing.""" + options = [Option("--test-two")] + + +def local_subclass(cls): + """get a subclass of ``cls`` that adds no functionality. + + This can be used to subclass the various test classes above so + that their options don't get modified by option parsing. + """ + return type("Local%s" % cls.__name__, (cls,), {}) + + +class TestSubcommands(OptionTestCase): + """tests for subcommands and subparsers.""" + + def setUp(self): + self.registry = CommandRegistry() + + self.one = local_subclass(One) + self.two = local_subclass(Two) + + self.registry.register_command(self.one) + self.registry.register_command(self.two) + + self.result = argparse.Namespace() + Bcfg2.Options.Subcommands.master_setup = self.result + + new_parser() + self.parser = get_parser(namespace=self.result, + components=[self]) + self.parser.add_options(self.registry.subcommand_options) + + def test_register_commands(self): + """register subcommands.""" + registry = CommandRegistry() + registry.register_commands(globals().values(), + parent=MockSubcommand) + self.assertItemsEqual(registry.commands.keys(), + ["one", "two", "help"]) + self.assertIsInstance(registry.commands['one'], One) + self.assertIsInstance(registry.commands['two'], Two) + + @make_config() + def test_get_subcommand(self, config_file): + """parse simple subcommands.""" + self.parser.parse(["-C", config_file, "localone"]) + self.assertEqual(self.result.subcommand, "localone") + + def test_subcommand_usage(self): + """sane usage message from subcommands.""" + self.assertEqual( + One().usage(), + "one [--test-one TEST_ONE] - fake subcommand for testing.") + + # subclasses do not inherit the docstring from the parent, so + # this tests a command subclass without a docstring, even + # though that should never happen due to the pylint tests. + self.assertEqual(self.one().usage().strip(), + "localone [--test-one TEST_ONE]") + + @make_config() + def test_help(self, config_file): + """sane help message from subcommand registry.""" + self.parser.parse(["-C", config_file, "help"]) + old_stdout = sys.stdout + sys.stdout = StringIO() + self.assertIn(self.registry.runcommand(), [0, None]) + help_message = sys.stdout.getvalue().splitlines() + sys.stdout = old_stdout + + # the help message will look like: + # + # localhelp [] + # localone [--test-one TEST_ONE] + # localtwo [--test-two TEST_TWO] + commands = [] + command_help = { + "help": self.registry.help.usage(), + "localone": self.one().usage(), + "localtwo": self.two().usage()} + for line in help_message: + command = line.split()[0] + commands.append(command) + if command not in command_help: + self.fail("Got help for unknown command %s: %s" % + (command, line)) + self.assertEqual(line, command_help[command]) + self.assertItemsEqual(commands, command_help.keys()) + + @make_config() + def test_subcommand_help(self, config_file): + """get help message on a single command.""" + self.parser.parse(["-C", config_file, "help", "localone"]) + old_stdout = sys.stdout + sys.stdout = StringIO() + self.assertIn(self.registry.runcommand(), [0, None]) + help_message = sys.stdout.getvalue().splitlines() + sys.stdout = old_stdout + + self.assertEqual(help_message[0].strip(), + "usage: %s" % self.one().usage().strip()) + + @make_config() + def test_nonexistent_subcommand_help(self, config_file): + """get help message on a nonexistent command.""" + self.parser.parse(["-C", config_file, "help", "blargle"]) + old_stdout = sys.stdout + sys.stdout = StringIO() + self.assertNotEqual(self.registry.runcommand(), 0) + help_message = sys.stdout.getvalue().splitlines() + sys.stdout = old_stdout + + self.assertIn("No such command", help_message[0]) diff --git a/testsuite/Testsrc/Testlib/TestOptions/TestTypes.py b/testsuite/Testsrc/Testlib/TestOptions/TestTypes.py new file mode 100644 index 000000000..404d67fdc --- /dev/null +++ b/testsuite/Testsrc/Testlib/TestOptions/TestTypes.py @@ -0,0 +1,111 @@ +"""test builtin option types.""" + +import argparse + +from mock import patch + +from Bcfg2.Options import Option, Types, Parser +from testsuite.common import Bcfg2TestCase + + +class TestOptionTypes(Bcfg2TestCase): + """test builtin option types.""" + def setUp(self): + self.options = None + + def _test_options(self, options): + """helper to test option types. + + this expects that self.options is set to a single option named + test. The value of that option is returned. + """ + result = argparse.Namespace() + parser = Parser(components=[self], namespace=result) + parser.parse(options) + return result.test + + def test_comma_list(self): + """parse comma-list values.""" + self.options = [Option("--test", type=Types.comma_list)] + + expected = ["one", "two", "three"] + self.assertItemsEqual(self._test_options(["--test", "one,two,three"]), + expected) + self.assertItemsEqual(self._test_options(["--test", + "one, two, three"]), + expected) + self.assertItemsEqual(self._test_options(["--test", + "one , two ,three"]), + expected) + self.assertItemsEqual(self._test_options(["--test", "one two, three"]), + ["one two", "three"]) + + def test_colon_list(self): + """parse colon-list values.""" + self.options = [Option("--test", type=Types.colon_list)] + self.assertItemsEqual(self._test_options(["--test", "one:two three"]), + ["one", "two three"]) + + def test_comma_dict(self): + """parse comma-dict values.""" + self.options = [Option("--test", type=Types.comma_dict)] + expected = { + "one": True, + "two": 2, + "three": "three", + "four": False} + self.assertDictEqual( + self._test_options(["--test", + "one=yes, two=2 , three=three,four=no"]), + expected) + + self.assertDictEqual( + self._test_options(["--test", "one,two=2,three=three,four=off"]), + expected) + + def test_anchored_regex_list(self): + """parse regex lists.""" + self.options = [Option("--test", type=Types.anchored_regex_list)] + self.assertItemsEqual( + [r.pattern for r in self._test_options(["--test", r'\d+ \s*'])], + [r'^\d+$', r'^\s*$']) + self.assertRaises(SystemExit, + self._test_options, ["--test", '(]']) + + def test_octal(self): + """parse octal options.""" + self.options = [Option("--test", type=Types.octal)] + self.assertEqual(self._test_options(["--test", "0777"]), 511) + self.assertEqual(self._test_options(["--test", "133114255"]), 23894189) + + @patch("pwd.getpwnam") + def test_username(self, mock_getpwnam): + """parse username options.""" + self.options = [Option("--test", type=Types.username)] + mock_getpwnam.return_value = ("test", '********', 1001, 1001, + "Test user", "/home/test", "/bin/bash") + self.assertEqual(self._test_options(["--test", "1001"]), 1001) + self.assertEqual(self._test_options(["--test", "test"]), 1001) + + @patch("grp.getgrnam") + def test_groupname(self, mock_getpwnam): + """parse group name options.""" + self.options = [Option("--test", type=Types.groupname)] + mock_getpwnam.return_value = ("test", '*', 1001, ["test"]) + self.assertEqual(self._test_options(["--test", "1001"]), 1001) + self.assertEqual(self._test_options(["--test", "test"]), 1001) + + def test_timeout(self): + """parse timeout options.""" + self.options = [Option("--test", type=Types.timeout)] + self.assertEqual(self._test_options(["--test", "1.0"]), 1.0) + self.assertEqual(self._test_options(["--test", "1"]), 1.0) + self.assertEqual(self._test_options(["--test", "0"]), None) + + def test_size(self): + """parse human-readable size options.""" + self.options = [Option("--test", type=Types.size)] + self.assertEqual(self._test_options(["--test", "5k"]), 5120) + self.assertEqual(self._test_options(["--test", "5"]), 5) + self.assertRaises(SystemExit, + self._test_options, ["--test", "g5m"]) diff --git a/testsuite/Testsrc/Testlib/TestOptions/TestWildcards.py b/testsuite/Testsrc/Testlib/TestOptions/TestWildcards.py new file mode 100644 index 000000000..da196a912 --- /dev/null +++ b/testsuite/Testsrc/Testlib/TestOptions/TestWildcards.py @@ -0,0 +1,47 @@ +"""test wildcard options.""" + +import argparse + +from Bcfg2.Options import Option, Parser +from testsuite.Testsrc.Testlib.TestOptions import OptionTestCase, make_config + + +class TestWildcardOptions(OptionTestCase): + """test parsing wildcard options.""" + config = { + "foo": { + "test1": "test1", + "test2": "test2", + "thing1": "thing1", + "thing2": "thing2", + "foo": "foo" + } + } + + def setUp(self): + # parsing options can modify the Option objects themselves. + # that's probably bad -- and it's definitely bad if we ever + # want to do real on-the-fly config changes -- but it's easier + # to leave it as is and set the options on each test. + self.options = [ + Option(cf=("foo", "*"), dest="all"), + Option(cf=("foo", "test*"), dest="test"), + Option(cf=("foo", "bogus*"), dest="unmatched"), + Option(cf=("bar", "*"), dest="no_section"), + Option(cf=("foo", "foo"))] + + @make_config(config) + def test_wildcard_options(self, config_file): + """parse wildcard options.""" + result = argparse.Namespace() + parser = Parser(components=[self], namespace=result) + parser.parse(argv=["-C", config_file]) + + self.assertDictEqual(result.all, {"test1": "test1", + "test2": "test2", + "thing1": "thing1", + "thing2": "thing2"}) + self.assertDictEqual(result.test, {"test1": "test1", + "test2": "test2"}) + self.assertDictEqual(result.unmatched, {}) + self.assertDictEqual(result.no_section, {}) diff --git a/testsuite/Testsrc/Testlib/TestOptions/Two.py b/testsuite/Testsrc/Testlib/TestOptions/Two.py new file mode 100644 index 000000000..189e0817f --- /dev/null +++ b/testsuite/Testsrc/Testlib/TestOptions/Two.py @@ -0,0 +1,6 @@ +"""Test module for component loading.""" + + +class Two(object): + """Test class for component loading.""" + pass diff --git a/testsuite/Testsrc/Testlib/TestOptions/__init__.py b/testsuite/Testsrc/Testlib/TestOptions/__init__.py new file mode 100644 index 000000000..b051f65e5 --- /dev/null +++ b/testsuite/Testsrc/Testlib/TestOptions/__init__.py @@ -0,0 +1,85 @@ +"""helper functions for option testing.""" + +import os +import tempfile + +from Bcfg2.Compat import wraps, ConfigParser +from Bcfg2.Options import Parser +from testsuite.common import Bcfg2TestCase + + +class make_config(object): # pylint: disable=invalid-name + """decorator to create a temporary config file from a dict. + + The filename of the temporary config file is added as the last + positional argument to the function call. + """ + def __init__(self, config_data=None): + self.config_data = config_data or {} + + def __call__(self, func): + @wraps(func) + def inner(*args, **kwargs): + """decorated function.""" + cfp = ConfigParser.ConfigParser() + for section, options in self.config_data.items(): + cfp.add_section(section) + for key, val in options.items(): + cfp.set(section, key, val) + fd, name = tempfile.mkstemp() + config_file = os.fdopen(fd, 'w') + cfp.write(config_file) + config_file.close() + + args = list(args) + [name] + rv = func(*args, **kwargs) + os.unlink(name) + return rv + + return inner + + +def clean_environment(func): + """decorator that unsets any environment variables used by options. + + The list of options is taken from the first argument, which is + presumed to be ``self``. The variables are restored at the end of + the function. + """ + @wraps(func) + def inner(self, *args, **kwargs): + """decorated function.""" + envvars = {} + for opt in self.options: + if opt.env is not None: + envvars[opt.env] = os.environ.get(opt.env) + if opt.env in os.environ: + del os.environ[opt.env] + rv = func(self, *args, **kwargs) + for name, val in envvars.items(): + if val is None and name in os.environ: + del os.environ[name] + elif val is not None: + os.environ[name] = val + return rv + + return inner + + +class OptionTestCase(Bcfg2TestCase): + """test case that doesn't mock out config file reading.""" + + @classmethod + def setUpClass(cls): + # ensure that the option parser actually reads config files + Parser.unit_test = False + + @classmethod + def tearDownClass(cls): + Parser.unit_test = True + + + +# TODO: +# * subcommands +# * common options diff --git a/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testbase.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testbase.py index f135a0197..290a7c092 100644 --- a/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testbase.py +++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testbase.py @@ -22,6 +22,7 @@ class TestPlugin(TestDebuggable): def setUp(self): TestDebuggable.setUp(self) set_setup_default("filemonitor", MagicMock()) + set_setup_default("repository", datastore) def get_obj(self, core=None): if core is None: diff --git a/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testhelpers.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testhelpers.py index 37beaa26c..5a82100d0 100644 --- a/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testhelpers.py +++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testhelpers.py @@ -55,6 +55,7 @@ class TestDatabaseBacked(TestPlugin): def setUp(self): TestPlugin.setUp(self) set_setup_default("%s_db" % self.test_obj.__name__.lower(), False) + set_setup_default("db_engine", None) @skipUnless(HAS_DJANGO, "Django not found") def test__use_db(self): diff --git a/testsuite/Testsrc/Testlib/TestUtils.py b/testsuite/Testsrc/Testlib/TestUtils.py index 349d6cd40..4bed67248 100644 --- a/testsuite/Testsrc/Testlib/TestUtils.py +++ b/testsuite/Testsrc/Testlib/TestUtils.py @@ -1,9 +1,5 @@ import os import sys -import copy -import lxml.etree -import subprocess -from mock import Mock, MagicMock, patch from Bcfg2.Utils import * # add all parent testsuite directories to sys.path to allow (most) diff --git a/testsuite/Testsrc/__init__.py b/testsuite/Testsrc/__init__.py new file mode 100644 index 000000000..e69de29bb -- cgit v1.2.3-1-g7c22 From 7a763f1ca474203e07379fe2e71606b01c5b62fb Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Tue, 14 Oct 2014 09:40:24 -0500 Subject: testsuite: skip nested exclusive option group test on py2.6 --- .../Testsrc/Testlib/TestOptions/TestOptionGroups.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) (limited to 'testsuite') diff --git a/testsuite/Testsrc/Testlib/TestOptions/TestOptionGroups.py b/testsuite/Testsrc/Testlib/TestOptions/TestOptionGroups.py index de1abbb1b..7611d6202 100644 --- a/testsuite/Testsrc/Testlib/TestOptions/TestOptionGroups.py +++ b/testsuite/Testsrc/Testlib/TestOptions/TestOptionGroups.py @@ -1,11 +1,12 @@ """test reading multiple config files.""" import argparse +import sys from Bcfg2.Options import Option, BooleanOption, Parser, OptionGroup, \ ExclusiveOptionGroup, WildcardSectionGroup, new_parser, get_parser -from testsuite.common import Bcfg2TestCase +from testsuite.common import Bcfg2TestCase, skipUnless from testsuite.Testsrc.Testlib.TestOptions import make_config, OptionTestCase @@ -59,8 +60,10 @@ class TestOptionGroups(Bcfg2TestCase): self.assertRaises(SystemExit, self._test_options, []) - def test_option_group(self): - """nest option groups.""" + +class TestNestedOptionGroups(TestOptionGroups): + def setUp(self): + TestOptionGroups.setUp(self) self.options = [ OptionGroup( BooleanOption("--foo"), @@ -73,6 +76,9 @@ class TestOptionGroups(Bcfg2TestCase): BooleanOption("--test2")), title="inner"), title="outer")] + + def test_option_group(self): + """nest option groups.""" result = self._test_options(["--foo", "--baz", "--test1"]) self.assertTrue(result.foo) self.assertFalse(result.bar) @@ -81,6 +87,10 @@ class TestOptionGroups(Bcfg2TestCase): self.assertTrue(result.test1) self.assertFalse(result.test2) + @skipUnless(sys.version_info >= (2, 7), + "Nested exclusive option groups do not work in Python 2.6") + def test_nested_exclusive_option_groups(self): + """nest exclusive option groups.""" self.assertRaises(SystemExit, self._test_options, ["--test1", "--test2"]) -- cgit v1.2.3-1-g7c22 From a73d70b7fa79b988c97e8c258bc2b6c29224bf01 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Wed, 15 Oct 2014 12:41:31 -0700 Subject: Test failure to parse config file when bcfg2.conf exists --- testsuite/Testsrc/Testlib/TestOptions/TestConfigFiles.py | 4 +++- testsuite/Testsrc/Testlib/TestOptions/__init__.py | 6 ------ 2 files changed, 3 insertions(+), 7 deletions(-) (limited to 'testsuite') diff --git a/testsuite/Testsrc/Testlib/TestOptions/TestConfigFiles.py b/testsuite/Testsrc/Testlib/TestOptions/TestConfigFiles.py index aee2ff666..78acadf1f 100644 --- a/testsuite/Testsrc/Testlib/TestOptions/TestConfigFiles.py +++ b/testsuite/Testsrc/Testlib/TestOptions/TestConfigFiles.py @@ -2,9 +2,10 @@ import argparse +import mock + from Bcfg2.Options import Option, PathOption, ConfigFileAction, get_parser, \ new_parser - from testsuite.Testsrc.Testlib.TestOptions import make_config, OptionTestCase @@ -43,6 +44,7 @@ class TestConfigFiles(OptionTestCase): inner1() + @mock.patch("os.path.exists", mock.Mock(return_value=False)) def test_no_config_file(self): """fail to read config file.""" self.assertRaises(SystemExit, self.parser.parse, []) diff --git a/testsuite/Testsrc/Testlib/TestOptions/__init__.py b/testsuite/Testsrc/Testlib/TestOptions/__init__.py index b051f65e5..00e250356 100644 --- a/testsuite/Testsrc/Testlib/TestOptions/__init__.py +++ b/testsuite/Testsrc/Testlib/TestOptions/__init__.py @@ -77,9 +77,3 @@ class OptionTestCase(Bcfg2TestCase): @classmethod def tearDownClass(cls): Parser.unit_test = True - - - -# TODO: -# * subcommands -# * common options -- cgit v1.2.3-1-g7c22 From 9c08059110a62a926128c5f2ec0b726ef1083822 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Wed, 15 Oct 2014 12:46:51 -0700 Subject: testsuite: capture stderr by default This quiets down a lot of tests, especially for option parsing. --- testsuite/Testsrc/Testlib/TestOptions/__init__.py | 2 ++ testsuite/common.py | 13 +++++++++++++ 2 files changed, 15 insertions(+) (limited to 'testsuite') diff --git a/testsuite/Testsrc/Testlib/TestOptions/__init__.py b/testsuite/Testsrc/Testlib/TestOptions/__init__.py index 00e250356..688f4e54c 100644 --- a/testsuite/Testsrc/Testlib/TestOptions/__init__.py +++ b/testsuite/Testsrc/Testlib/TestOptions/__init__.py @@ -73,7 +73,9 @@ class OptionTestCase(Bcfg2TestCase): def setUpClass(cls): # ensure that the option parser actually reads config files Parser.unit_test = False + Bcfg2TestCase.setUpClass() @classmethod def tearDownClass(cls): Parser.unit_test = True + Bcfg2TestCase.tearDownClass() diff --git a/testsuite/common.py b/testsuite/common.py index 5a08f8db5..49579d7ef 100644 --- a/testsuite/common.py +++ b/testsuite/common.py @@ -119,6 +119,19 @@ class Bcfg2TestCase(TestCase): :func:`assertXMLEqual`, a useful assertion method given all the XML used by Bcfg2. """ + capture_stderr = True + + @classmethod + def setUpClass(cls): + cls._stderr = sys.stderr + if cls.capture_stderr: + sys.stderr = sys.stdout + + @classmethod + def tearDownClass(cls): + if cls.capture_stderr: + sys.stderr = cls._stderr + def assertXMLEqual(self, el1, el2, msg=None): """ Test that the two XML trees given are equal. """ if msg is None: -- cgit v1.2.3-1-g7c22 From 474462ff24e8f29d715ee80ee0c07acb16eb1dab Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Tue, 16 Sep 2014 15:50:04 -0700 Subject: testsuite: Added unit tests for new option parsing --- testsuite/Testsrc/Testlib/TestClient/TestTools/Test_init.py | 1 + 1 file changed, 1 insertion(+) (limited to 'testsuite') diff --git a/testsuite/Testsrc/Testlib/TestClient/TestTools/Test_init.py b/testsuite/Testsrc/Testlib/TestClient/TestTools/Test_init.py index b9de703ff..047905fc3 100644 --- a/testsuite/Testsrc/Testlib/TestClient/TestTools/Test_init.py +++ b/testsuite/Testsrc/Testlib/TestClient/TestTools/Test_init.py @@ -21,6 +21,7 @@ from common import * class TestTool(Bcfg2TestCase): test_obj = Tool + # try to find true if os.path.exists("/bin/true"): true = "/bin/true" elif os.path.exists("/usr/bin/true"): -- cgit v1.2.3-1-g7c22 From 7a4dd4b3436cd85ee46acd07e85e7769f739b87f Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Wed, 22 Oct 2014 11:02:51 -0500 Subject: testsuite: better debug capturing for options tests --- .../Testsrc/Testlib/TestOptions/TestSubcommands.py | 44 +++++++++++----------- testsuite/common.py | 6 +++ 2 files changed, 27 insertions(+), 23 deletions(-) (limited to 'testsuite') diff --git a/testsuite/Testsrc/Testlib/TestOptions/TestSubcommands.py b/testsuite/Testsrc/Testlib/TestOptions/TestSubcommands.py index 35da909cb..65b4c19c0 100644 --- a/testsuite/Testsrc/Testlib/TestOptions/TestSubcommands.py +++ b/testsuite/Testsrc/Testlib/TestOptions/TestSubcommands.py @@ -86,15 +86,21 @@ class TestSubcommands(OptionTestCase): self.assertEqual(self.one().usage().strip(), "localone [--test-one TEST_ONE]") - @make_config() - def test_help(self, config_file): - """sane help message from subcommand registry.""" - self.parser.parse(["-C", config_file, "help"]) + def _get_subcommand_output(self, args): + self.parser.parse(args) old_stdout = sys.stdout sys.stdout = StringIO() - self.assertIn(self.registry.runcommand(), [0, None]) - help_message = sys.stdout.getvalue().splitlines() + rv = self.registry.runcommand() + output = [l for l in sys.stdout.getvalue().splitlines() + if not l.startswith("DEBUG: ")] sys.stdout = old_stdout + return (rv, output) + + @make_config() + def test_help(self, config_file): + """sane help message from subcommand registry.""" + rv, output = self._get_subcommand_output(["-C", config_file, "help"]) + self.assertIn(rv, [0, None]) # the help message will look like: # @@ -106,7 +112,7 @@ class TestSubcommands(OptionTestCase): "help": self.registry.help.usage(), "localone": self.one().usage(), "localtwo": self.two().usage()} - for line in help_message: + for line in output: command = line.split()[0] commands.append(command) if command not in command_help: @@ -118,24 +124,16 @@ class TestSubcommands(OptionTestCase): @make_config() def test_subcommand_help(self, config_file): """get help message on a single command.""" - self.parser.parse(["-C", config_file, "help", "localone"]) - old_stdout = sys.stdout - sys.stdout = StringIO() - self.assertIn(self.registry.runcommand(), [0, None]) - help_message = sys.stdout.getvalue().splitlines() - sys.stdout = old_stdout - - self.assertEqual(help_message[0].strip(), + rv, output = self._get_subcommand_output( + ["-C", config_file, "help", "localone"]) + self.assertIn(rv, [0, None]) + self.assertEqual(output[0].strip(), "usage: %s" % self.one().usage().strip()) @make_config() def test_nonexistent_subcommand_help(self, config_file): """get help message on a nonexistent command.""" - self.parser.parse(["-C", config_file, "help", "blargle"]) - old_stdout = sys.stdout - sys.stdout = StringIO() - self.assertNotEqual(self.registry.runcommand(), 0) - help_message = sys.stdout.getvalue().splitlines() - sys.stdout = old_stdout - - self.assertIn("No such command", help_message[0]) + rv, output = self._get_subcommand_output( + ["-C", config_file, "help", "blargle"]) + self.assertNotEqual(rv, 0) + self.assertIn("No such command", output[0]) diff --git a/testsuite/common.py b/testsuite/common.py index 49579d7ef..a86e9c5d9 100644 --- a/testsuite/common.py +++ b/testsuite/common.py @@ -38,7 +38,13 @@ def set_setup_default(option, value=None): if not hasattr(Bcfg2.Options.setup, option): setattr(Bcfg2.Options.setup, option, value) +# these two variables do slightly different things for unit tests; the +# former skips config file reading, while the latter sends option +# debug logging to stdout so it can be captured. These are separate +# because we want to enable config file reading in order to test +# option parsing. Bcfg2.Options.Parser.unit_test = True +Bcfg2.Options.Options.unit_test = True try: import django.conf -- cgit v1.2.3-1-g7c22 From 58caed11b409905641913f545f3a87280705f1a6 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Wed, 22 Oct 2014 11:03:48 -0500 Subject: Options: ensure macros are always fixed up This fixes several cases in which macros would not be properly processed: options that are not added to the parser yet when early options are parsed; and config file options whose default value is used. --- .../Testsrc/Testlib/TestOptions/TestComponents.py | 25 +++++++++++++++++++--- .../Testsrc/Testlib/TestOptions/TestOptions.py | 8 ++++++- testsuite/Testsrc/Testlib/TestOptions/__init__.py | 7 ++++-- 3 files changed, 34 insertions(+), 6 deletions(-) (limited to 'testsuite') diff --git a/testsuite/Testsrc/Testlib/TestOptions/TestComponents.py b/testsuite/Testsrc/Testlib/TestOptions/TestComponents.py index d637d34c5..61b87de2a 100644 --- a/testsuite/Testsrc/Testlib/TestOptions/TestComponents.py +++ b/testsuite/Testsrc/Testlib/TestOptions/TestComponents.py @@ -3,8 +3,8 @@ import argparse import os -from Bcfg2.Options import Option, BooleanOption, ComponentAction, get_parser, \ - new_parser, Types, ConfigFileAction +from Bcfg2.Options import Option, BooleanOption, PathOption, ComponentAction, \ + get_parser, new_parser, Types, ConfigFileAction, Common from testsuite.Testsrc.Testlib.TestOptions import make_config, One, Two, \ OptionTestCase @@ -51,18 +51,27 @@ class ConfigFileComponent(object): default="bar")] +class PathComponent(object): + """fake component for testing macros in child components.""" + options = [PathOption(cf=("test", "test_path")), + PathOption(cf=("test", "test_path_default"), + default="/test/default")] + + class ParentComponentAction(ComponentAction): """parent component loader action.""" mapping = {"one": ComponentOne, "two": ComponentTwo, "three": ComponentThree, - "config": ConfigFileComponent} + "config": ConfigFileComponent, + "path": PathComponent} class TestComponentOptions(OptionTestCase): """test cases for component loading.""" def setUp(self): + OptionTestCase.setUp(self) self.options = [ Option("--parent", type=Types.comma_list, default=["one", "two"], action=ParentComponentAction)] @@ -147,6 +156,16 @@ class TestComponentOptions(OptionTestCase): self.parser.parse(["-C", config_file, "--parent", "config"]) self.assertEqual(self.result.config2, None) + @make_config({"test": {"test_path": "/test"}}) + def test_macros_in_component_options(self, config_file): + """fix up macros in component options.""" + self.parser.add_options([Common.repository]) + self.parser.parse(["-C", config_file, "-Q", "/foo/bar", + "--parent", "path"]) + self.assertEqual(self.result.test_path, "/foo/bar/test") + self.assertEqual(self.result.test_path_default, + "/foo/bar/test/default") + class ImportComponentAction(ComponentAction): """action that imports real classes for testing.""" diff --git a/testsuite/Testsrc/Testlib/TestOptions/TestOptions.py b/testsuite/Testsrc/Testlib/TestOptions/TestOptions.py index b76cd6d3a..9f4a7873c 100644 --- a/testsuite/Testsrc/Testlib/TestOptions/TestOptions.py +++ b/testsuite/Testsrc/Testlib/TestOptions/TestOptions.py @@ -20,6 +20,7 @@ class TestBasicOptions(OptionTestCase): # that's probably bad -- and it's definitely bad if we ever # want to do real on-the-fly config changes -- but it's easier # to leave it as is and set the options on each test. + OptionTestCase.setUp(self) self.options = [ BooleanOption("--test-true-boolean", env="TEST_TRUE_BOOLEAN", cf=("test", "true_boolean"), default=True), @@ -367,13 +368,16 @@ class TestBasicOptions(OptionTestCase): parser.add_options, [Option(cf=("test", "option"))]) - @make_config() + @make_config({"test": {"test_path": "/test"}}) def test_repository_macro(self, config_file): """fix up macros.""" result = argparse.Namespace() parser = Parser(namespace=result) parser.add_options([PathOption("--test1"), PathOption("--test2"), + PathOption(cf=("test", "test_path")), + PathOption(cf=("test", "test_path_default"), + default="/test/default"), Common.repository]) parser.parse(["-C", config_file, "-Q", "/foo/bar", "--test1", "/test1", @@ -381,6 +385,8 @@ class TestBasicOptions(OptionTestCase): self.assertEqual(result.repository, "/foo/bar") self.assertEqual(result.test1, "/foo/bar/test1") self.assertEqual(result.test2, "/foo/bar/foo/bar") + self.assertEqual(result.test_path, "/foo/bar/test") + self.assertEqual(result.test_path_default, "/foo/bar/test/default") @make_config() def test_file_like_path_option(self, config_file): diff --git a/testsuite/Testsrc/Testlib/TestOptions/__init__.py b/testsuite/Testsrc/Testlib/TestOptions/__init__.py index 688f4e54c..ca2c41359 100644 --- a/testsuite/Testsrc/Testlib/TestOptions/__init__.py +++ b/testsuite/Testsrc/Testlib/TestOptions/__init__.py @@ -4,7 +4,7 @@ import os import tempfile from Bcfg2.Compat import wraps, ConfigParser -from Bcfg2.Options import Parser +from Bcfg2.Options import Parser, PathOption from testsuite.common import Bcfg2TestCase @@ -75,7 +75,10 @@ class OptionTestCase(Bcfg2TestCase): Parser.unit_test = False Bcfg2TestCase.setUpClass() + def setUp(self): + Bcfg2TestCase.setUp(self) + PathOption.repository = None + @classmethod def tearDownClass(cls): Parser.unit_test = True - Bcfg2TestCase.tearDownClass() -- cgit v1.2.3-1-g7c22 From ee50f531ce6ba4a23d0b8c6e1ec81f09c0652874 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Wed, 22 Oct 2014 12:53:54 -0500 Subject: testsuite: unlink temporary files This cleans up the temporary config files created by the option parsing unit tests. Courtesy Alexander Sulfrian. --- testsuite/Testsrc/Testlib/TestOptions/TestOptions.py | 9 ++++++--- testsuite/Testsrc/Testlib/TestOptions/__init__.py | 6 ++++-- 2 files changed, 10 insertions(+), 5 deletions(-) (limited to 'testsuite') diff --git a/testsuite/Testsrc/Testlib/TestOptions/TestOptions.py b/testsuite/Testsrc/Testlib/TestOptions/TestOptions.py index 9f4a7873c..94d30dd3a 100644 --- a/testsuite/Testsrc/Testlib/TestOptions/TestOptions.py +++ b/testsuite/Testsrc/Testlib/TestOptions/TestOptions.py @@ -400,9 +400,12 @@ class TestBasicOptions(OptionTestCase): fh.write("test") fh.close() - parser.parse(["-C", config_file, "--test", name]) - self.assertEqual(result.test.name, name) - self.assertEqual(result.test.read(), "test") + try: + parser.parse(["-C", config_file, "--test", name]) + self.assertEqual(result.test.name, name) + self.assertEqual(result.test.read(), "test") + finally: + os.unlink(name) @clean_environment @make_config() diff --git a/testsuite/Testsrc/Testlib/TestOptions/__init__.py b/testsuite/Testsrc/Testlib/TestOptions/__init__.py index ca2c41359..e92f95e94 100644 --- a/testsuite/Testsrc/Testlib/TestOptions/__init__.py +++ b/testsuite/Testsrc/Testlib/TestOptions/__init__.py @@ -32,8 +32,10 @@ class make_config(object): # pylint: disable=invalid-name config_file.close() args = list(args) + [name] - rv = func(*args, **kwargs) - os.unlink(name) + try: + rv = func(*args, **kwargs) + finally: + os.unlink(name) return rv return inner -- cgit v1.2.3-1-g7c22 From bb21c79f38c155fb14289479840efd8abdf54689 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Wed, 22 Oct 2014 13:02:35 -0500 Subject: Options: fix path canonicalization and file-like objects This fixes canonicalizing PathOption values when the default value of a config file-only option is used. It also fixes PathOptions that get a file-like object instead of a filename string. --- testsuite/Testsrc/Testlib/TestOptions/TestOptions.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) (limited to 'testsuite') diff --git a/testsuite/Testsrc/Testlib/TestOptions/TestOptions.py b/testsuite/Testsrc/Testlib/TestOptions/TestOptions.py index 94d30dd3a..a3190f2ca 100644 --- a/testsuite/Testsrc/Testlib/TestOptions/TestOptions.py +++ b/testsuite/Testsrc/Testlib/TestOptions/TestOptions.py @@ -75,6 +75,20 @@ class TestBasicOptions(OptionTestCase): self.assertEqual(options.test_path_option, os.path.abspath("./test")) + @make_config() + def test_default_path_canonicalization(self, config_file): + """canonicalize default PathOption values.""" + testdir = os.path.expanduser("~/test") + result = argparse.Namespace() + parser = Parser(namespace=result) + parser.add_options([PathOption("--test1", default="~/test"), + PathOption(cf=("test", "test2"), + default="~/test"), + Common.repository]) + parser.parse(["-C", config_file]) + self.assertEqual(result.test1, testdir) + self.assertEqual(result.test2, testdir) + def test_default_bool(self): """use the default value of boolean options.""" options = self._test_options() -- cgit v1.2.3-1-g7c22 From 0e133c157755908d05c44c3a36b1dc0668e1d111 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Mon, 10 Nov 2014 11:42:42 -0600 Subject: Options: Fixed non-path database name parsing The database name is sometimes a path (SQLite) and sometimes not (MySQL, PostgreSQL). This introduces a new Option type, RepositoryMacroOption, that expands macros without canonicalizing the path, so SQLite users can use in their settings but MySQL users' database name settings will not be destroyed by path canonicalization. The unfortunate downside is that SQLite users can't use ~ in their database name. --- testsuite/Testsrc/Testlib/TestOptions/TestOptions.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) (limited to 'testsuite') diff --git a/testsuite/Testsrc/Testlib/TestOptions/TestOptions.py b/testsuite/Testsrc/Testlib/TestOptions/TestOptions.py index a3190f2ca..a2dc8ffe2 100644 --- a/testsuite/Testsrc/Testlib/TestOptions/TestOptions.py +++ b/testsuite/Testsrc/Testlib/TestOptions/TestOptions.py @@ -7,8 +7,9 @@ import tempfile import mock from Bcfg2.Compat import ConfigParser -from Bcfg2.Options import Option, PathOption, BooleanOption, Parser, \ - PositionalArgument, OptionParserException, Common, new_parser, get_parser +from Bcfg2.Options import Option, PathOption, RepositoryMacroOption, \ + BooleanOption, Parser, PositionalArgument, OptionParserException, \ + Common, new_parser, get_parser from testsuite.Testsrc.Testlib.TestOptions import OptionTestCase, \ make_config, clean_environment @@ -382,16 +383,21 @@ class TestBasicOptions(OptionTestCase): parser.add_options, [Option(cf=("test", "option"))]) - @make_config({"test": {"test_path": "/test"}}) + @make_config({"test": {"test_path": "/test", + "test_macro": ""}}) def test_repository_macro(self, config_file): """fix up macros.""" result = argparse.Namespace() parser = Parser(namespace=result) parser.add_options([PathOption("--test1"), - PathOption("--test2"), + RepositoryMacroOption("--test2"), PathOption(cf=("test", "test_path")), PathOption(cf=("test", "test_path_default"), default="/test/default"), + RepositoryMacroOption(cf=("test", "test_macro")), + RepositoryMacroOption( + cf=("test", "test_macro_default"), + default=""), Common.repository]) parser.parse(["-C", config_file, "-Q", "/foo/bar", "--test1", "/test1", @@ -399,6 +405,8 @@ class TestBasicOptions(OptionTestCase): self.assertEqual(result.repository, "/foo/bar") self.assertEqual(result.test1, "/foo/bar/test1") self.assertEqual(result.test2, "/foo/bar/foo/bar") + self.assertEqual(result.test_macro, "/foo/bar") + self.assertEqual(result.test_macro_default, "/foo/bar") self.assertEqual(result.test_path, "/foo/bar/test") self.assertEqual(result.test_path_default, "/foo/bar/test/default") -- cgit v1.2.3-1-g7c22