def test_default_help_text(help_builder): """All different parts for the default help.""" cmd1 = create_command("cmd1", "Cmd help which is very long but whatever.", common_=True) cmd2 = create_command("command-2", "Cmd help.", common_=True) cmd3 = create_command("cmd3", "Extremely " + "super crazy long " * 5 + " help.", common_=True) cmd4 = create_command("cmd4", "Some help.") cmd5 = create_command("cmd5", "More help.") cmd6 = create_command("cmd6-really-long", "More help.", common_=True) cmd7 = create_command("cmd7", "More help.") command_groups = [ CommandGroup("group1", [cmd6, cmd2]), CommandGroup("group3", [cmd7]), CommandGroup("group2", [cmd3, cmd4, cmd5, cmd1]), ] fake_summary = textwrap.dedent(""" This is the summary for the whole program. """) global_options = [ ("-h, --help", "Show this help message and exit."), ("-q, --quiet", "Only show warnings and errors, not progress."), ] help_builder.init("testapp", fake_summary, command_groups) text = help_builder.get_full_help(global_options) expected = textwrap.dedent("""\ Usage: testapp [help] <command> Summary: This is the summary for the whole program. Global options: -h, --help: Show this help message and exit. -q, --quiet: Only show warnings and errors, not progress. Starter commands: cmd1: Cmd help which is very long but whatever. cmd3: Extremely super crazy long super crazy long super crazy long super crazy long super crazy long help. cmd6-really-long: More help. command-2: Cmd help. Commands can be classified as follows: group1: cmd6-really-long, command-2 group2: cmd1, cmd3, cmd4, cmd5 group3: cmd7 For more information about a command, run 'charmcraft help <command>'. For a summary of all commands, run 'charmcraft help --all'. """) assert text == expected
def test_dispatcher_build_commands_ok(): """Correct command loading.""" cmd0, cmd1, cmd2 = [create_command("cmd-name-{}".format(n), "cmd help") for n in range(3)] groups = [ CommandGroup("whatever title", [cmd0]), CommandGroup("other title", [cmd1, cmd2]), ] dispatcher = Dispatcher(groups) assert len(dispatcher.commands) == 3 for cmd in [cmd0, cmd1, cmd2]: expected_class = dispatcher.commands[cmd.name] assert expected_class == cmd
def test_command_help_text_no_parameters(config, help_builder): """All different parts for a specific command help that doesn't have parameters.""" overview = textwrap.dedent(""" Quite some long text. Multiline! """) cmd1 = create_command("somecommand", "Command one line help.", overview_=overview) cmd2 = create_command("other-cmd-2", "Some help.") cmd3 = create_command("other-cmd-3", "Some help.") cmd4 = create_command("other-cmd-4", "Some help.") command_groups = [ CommandGroup("group1", [cmd1, cmd2, cmd4]), CommandGroup("group2", [cmd3]), ] options = [ ("-h, --help", "Show this help message and exit."), ("-q, --quiet", "Only show warnings and errors, not progress."), ("--name", "The name of the charm."), ("--revision", "The revision to release (defaults to latest)."), ] help_builder.init("testapp", "general summary", command_groups) text = help_builder.get_command_help(cmd1(config), options) expected = textwrap.dedent("""\ Usage: charmcraft somecommand [options] Summary: Quite some long text. Multiline! Options: -h, --help: Show this help message and exit. -q, --quiet: Only show warnings and errors, not progress. --name: The name of the charm. --revision: The revision to release (defaults to latest). See also: other-cmd-2 other-cmd-4 For a summary of all commands, run 'charmcraft help --all'. """) assert text == expected
def test_tool_exec_command_bad_option_type(help_builder): """Execute a correct command but giving the valid option a bad value.""" def fill_parser(self, parser): parser.add_argument("--number", type=int) cmd = create_command("somecommand", "This command does that.") cmd.fill_parser = fill_parser command_groups = [CommandGroup("group", [cmd])] dispatcher = Dispatcher(command_groups) dispatcher.pre_parse_args(["somecommand", "--number=foo"]) help_builder.init("testapp", "general summary", command_groups) with pytest.raises(ArgumentParsingError) as cm: dispatcher.load_command("config") expected = textwrap.dedent("""\ Usage: testapp [options] command [args]... Try 'testapp somecommand -h' for help. Error: argument --number: invalid int value: 'foo' """) error = cm.value assert str(error) == expected
def test_dispatcher_commands_are_not_loaded_if_not_needed(): class MyCommand1(BaseCommand): """Expected to be executed.""" name = "command1" help_msg = "some help" _executed = [] def run(self, parsed_args): self._executed.append(parsed_args) class MyCommand2(BaseCommand): """Expected to not be instantiated, or parse args, or run.""" name = "command2" help_msg = "some help" def __init__(self, *args): raise AssertionError def fill_parser(self, parser): raise AssertionError def run(self, parsed_args): raise AssertionError groups = [CommandGroup("title", [MyCommand1, MyCommand2])] dispatcher = Dispatcher(groups) dispatcher.pre_parse_args(["command1"]) dispatcher.load_command("config") dispatcher.run() assert isinstance(MyCommand1._executed[0], argparse.Namespace)
def test_tool_exec_command_dash_help_missing_params(help_option): """Execute a command (which needs params) asking for help.""" def fill_parser(self, parser): parser.add_argument("mandatory") cmd = create_command("somecommand", "This command does that.") cmd.fill_parser = fill_parser command_groups = [CommandGroup("group", [cmd])] dispatcher = Dispatcher(command_groups) with patch("charmcraft.helptexts.HelpBuilder.get_command_help") as mock: mock.return_value = "test help" with pytest.raises(ProvideHelpException) as cm: dispatcher.pre_parse_args(["somecommand", help_option]) # check the result of the full help builder is what is transmitted assert str(cm.value) == "test help" # check the given information to the help text builder args = mock.call_args[0] assert args[0].__class__ == cmd assert sorted(x[0] for x in args[1]) == [ "-h, --help", "-q, --quiet", "-t, --trace", "-v, --verbose", "mandatory", ]
def test_dispatcher_global_arguments_default(): """The dispatcher uses the default global arguments.""" cmd = create_command("somecommand") groups = [CommandGroup("title", [cmd])] dispatcher = Dispatcher(groups) assert dispatcher.global_arguments == _DEFAULT_GLOBAL_ARGS
def test_dispatcher_generic_setup_paramglobal_with_param(options): """Generic parameter handling for a param type global arg, directly or after the cmd.""" cmd = create_command("somecommand") groups = [CommandGroup("title", [cmd])] extra = GlobalArgument("globalparam", "option", "-g", "--globalparam", "Test global param.") dispatcher = Dispatcher(groups, [extra]) global_args = dispatcher.pre_parse_args(options) assert global_args["globalparam"] == "foobar"
def test_dispatcher_global_arguments_extra_arguments(): """The dispatcher uses the default global arguments.""" cmd = create_command("somecommand") groups = [CommandGroup("title", [cmd])] extra_arg = GlobalArgument("other", "flag", "-o", "--other", "Other stuff") dispatcher = Dispatcher(groups, extra_global_args=[extra_arg]) assert dispatcher.global_arguments == _DEFAULT_GLOBAL_ARGS + [extra_arg]
def test_dispatcher_generic_setup_trace(options): """Generic parameter handling for trace log setup, directly or after the command.""" cmd = create_command("somecommand") groups = [CommandGroup("title", [cmd])] emit.set_mode(EmitterMode.NORMAL) # this is how `main` will init the Emitter dispatcher = Dispatcher(groups) dispatcher.pre_parse_args(options) assert emit.get_mode() == EmitterMode.TRACE
def test_dispatcher_generic_setup_mutually_exclusive(options): """Disallow mutually exclusive generic options.""" cmd = create_command("somecommand") groups = [CommandGroup("title", [cmd])] dispatcher = Dispatcher(groups) with pytest.raises(ArgumentParsingError) as err: dispatcher.pre_parse_args(options) assert str(err.value) == "The 'verbose', 'trace' and 'quiet' options are mutually exclusive."
def test_dispatcher_generic_setup_default(): """Generic parameter handling for default values.""" cmd = create_command("somecommand") groups = [CommandGroup("title", [cmd])] emit.set_mode(EmitterMode.NORMAL) # this is how `main` will init the Emitter dispatcher = Dispatcher(groups) dispatcher.pre_parse_args(["somecommand"]) assert emit.get_mode() == EmitterMode.NORMAL
def test_dispatcher_generic_setup_paramglobal_without_param_simple(options): """Generic parameter handling for a param type global arg without the requested parameter.""" cmd = create_command("somecommand") groups = [CommandGroup("title", [cmd])] extra = GlobalArgument("globalparam", "option", "-g", "--globalparam", "Test global param.") dispatcher = Dispatcher(groups, [extra]) with pytest.raises(ArgumentParsingError) as err: dispatcher.pre_parse_args(options) assert str(err.value) == "The 'globalparam' option expects one argument."
def test_dispatcher_command_loading(): """Parses and return global arguments.""" cmd = create_command("somecommand") groups = [CommandGroup("title", [cmd])] dispatcher = Dispatcher(groups) dispatcher.pre_parse_args(["somecommand"]) command = dispatcher.load_command("test-config") assert isinstance(command, cmd) assert command.config == "test-config"
def test_dispatcher_generic_setup_paramglobal_without_param_confusing(options): """Generic parameter handling for a param type global arg confusing the command as the arg.""" cmd = create_command("somecommand") groups = [CommandGroup("title", [cmd])] extra = GlobalArgument("globalparam", "option", "-g", "--globalparam", "Test global param.") dispatcher = Dispatcher(groups, [extra]) with patch("charmcraft.helptexts.HelpBuilder.get_full_help") as mock_helper: mock_helper.return_value = "help text" with pytest.raises(ArgumentParsingError) as err: dispatcher.pre_parse_args(options) # generic usage message because "no command" (as 'somecommand' was consumed by --globalparam) assert str(err.value) == "help text"
def test_dispatcher_build_commands_repeated(): """Error while loading commands with repeated name.""" class Foo(BaseCommand): help_msg = "some help" name = "repeated" class Bar(BaseCommand): help_msg = "some help" name = "cool" class Baz(BaseCommand): help_msg = "some help" name = "repeated" groups = [ CommandGroup("whatever title", [Foo, Bar]), CommandGroup("other title", [Baz]), ] expected_msg = "Multiple commands with same name: (Foo|Baz) and (Baz|Foo)" with pytest.raises(RuntimeError, match=expected_msg): Dispatcher(groups)
def test_main_load_config_not_present_ok(): """Command ends indicating the return code to be used.""" class MyCommand(BaseCommand): help_msg = "some help" name = "cmdname" def run(self, parsed_args): assert self.config.type is None assert not self.config.project.config_provided with patch("charmcraft.main.COMMAND_GROUPS", [CommandGroup("title", [MyCommand])]): retcode = main(["charmcraft", "cmdname", "--project-dir=/whatever"]) assert retcode == 0
def test_main_load_config_not_present_but_needed(capsys): """Command ends indicating the return code to be used.""" cmd = create_command("cmdname", needs_config_=True) with patch("charmcraft.main.COMMAND_GROUPS", [CommandGroup("title", [cmd])]): retcode = main(["charmcraft", "cmdname", "--project-dir=/whatever"]) assert retcode == 1 out, err = capsys.readouterr() assert not out assert err == ( "The specified command needs a valid 'charmcraft.yaml' configuration file (in " "the current directory or where specified with --project-dir option); see " "the reference: https://discourse.charmhub.io/t/charmcraft-configuration/4138\n" )
def test_command_help_text_loneranger(config, help_builder): """All different parts for a specific command that's the only one in its group.""" overview = textwrap.dedent(""" Quite some long text. """) cmd1 = create_command("somecommand", "Command one line help.", overview_=overview) cmd2 = create_command("other-cmd-2", "Some help.") command_groups = [ CommandGroup("group1", [cmd1]), CommandGroup("group2", [cmd2]), ] options = [ ("-h, --help", "Show this help message and exit."), ("-q, --quiet", "Only show warnings and errors, not progress."), ] help_builder.init("testapp", "general summary", command_groups) text = help_builder.get_command_help(cmd1(config), options) expected = textwrap.dedent("""\ Usage: charmcraft somecommand [options] Summary: Quite some long text. Options: -h, --help: Show this help message and exit. -q, --quiet: Only show warnings and errors, not progress. For a summary of all commands, run 'charmcraft help --all'. """) assert text == expected
def test_dispatcher_command_execution_crash(): """Command crashing doesn't pass through, we inform nicely.""" class MyCommand(BaseCommand): help_msg = "some help" name = "cmdname" def run(self, parsed_args): raise ValueError() groups = [CommandGroup("title", [MyCommand])] dispatcher = Dispatcher(groups) dispatcher.pre_parse_args(["cmdname"]) dispatcher.load_command("config") with pytest.raises(ValueError): dispatcher.run()
def test_dispatcher_command_return_code(): """Command ends indicating the return code to be used.""" class MyCommand(BaseCommand): help_msg = "some help" name = "cmdname" def run(self, parsed_args): return 17 groups = [CommandGroup("title", [MyCommand])] dispatcher = Dispatcher(groups) dispatcher.pre_parse_args(["cmdname"]) dispatcher.load_command("config") retcode = dispatcher.run() assert retcode == 17
def test_tool_exec_help_command_on_command_wrong(): """Execute charmcraft asking for help on a command which does not exist.""" command_groups = [CommandGroup("group", [])] dispatcher = Dispatcher(command_groups) with patch("charmcraft.helptexts.HelpBuilder.get_usage_message") as mock: mock.return_value = "test help" with pytest.raises(ArgumentParsingError) as cm: dispatcher.pre_parse_args(["help", "wrongcommand"]) error = cm.value # check the given information to the help text builder assert mock.call_args[0] == ( "command 'wrongcommand' not found to provide help for", ) # check the result of the help builder is what is shown assert str(error) == "test help"
def test_main_load_config_ok(create_config): """Command is properly executed, after loading and receiving the config.""" tmp_path = create_config( """ type: charm """ ) class MyCommand(BaseCommand): help_msg = "some help" name = "cmdname" def run(self, parsed_args): assert self.config.type == "charm" with patch("charmcraft.main.COMMAND_GROUPS", [CommandGroup("title", [MyCommand])]): retcode = main(["charmcraft", "cmdname", f"--project-dir={tmp_path}"]) assert retcode == 0
def test_command_help_text_with_parameters(config, help_builder): """All different parts for a specific command help that has parameters.""" overview = textwrap.dedent(""" Quite some long text. """) cmd1 = create_command("somecommand", "Command one line help.", overview_=overview) cmd2 = create_command("other-cmd-2", "Some help.") command_groups = [ CommandGroup("group1", [cmd1, cmd2]), ] options = [ ("-h, --help", "Show this help message and exit."), ("name", "The name of the charm."), ("--revision", "The revision to release (defaults to latest)."), ("extraparam", "Another parameter.."), ("--other-option", "Other option."), ] help_builder.init("testapp", "general summary", command_groups) text = help_builder.get_command_help(cmd1(config), options) expected = textwrap.dedent("""\ Usage: charmcraft somecommand [options] <name> <extraparam> Summary: Quite some long text. Options: -h, --help: Show this help message and exit. --revision: The revision to release (defaults to latest). --other-option: Other option. See also: other-cmd-2 For a summary of all commands, run 'charmcraft help --all'. """) assert text == expected
def test_tool_exec_command_wrong_option(help_builder): """Execute a correct command but with a wrong option.""" cmd = create_command("somecommand", "This command does that.") command_groups = [CommandGroup("group", [cmd])] dispatcher = Dispatcher(command_groups) dispatcher.pre_parse_args(["somecommand", "--whatever"]) help_builder.init("testapp", "general summary", command_groups) with pytest.raises(ArgumentParsingError) as cm: dispatcher.load_command("config") expected = textwrap.dedent("""\ Usage: testapp [options] command [args]... Try 'testapp somecommand -h' for help. Error: unrecognized arguments: --whatever """) error = cm.value assert str(error) == expected
def test_tool_exec_help_command_all(): """Execute charmcraft asking for detailed help.""" command_groups = [CommandGroup("group", [])] dispatcher = Dispatcher(command_groups) with patch("charmcraft.helptexts.HelpBuilder.get_detailed_help") as mock: mock.return_value = "test help" with pytest.raises(ProvideHelpException) as cm: dispatcher.pre_parse_args(["help", "--all"]) # check the result of the help builder is what is transmitted assert str(cm.value) == "test help" # check the given information to the help text builder args = mock.call_args[0] assert sorted(x[0] for x in args[0]) == [ "-h, --help", "-q, --quiet", "-t, --trace", "-v, --verbose", ]
def test_tool_exec_help_command_on_command_complex(): """Execute charmcraft asking for help on a command with parameters and options.""" def fill_parser(self, parser): parser.add_argument("param1", help="help on param1") parser.add_argument("param2", help="help on param2") parser.add_argument("param3", metavar="transformed3", help="help on param2") parser.add_argument("--option1", help="help on option1") parser.add_argument("-o2", "--option2", help="help on option2") cmd = create_command("somecommand", "This command does that.") cmd.fill_parser = fill_parser command_groups = [CommandGroup("group", [cmd])] dispatcher = Dispatcher(command_groups) with patch("charmcraft.helptexts.HelpBuilder.get_command_help") as mock: mock.return_value = "test help" with pytest.raises(ProvideHelpException) as cm: dispatcher.pre_parse_args(["help", "somecommand"]) # check the result of the help builder is what is transmitted assert str(cm.value) == "test help" # check the given information to the help text builder args = mock.call_args[0] assert args[0].__class__ == cmd expected_options = [ "--option1", "-h, --help", "-o2, --option2", "-q, --quiet", "-t, --trace", "-v, --verbose", "param1", "param2", "transformed3", ] assert sorted(x[0] for x in args[1]) == expected_options
def test_tool_exec_help_command_on_command_ok(): """Execute charmcraft asking for help on a command ok.""" cmd = create_command("somecommand", "This command does that.") command_groups = [CommandGroup("group", [cmd])] dispatcher = Dispatcher(command_groups) with patch("charmcraft.helptexts.HelpBuilder.get_command_help") as mock: mock.return_value = "test help" with pytest.raises(ProvideHelpException) as cm: dispatcher.pre_parse_args(["help", "somecommand"]) # check the result of the help builder is what is transmitted assert str(cm.value) == "test help" # check the given information to the help text builder args = mock.call_args[0] assert isinstance(args[0], cmd) assert sorted(x[0] for x in args[1]) == [ "-h, --help", "-q, --quiet", "-t, --trace", "-v, --verbose", ]
def test_dispatcher_command_execution_ok(): """Command execution depends of the indicated name in command line, return code ok.""" class MyCommandControl(BaseCommand): help_msg = "some help" def run(self, parsed_args): self._executed.append(parsed_args) class MyCommand1(MyCommandControl): name = "name1" _executed = [] class MyCommand2(MyCommandControl): name = "name2" _executed = [] groups = [CommandGroup("title", [MyCommand1, MyCommand2])] dispatcher = Dispatcher(groups) dispatcher.pre_parse_args(["name2"]) dispatcher.load_command("config") dispatcher.run() assert MyCommand1._executed == [] assert isinstance(MyCommand2._executed[0], argparse.Namespace)
def test_dispatcher_pre_parsing(): """Parses and return global arguments.""" groups = [CommandGroup("title", [create_command("somecommand")])] dispatcher = Dispatcher(groups) global_args = dispatcher.pre_parse_args(["-q", "somecommand"]) assert global_args == {"help": False, "verbose": False, "quiet": True, "trace": False}