Beispiel #1
0
def test_no_bases_is_ok_for_bundles(emitter, create_config, tmp_path):
    """Do not send a deprecation message if it is a bundle."""
    create_config("""
        type: bundle
    """)

    load(tmp_path)
    assert not emitter.interactions
Beispiel #2
0
    def check_schema_error(expected_msg):
        """The real checker.

        Note this compares for multiple messages, as for Python 3.5 we don't have control on
        which of the verifications it will fail. After 3.5 is dropped this could be changed
        to receive only one message and compare with equality below, not inclusion.
        """
        with pytest.raises(CommandError) as cm:
            load(tmp_path)
        assert str(cm.value) == expected_msg
Beispiel #3
0
def test_load_managed_mode_directory(create_config, monkeypatch, tmp_path):
    """Validate managed-mode default directory is /root/project."""
    monkeypatch.chdir(tmp_path)
    monkeypatch.setenv("CHARMCRAFT_MANAGED_MODE", "1")

    # Patch out Config (and Project) to prevent directory validation checks.
    with patch("charmcraft.config.Config"):
        with patch("charmcraft.config.Project") as mock_project:
            with patch("charmcraft.config.load_yaml"):
                load(None)

    assert mock_project.call_args.kwargs["dirpath"] == pathlib.Path(
        "/root/project")
Beispiel #4
0
def test_load_optional_charmcraft_bad_directory(tmp_path):
    """Specify a missing directory."""
    missing_directory = tmp_path / "missing"
    config = load(missing_directory)
    assert config.type is None
    assert config.project.dirpath == missing_directory
    assert not config.project.config_provided
Beispiel #5
0
def test_bases_minimal_short_form(create_config):
    tmp_path = create_config("""
        type: charm
        bases:
          - name: test-name
            channel: test-channel
    """)

    config = load(tmp_path)
    assert config.bases == [
        BasesConfiguration(
            **{
                "build-on": [
                    Base(
                        name="test-name",
                        channel="test-channel",
                        architectures=[get_host_architecture()],
                    ),
                ],
                "run-on": [
                    Base(
                        name="test-name",
                        channel="test-channel",
                        architectures=[get_host_architecture()],
                    ),
                ],
            })
    ]
Beispiel #6
0
def test_load_specific_directory_ok(create_config):
    """Init the config using charmcraft.yaml in a specific directory."""
    tmp_path = create_config("""
        type: charm
    """)
    config = load(tmp_path)
    assert config.type == "charm"
    assert config.project.dirpath == tmp_path
Beispiel #7
0
def test_schema_analysis_missing(create_config, tmp_path):
    """No analysis configuration leads to some defaults in place."""
    create_config("""
        type: charm  # mandatory
    """)
    config = load(tmp_path)
    assert config.analysis.ignore.attributes == []
    assert config.analysis.ignore.linters == []
Beispiel #8
0
def test_multiple_long_form_bases(create_config):
    tmp_path = create_config("""
        type: charm
        bases:
          - build-on:
              - name: test-build-name-1
                channel: test-build-channel-1
            run-on:
              - name: test-run-name-1
                channel: test-run-channel-1
                architectures: [amd64, arm64]
          - build-on:
              - name: test-build-name-2
                channel: test-build-channel-2
            run-on:
              - name: test-run-name-2
                channel: test-run-channel-2
                architectures: [amd64, arm64]
    """)

    config = load(tmp_path)
    assert config.bases == [
        BasesConfiguration(
            **{
                "build-on": [
                    Base(
                        name="test-build-name-1",
                        channel="test-build-channel-1",
                        architectures=[get_host_architecture()],
                    ),
                ],
                "run-on": [
                    Base(
                        name="test-run-name-1",
                        channel="test-run-channel-1",
                        architectures=["amd64", "arm64"],
                    ),
                ],
            }),
        BasesConfiguration(
            **{
                "build-on": [
                    Base(
                        name="test-build-name-2",
                        channel="test-build-channel-2",
                        architectures=[get_host_architecture()],
                    ),
                ],
                "run-on": [
                    Base(
                        name="test-run-name-2",
                        channel="test-run-channel-2",
                        architectures=["amd64", "arm64"],
                    ),
                ],
            }),
    ]
Beispiel #9
0
def test_load_current_directory(create_config, monkeypatch):
    """Init the config using charmcraft.yaml in current directory."""
    tmp_path = create_config("""
        type: charm
    """)
    monkeypatch.chdir(tmp_path)
    config = load(None)
    assert config.type == 'charm'
    assert config.project.dirpath == tmp_path
    assert config.project.config_provided
Beispiel #10
0
def test_load_specific_directory_expanded(create_config, monkeypatch):
    """Ensure that the given directory is user-expanded."""
    tmp_path = create_config("""
        type: charm
    """)
    # fake HOME so the '~' indication is verified to work
    monkeypatch.setitem(os.environ, "HOME", str(tmp_path))
    config = load("~")

    assert config.type == "charm"
    assert config.project.dirpath == tmp_path
Beispiel #11
0
def test_schema_analysis_full_struct_just_empty(create_config, tmp_path):
    """Complete analysis structure, empty."""
    create_config("""
        type: charm  # mandatory
        analysis:
            ignore:
                attributes: []
                linters: []
    """)
    config = load(tmp_path)
    assert config.analysis.ignore.attributes == []
    assert config.analysis.ignore.linters == []
Beispiel #12
0
def test_load_current_directory(create_config, monkeypatch):
    """Init the config using charmcraft.yaml in current directory."""
    tmp_path = create_config("""
        type: charm
    """)
    monkeypatch.chdir(tmp_path)
    with patch("datetime.datetime") as mock:
        mock.utcnow.return_value = "test_timestamp"
        config = load(None)
    assert config.type == "charm"
    assert config.project.dirpath == tmp_path
    assert config.project.config_provided
    assert config.project.started_at == "test_timestamp"
Beispiel #13
0
def test_load_specific_directory_resolved(create_config, monkeypatch):
    """Ensure that the given directory is resolved to always show the whole path."""
    tmp_path = create_config("""
        type: charm
    """)
    # change to some dir, and reference the config dir relatively
    subdir = tmp_path / "subdir"
    subdir.mkdir()
    monkeypatch.chdir(subdir)
    config = load("../")

    assert config.type == "charm"
    assert config.project.dirpath == tmp_path
Beispiel #14
0
def test_schema_analysis_ignore_linters(create_config, tmp_path,
                                        create_checker):
    """Some linters are correctly ignored."""
    create_checker("check_ok_1", linters.CheckType.lint)
    create_checker("check_ok_2", linters.CheckType.lint)
    create_config("""
        type: charm  # mandatory
        analysis:
            ignore:
                linters: [check_ok_1, check_ok_2]
    """)
    config = load(tmp_path)
    assert config.analysis.ignore.attributes == []
    assert config.analysis.ignore.linters == ["check_ok_1", "check_ok_2"]
Beispiel #15
0
def test_charmhub_underscore_backwards_compatibility(create_config, tmp_path,
                                                     emitter):
    """Support underscore in these attributes for a while."""
    create_config("""
        type: charm  # mandatory
        charmhub:
            storage_url: https://server1.com
            api_url: https://server2.com
            registry_url: https://server3.com
    """)
    cfg = load(tmp_path)
    assert cfg.charmhub.storage_url == "https://server1.com"
    assert cfg.charmhub.api_url == "https://server2.com"
    assert cfg.charmhub.registry_url == "https://server3.com"
    deprecation_msg = "DEPRECATED: Configuration keywords are now separated using dashes."
    emitter.assert_message(deprecation_msg, intermediate=True)
Beispiel #16
0
def test_bundle_parts_with_bundle_part(tmp_path, monkeypatch, bundle_yaml):
    """Parts are declared with a charm part with implicit plugin.

    When the "parts" section exists in chamcraft.yaml and a part named "bundle"
    is defined with implicit plugin (or explicit "bundle" plugin), populate it
    with the defaults for bundle building.
    """
    bundle_yaml(name="testbundle")
    (tmp_path / "README.md").write_text("test readme")

    charmcraft_file = tmp_path / "charmcraft.yaml"
    charmcraft_file.write_text(
        dedent("""
            type: bundle
            parts:
              bundle:
                prime:
                  - my_extra_file.txt
        """))

    config = load(tmp_path)

    monkeypatch.setenv("CHARMCRAFT_MANAGED_MODE", "1")
    with patch("charmcraft.parts.PartsLifecycle",
               autospec=True) as mock_lifecycle:
        mock_lifecycle.side_effect = SystemExit()
        with pytest.raises(SystemExit):
            PackCommand(config).run(get_namespace(shell_after=True))
    mock_lifecycle.assert_has_calls([
        call(
            {
                "bundle": {
                    "plugin": "bundle",
                    "source": str(tmp_path),
                    "prime": [
                        "my_extra_file.txt",
                        "bundle.yaml",
                        "README.md",
                    ],
                }
            },
            work_dir=pathlib.Path("/root"),
            project_dir=tmp_path,
            project_name="testbundle",
            ignore_local_sources=["testbundle.zip"],
        )
    ])
Beispiel #17
0
def test_no_bases_defaults_to_ubuntu_20_04_with_dn03(emitter, create_config,
                                                     tmp_path):
    create_config("""
        type: charm
    """)

    config = load(tmp_path)

    assert config.bases == [
        BasesConfiguration(
            **{
                "build-on": [Base(name="ubuntu", channel="20.04")],
                "run-on": [Base(name="ubuntu", channel="20.04")],
            })
    ]
    emitter.assert_message("DEPRECATED: Bases configuration is now required.",
                           intermediate=True)
Beispiel #18
0
def test_charmhub_underscore_backwards_compatibility(create_config, tmp_path,
                                                     caplog):
    """Support underscore in these attributes for a while."""
    caplog.set_level(logging.WARNING, logger="charmcraft")

    create_config("""
        type: charm  # mandatory
        charmhub:
            storage_url: https://server1.com
            api_url: https://server2.com
            registry_url: https://server3.com
    """)
    cfg = load(tmp_path)
    assert cfg.charmhub.storage_url == "https://server1.com"
    assert cfg.charmhub.api_url == "https://server2.com"
    assert cfg.charmhub.registry_url == "https://server3.com"
    deprecation_msg = "DEPRECATED: Configuration keywords are now separated using dashes."
    assert deprecation_msg in [rec.message for rec in caplog.records]
Beispiel #19
0
def test_no_bases_defaults_to_ubuntu_20_04_with_dn03(caplog, create_config,
                                                     tmp_path):
    caplog.set_level(logging.WARNING, logger="charmcraft")
    create_config("""
        type: charm
    """)

    config = load(tmp_path)

    assert config.bases == [
        BasesConfiguration(
            **{
                "build-on": [Base(name="ubuntu", channel="20.04")],
                "run-on": [Base(name="ubuntu", channel="20.04")],
            })
    ]
    assert "DEPRECATED: Bases configuration is now required." in [
        rec.message for rec in caplog.records
    ]
Beispiel #20
0
def test_bundle_parts_with_bundle_part_with_plugin(tmp_path, monkeypatch,
                                                   bundle_yaml):
    """Parts are declared with a bundle part that uses a different plugin.

    When the "parts" section exists in chamcraft.yaml and a part named "bundle"
    is defined with a plugin that's not "bundle", handle it as a regular part
    without populating fields for bundle building.
    """
    bundle_yaml(name="testbundle")
    (tmp_path / "README.md").write_text("test readme")

    charmcraft_file = tmp_path / "charmcraft.yaml"
    charmcraft_file.write_text(
        dedent("""
            type: bundle
            parts:
              bundle:
                plugin: nil
        """))

    config = load(tmp_path)

    monkeypatch.setenv("CHARMCRAFT_MANAGED_MODE", "1")
    with patch("charmcraft.parts.PartsLifecycle",
               autospec=True) as mock_lifecycle:
        mock_lifecycle.side_effect = SystemExit()
        with pytest.raises(SystemExit):
            PackCommand(config).run(get_namespace(shell_after=True))
    mock_lifecycle.assert_has_calls([
        call(
            {"bundle": {
                "plugin": "nil",
            }},
            work_dir=pathlib.Path("/root"),
            project_dir=tmp_path,
            project_name="testbundle",
            ignore_local_sources=["testbundle.zip"],
        )
    ])
Beispiel #21
0
def test_bundle_parts_not_defined(tmp_path, monkeypatch, bundle_yaml):
    """Parts are not defined.

    When the "parts" section does not exist, create an implicit "bundle" part and
    populate it with the default bundle building parameters.
    """
    bundle_yaml(name="testbundle")
    (tmp_path / "README.md").write_text("test readme")

    charmcraft_file = tmp_path / "charmcraft.yaml"
    charmcraft_file.write_text("type: bundle")

    config = load(tmp_path)

    monkeypatch.setenv("CHARMCRAFT_MANAGED_MODE", "1")
    with patch("charmcraft.parts.PartsLifecycle",
               autospec=True) as mock_lifecycle:
        mock_lifecycle.side_effect = SystemExit()
        with pytest.raises(SystemExit):
            PackCommand(config).run(get_namespace(shell_after=True))
    mock_lifecycle.assert_has_calls([
        call(
            {
                "bundle": {
                    "plugin": "bundle",
                    "source": str(tmp_path),
                    "prime": [
                        "bundle.yaml",
                        "README.md",
                    ],
                }
            },
            work_dir=pathlib.Path("/root"),
            project_dir=tmp_path,
            project_name="testbundle",
            ignore_local_sources=["testbundle.zip"],
        )
    ])
Beispiel #22
0
def test_bundle_parts_without_bundle_part(tmp_path, monkeypatch, bundle_yaml):
    """Parts are declared without a bundle part.

    When the "parts" section exists in chamcraft.yaml and a part named "bundle"
    is not defined, process parts normally and don't invoke the bundle plugin.
    """
    bundle_yaml(name="testbundle")
    (tmp_path / "README.md").write_text("test readme")

    charmcraft_file = tmp_path / "charmcraft.yaml"
    charmcraft_file.write_text(
        dedent("""
            type: bundle
            parts:
              foo:
                plugin: nil
        """))

    config = load(tmp_path)

    monkeypatch.setenv("CHARMCRAFT_MANAGED_MODE", "1")
    with patch("charmcraft.parts.PartsLifecycle",
               autospec=True) as mock_lifecycle:
        mock_lifecycle.side_effect = SystemExit()
        with pytest.raises(SystemExit):
            PackCommand(config).run(get_namespace(shell_after=True))
    mock_lifecycle.assert_has_calls([
        call(
            {"foo": {
                "plugin": "nil",
            }},
            work_dir=pathlib.Path("/root"),
            project_dir=tmp_path,
            ignore_local_sources=["testbundle.zip"],
        )
    ])
Beispiel #23
0
def main(argv=None):
    """Provide the main entry point."""
    if env.is_charmcraft_running_in_managed_mode():
        logpath = env.get_managed_environment_log_path()
    else:
        logpath = None

    emit.init(
        EmitterMode.NORMAL,
        "charmcraft",
        f"Starting charmcraft version {__version__}",
        log_filepath=logpath,
    )

    if argv is None:
        argv = sys.argv

    extra_global_options = [
        GlobalArgument(
            "project_dir",
            "option",
            "-p",
            "--project-dir",
            "Specify the project's directory (defaults to current)",
        ),
    ]

    # process
    try:
        setup_parts()

        # load the dispatcher and put everything in motion
        dispatcher = Dispatcher(
            "charmcraft",
            COMMAND_GROUPS,
            summary=GENERAL_SUMMARY,
            extra_global_args=extra_global_options,
        )
        global_args = dispatcher.pre_parse_args(argv[1:])
        loaded_config = config.load(global_args["project_dir"])
        command = dispatcher.load_command(loaded_config)
        if command.needs_config and not loaded_config.project.config_provided:
            raise ArgumentParsingError(
                "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"
            )
        emit.trace(_get_system_details())
        retcode = dispatcher.run()

    except ArgumentParsingError as err:
        print(err, file=sys.stderr)  # to stderr, as argparse normally does
        emit.ended_ok()
        retcode = 1
    except ProvideHelpException as err:
        print(err, file=sys.stderr)  # to stderr, as argparse normally does
        emit.ended_ok()
        retcode = 0
    except CraftError as err:
        emit.error(err)
        retcode = err.retcode
    except errors.CraftStoreError as err:
        error = CraftError(f"craft-store error: {err}")
        emit.error(error)
        retcode = 1
    except KeyboardInterrupt as exc:
        error = CraftError("Interrupted.")
        error.__cause__ = exc
        emit.error(error)
        retcode = 1
    except Exception as err:
        error = CraftError(f"charmcraft internal error: {err!r}")
        error.__cause__ = err
        emit.error(error)
        retcode = 1
    else:
        emit.ended_ok()
        if retcode is None:
            retcode = 0

    return retcode
Beispiel #24
0
def test_load_optional_charmcraft_missing(tmp_path):
    """Specify a directory where the file is missing."""
    config = load(tmp_path)
    assert config.project.dirpath == tmp_path
    assert not config.project.config_provided
Beispiel #25
0
    def _pre_parse_args(self, sysargs):
        """Pre-parse sys args.

        Several steps:

        - extract the global options and detects the possible command and its args

        - validate global options and apply them

        - validate that command is correct (NOT loading and parsing its arguments)
        """
        # get all arguments (default to what's specified) and those per options, to filter sysargs
        global_args = {}
        arg_per_option = {}
        options_with_equal = []
        for arg in GLOBAL_ARGS:
            arg_per_option[arg.short_option] = arg
            arg_per_option[arg.long_option] = arg
            if arg.type == 'flag':
                default = False
            elif arg.type == 'option':
                default = None
                options_with_equal.append(arg.long_option + '=')
            else:
                raise ValueError("Bad GLOBAL_ARGS structure.")
            global_args[arg.name] = default

        filtered_sysargs = []
        sysargs = iter(sysargs)
        options_with_equal = tuple(options_with_equal)
        for sysarg in sysargs:
            if sysarg in arg_per_option:
                arg = arg_per_option[sysarg]
                if arg.type == 'flag':
                    value = True
                else:
                    try:
                        value = next(sysargs)
                    except StopIteration:
                        raise CommandError(
                            "The 'project-dir' option expects one argument.")
                global_args[arg.name] = value
            elif sysarg.startswith(options_with_equal):
                option, value = sysarg.split('=', 1)
                if not value:
                    raise CommandError(
                        "The 'project-dir' option expects one argument.")
                arg = arg_per_option[option]
                global_args[arg.name] = value
            else:
                filtered_sysargs.append(sysarg)

        # control and use quiet/verbose options
        if global_args['quiet'] and global_args['verbose']:
            raise CommandError(
                "The 'verbose' and 'quiet' options are mutually exclusive.")
        if global_args['quiet']:
            message_handler.set_mode(message_handler.QUIET)
        elif global_args['verbose']:
            message_handler.set_mode(message_handler.VERBOSE)
        logger.debug("Raw pre-parsed sysargs: args=%s filtered=%s",
                     global_args, filtered_sysargs)

        # if help requested, transform the parameters to make that explicit
        if global_args['help']:
            command = HelpCommand.name
            cmd_args = filtered_sysargs
        elif filtered_sysargs:
            command = filtered_sysargs[0]
            cmd_args = filtered_sysargs[1:]
            if command not in self.commands:
                msg = "no such command {!r}".format(command)
                help_text = helptexts.get_usage_message('charmcraft', msg)
                raise CommandError(help_text, argsparsing=True)
        else:
            # no command!
            help_text = get_general_help()
            raise CommandError(help_text, argsparsing=True)

        # load the system's config
        charmcraft_config = config.load(global_args['project_dir'])

        logger.debug("General parsed sysargs: command=%r args=%s", command,
                     cmd_args)
        return command, cmd_args, charmcraft_config
Beispiel #26
0
 def check_schema_error(expected_msg):
     """The real checker."""
     with pytest.raises(CraftError) as cm:
         load(tmp_path)
     assert str(cm.value) == dedent(expected_msg)