Exemplo n.º 1
0
def test_build_generics_symlink_dir(tmp_path):
    """Respects a symlinked dir."""
    build_dir = tmp_path / BUILD_DIRNAME
    build_dir.mkdir()

    metadata = tmp_path / CHARM_METADATA
    metadata.write_text("name: crazycharm")
    entrypoint = tmp_path / "crazycharm.py"
    entrypoint.touch()
    somedir = tmp_path / "somedir"
    somedir.mkdir()
    somefile = somedir / "sanity check"
    somefile.touch()
    the_symlink = tmp_path / "thelink"
    the_symlink.symlink_to(somedir)

    builder = CharmBuilder(
        charmdir=tmp_path,
        builddir=build_dir,
        entrypoint=entrypoint,
    )
    builder.handle_generic_paths()

    built_symlink = build_dir / "thelink"
    assert built_symlink.is_symlink()
    assert built_symlink.resolve() == build_dir / "somedir"
    real_link = os.readlink(str(built_symlink))
    assert real_link == "somedir"

    # as a sanity check, the file inside the linked dir should exist
    assert (build_dir / "thelink" / "sanity check").exists()
Exemplo n.º 2
0
def test_build_generics_ignored_file(tmp_path, emitter):
    """Don't include ignored filed."""
    build_dir = tmp_path / BUILD_DIRNAME
    build_dir.mkdir()
    metadata = tmp_path / CHARM_METADATA
    metadata.write_text("name: crazycharm")

    # create two files (and the needed entrypoint)
    file1 = tmp_path / "file1.txt"
    file1.touch()
    file2 = tmp_path / "file2.txt"
    file2.touch()
    entrypoint = tmp_path / "crazycharm.py"
    entrypoint.touch()

    builder = CharmBuilder(
        charmdir=tmp_path,
        builddir=build_dir,
        entrypoint=entrypoint,
    )

    # set it up to ignore file 2 and make it work
    builder.ignore_rules.extend_patterns(["file2.*"])
    builder.handle_generic_paths()

    assert (build_dir / "file1.txt").exists()
    assert not (build_dir / "file2.txt").exists()

    expected = "Ignoring file because of rules: 'file2.txt'"
    emitter.assert_trace(expected)
Exemplo n.º 3
0
def test_build_dependencies_virtualenv_multiple(tmp_path):
    """A virtualenv is created with multiple requirements files."""
    metadata = tmp_path / CHARM_METADATA
    metadata.write_text("name: crazycharm")
    build_dir = tmp_path / BUILD_DIRNAME
    build_dir.mkdir()

    builder = CharmBuilder(
        charmdir=tmp_path,
        builddir=build_dir,
        entrypoint=pathlib.Path("whatever"),
        requirements=["reqs1.txt", "reqs2.txt"],
    )

    with patch("charmcraft.charm_builder.subprocess.run") as mock_run:
        mock_run.return_value.returncode = 1
        with patch("charmcraft.charm_builder._process_run") as mock:
            mock.return_value = 0
            builder.handle_dependencies()

    envpath = build_dir / VENV_DIRNAME
    assert mock.mock_calls == [
        call(["pip3", "--version"]),
        call([
            "pip3",
            "install",
            "--target={}".format(envpath),
            "--requirement=reqs1.txt",
            "--requirement=reqs2.txt",
        ]),
    ]
Exemplo n.º 4
0
def test_build_generics_symlink_deep(tmp_path):
    """Correctly re-links a symlink across deep dirs."""
    build_dir = tmp_path / BUILD_DIRNAME
    build_dir.mkdir()

    metadata = tmp_path / CHARM_METADATA
    metadata.write_text("name: crazycharm")
    entrypoint = tmp_path / "crazycharm.py"
    entrypoint.touch()

    dir1 = tmp_path / "dir1"
    dir1.mkdir()
    dir2 = tmp_path / "dir2"
    dir2.mkdir()
    original_target = dir1 / "file.real"
    original_target.touch()
    the_symlink = dir2 / "file.link"
    the_symlink.symlink_to(original_target)

    builder = CharmBuilder(
        charmdir=tmp_path,
        builddir=build_dir,
        entrypoint=entrypoint,
    )
    builder.handle_generic_paths()

    built_symlink = build_dir / "dir2" / "file.link"
    assert built_symlink.is_symlink()
    assert built_symlink.resolve() == build_dir / "dir1" / "file.real"
    real_link = os.readlink(str(built_symlink))
    assert real_link == "../dir1/file.real"
Exemplo n.º 5
0
def test_build_generics_ignored_dir(tmp_path, caplog):
    """Don't include ignored dir."""
    caplog.set_level(logging.DEBUG)
    build_dir = tmp_path / BUILD_DIRNAME
    build_dir.mkdir()
    metadata = tmp_path / CHARM_METADATA
    metadata.write_text("name: crazycharm")

    # create two files (and the needed entrypoint)
    dir1 = tmp_path / "dir1"
    dir1.mkdir()
    dir2 = tmp_path / "dir2"
    dir2.mkdir()
    entrypoint = tmp_path / "crazycharm.py"
    entrypoint.touch()

    builder = CharmBuilder(
        charmdir=tmp_path,
        builddir=build_dir,
        entrypoint=entrypoint,
    )

    # set it up to ignore dir 2 and make it work
    builder.ignore_rules.extend_patterns(["dir2"])
    builder.handle_generic_paths()

    assert (build_dir / "dir1").exists()
    assert not (build_dir / "dir2").exists()

    expected = "Ignoring directory because of rules: 'dir2'"
    assert expected in [rec.message for rec in caplog.records]
Exemplo n.º 6
0
def test_build_dependencies_needs_system(tmp_path, config):
    """pip3 is called with --system when pip3 needs it."""
    metadata = tmp_path / CHARM_METADATA
    metadata.write_text("name: crazycharm")
    build_dir = tmp_path / BUILD_DIRNAME
    build_dir.mkdir()

    builder = CharmBuilder(
        charmdir=tmp_path,
        builddir=build_dir,
        entrypoint=pathlib.Path("whatever"),
        requirements=["reqs"],
    )

    with patch("charmcraft.charm_builder.subprocess.run") as mock_run:
        mock_run.return_value.returncode = 0
        with patch("charmcraft.charm_builder._process_run") as mock:
            mock.return_value = 0
            builder.handle_dependencies()

    envpath = build_dir / VENV_DIRNAME
    assert mock.mock_calls == [
        call(["pip3", "--version"]),
        call([
            "pip3",
            "install",
            "--target={}".format(envpath),
            "--system",
            "--requirement=reqs",
        ]),
    ]
Exemplo n.º 7
0
def test_build_generics_different_filetype(tmp_path, emitter, monkeypatch):
    """Ignores whatever is not a regular file, symlink or dir."""
    # change into the tmp path and do everything locally, because otherwise the socket path
    # will be too long for mac os
    monkeypatch.chdir(tmp_path)

    metadata = tmp_path / CHARM_METADATA
    metadata.write_text("name: crazycharm")
    build_dir = pathlib.Path(BUILD_DIRNAME)
    build_dir.mkdir()
    entrypoint = pathlib.Path("crazycharm.py")
    entrypoint.touch()

    # create a socket
    sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    sock.bind("test-socket")

    builder = CharmBuilder(
        charmdir=tmp_path,
        builddir=build_dir,
        entrypoint=tmp_path / entrypoint,
    )
    builder.handle_generic_paths()

    assert not (build_dir / "test-socket").exists()
    expected = "Ignoring file because of type: 'test-socket'"
    emitter.assert_trace(expected)
Exemplo n.º 8
0
def test_build_generics_symlink_directory_outside(tmp_path, emitter):
    """Ignores (with warning) a symlink pointing a dir outside projects dir."""
    project_dir = tmp_path / "test-project"
    project_dir.mkdir()

    metadata = project_dir / CHARM_METADATA
    metadata.write_text("name: crazycharm")
    build_dir = project_dir / BUILD_DIRNAME
    build_dir.mkdir()
    entrypoint = project_dir / "crazycharm.py"
    entrypoint.touch()

    outside_project = tmp_path / "dangerous"
    outside_project.mkdir()
    the_symlink = project_dir / "external-dir"
    the_symlink.symlink_to(outside_project)

    builder = CharmBuilder(
        charmdir=project_dir,
        builddir=build_dir,
        entrypoint=entrypoint,
    )
    builder.handle_generic_paths()

    assert not (build_dir / "external-dir").exists()
    expected = "Ignoring symlink because targets outside the project: 'external-dir'"
    emitter.assert_trace(expected)
Exemplo n.º 9
0
def test_build_dispatcher_classic_hooks_linking_charm_replaced(tmp_path, emitter):
    """Hooks that are just a symlink to the entrypoint are replaced."""
    metadata = tmp_path / CHARM_METADATA
    metadata.write_text("name: crazycharm")
    build_dir = tmp_path / BUILD_DIRNAME
    build_dir.mkdir()

    # simple source code
    src_dir = build_dir / "src"
    src_dir.mkdir()
    built_charm_script = src_dir / "charm.py"
    with built_charm_script.open("wb") as fh:
        fh.write(b"all the magic")

    # a test hook, just a symlink to the charm
    built_hooks_dir = build_dir / "hooks"
    built_hooks_dir.mkdir()
    test_hook = built_hooks_dir / "somehook"
    test_hook.symlink_to(built_charm_script)

    included_dispatcher = build_dir / DISPATCH_FILENAME

    builder = CharmBuilder(
        charmdir=tmp_path,
        builddir=build_dir,
        entrypoint=pathlib.Path("whatever"),
    )
    builder.handle_dispatcher(built_charm_script)

    # the test hook is still there and a symlink, but now pointing to the dispatcher
    assert test_hook.is_symlink()
    assert test_hook.resolve() == included_dispatcher
    expected = "Replacing existing hook 'somehook' as it's a symlink to the entrypoint"
    emitter.assert_trace(expected)
Exemplo n.º 10
0
def test_build_dispatcher_classic_hooks_mandatory_respected(tmp_path):
    """The already included mandatory classic hooks are left untouched."""
    metadata = tmp_path / CHARM_METADATA
    metadata.write_text("name: crazycharm")
    build_dir = tmp_path / BUILD_DIRNAME
    build_dir.mkdir()

    built_hooks_dir = build_dir / "hooks"
    built_hooks_dir.mkdir()
    test_hook = built_hooks_dir / "testhook"
    with test_hook.open("wb") as fh:
        fh.write(b"abc")

    linked_entrypoint = build_dir / "somestuff.py"

    builder = CharmBuilder(
        charmdir=tmp_path,
        builddir=build_dir,
        entrypoint=pathlib.Path("whatever"),
    )
    with patch("charmcraft.charm_builder.MANDATORY_HOOK_NAMES", {"testhook"}):
        builder.handle_dispatcher(linked_entrypoint)

    with test_hook.open("rb") as fh:
        assert fh.read() == b"abc"
Exemplo n.º 11
0
def test_build_dependencies_virtualenv_binary_packages(tmp_path):
    """A virtualenv is created with the specified packages."""
    metadata = tmp_path / CHARM_METADATA
    metadata.write_text("name: crazycharm")
    build_dir = tmp_path / BUILD_DIRNAME
    build_dir.mkdir()

    builder = CharmBuilder(
        charmdir=tmp_path,
        builddir=build_dir,
        entrypoint=pathlib.Path("whatever"),
        binary_python_packages=["pkg1", "pkg2"],
        python_packages=[],
        requirements=[],
    )

    with patch("charmcraft.charm_builder._process_run") as mock:
        with patch("shutil.copytree") as mock_copytree:
            builder.handle_dependencies()

    pip_cmd = str(charm_builder._find_venv_bin(tmp_path / STAGING_VENV_DIRNAME, "pip3"))

    assert mock.mock_calls == [
        call(["python3", "-m", "venv", str(tmp_path / STAGING_VENV_DIRNAME)]),
        call([pip_cmd, "--version"]),
        call([pip_cmd, "install", "--upgrade", "pkg1", "pkg2"]),
    ]

    site_packages_dir = charm_builder._find_venv_site_packages(pathlib.Path(STAGING_VENV_DIRNAME))
    assert mock_copytree.mock_calls == [call(site_packages_dir, build_dir / VENV_DIRNAME)]
Exemplo n.º 12
0
def test_build_generics_symlink_file_outside(tmp_path, caplog):
    """Ignores (with warning) a symlink pointing a file outside projects dir."""
    caplog.set_level(logging.WARNING)

    project_dir = tmp_path / "test-project"
    project_dir.mkdir()

    metadata = project_dir / CHARM_METADATA
    metadata.write_text("name: crazycharm")
    build_dir = project_dir / BUILD_DIRNAME
    build_dir.mkdir()
    entrypoint = project_dir / "crazycharm.py"
    entrypoint.touch()

    outside_project = tmp_path / "dangerous.txt"
    outside_project.touch()
    the_symlink = project_dir / "external-file"
    the_symlink.symlink_to(outside_project)

    builder = CharmBuilder(
        charmdir=project_dir,
        builddir=build_dir,
        entrypoint=entrypoint,
    )
    builder.handle_generic_paths()

    assert not (build_dir / "external-file").exists()
    expected = "Ignoring symlink because targets outside the project: 'external-file'"
    assert expected in [rec.message for rec in caplog.records]
Exemplo n.º 13
0
def test_build_generics_simple_files(tmp_path):
    """Check transferred metadata and simple entrypoint, also return proper linked entrypoint."""
    build_dir = tmp_path / BUILD_DIRNAME
    build_dir.mkdir()

    metadata = tmp_path / CHARM_METADATA
    metadata.write_text("name: crazycharm")

    entrypoint = tmp_path / "crazycharm.py"
    entrypoint.touch()

    builder = CharmBuilder(
        charmdir=tmp_path,
        builddir=build_dir,
        entrypoint=entrypoint,
    )
    linked_entrypoint = builder.handle_generic_paths()

    # check files are there, are files, and are really hard links (so no
    # check for permissions needed)
    built_metadata = build_dir / CHARM_METADATA
    assert built_metadata.is_file()
    assert built_metadata.stat().st_ino == metadata.stat().st_ino

    built_entrypoint = build_dir / "crazycharm.py"
    assert built_entrypoint.is_file()
    assert built_entrypoint.stat().st_ino == entrypoint.stat().st_ino

    assert linked_entrypoint == built_entrypoint
Exemplo n.º 14
0
def test_build_dependencies_virtualenv_all(tmp_path, emitter):
    """A virtualenv is created with the specified packages."""
    build_dir = tmp_path / BUILD_DIRNAME
    build_dir.mkdir()

    reqs_file_1 = tmp_path / "reqs.txt"
    reqs_file_1.touch()
    reqs_file_2 = tmp_path / "reqs.txt"
    reqs_file_1.touch()

    builder = CharmBuilder(
        charmdir=tmp_path,
        builddir=build_dir,
        entrypoint=pathlib.Path("whatever"),
        binary_python_packages=["pkg1", "pkg2"],
        python_packages=["pkg3", "pkg4"],
        requirements=[reqs_file_1, reqs_file_2],
    )

    with patch("charmcraft.charm_builder._process_run") as mock:
        with patch("shutil.copytree") as mock_copytree:
            builder.handle_dependencies()

    pip_cmd = str(
        charm_builder._find_venv_bin(tmp_path / STAGING_VENV_DIRNAME, "pip3"))

    assert mock.mock_calls == [
        call(["python3", "-m", "venv",
              str(tmp_path / STAGING_VENV_DIRNAME)]),
        call([pip_cmd, "--version"]),
        call([pip_cmd, "install", "--upgrade", "pkg1", "pkg2"]),
        call([
            pip_cmd, "install", "--upgrade", "--no-binary", ":all:", "pkg3",
            "pkg4"
        ]),
        call([
            pip_cmd,
            "install",
            "--upgrade",
            "--no-binary",
            ":all:",
            f"--requirement={reqs_file_1}",
            f"--requirement={reqs_file_2}",
        ]),
    ]

    site_packages_dir = charm_builder._find_venv_site_packages(
        pathlib.Path(STAGING_VENV_DIRNAME))
    assert mock_copytree.mock_calls == [
        call(site_packages_dir, build_dir / VENV_DIRNAME)
    ]
    emitter.assert_trace("Handling dependencies")
    emitter.assert_progress("Installing dependencies")
Exemplo n.º 15
0
def test_builder_without_jujuignore(tmp_path):
    """Without a .jujuignore we still have a default set of ignores"""
    metadata = tmp_path / CHARM_METADATA
    metadata.write_text("name: crazycharm")
    build_dir = tmp_path / BUILD_DIRNAME
    build_dir.mkdir()

    builder = CharmBuilder(
        charmdir=tmp_path,
        builddir=build_dir,
        entrypoint=pathlib.Path("whatever"),
    )
    ignore = builder._load_juju_ignore()
    assert ignore.match("/.git", is_dir=True)
    assert ignore.match("/build", is_dir=True)
    assert not ignore.match("myfile.py", is_dir=False)
Exemplo n.º 16
0
def test_build_dependencies_virtualenv_none(tmp_path):
    """The virtualenv is NOT created if no needed."""
    metadata = tmp_path / CHARM_METADATA
    metadata.write_text("name: crazycharm")
    build_dir = tmp_path / BUILD_DIRNAME
    build_dir.mkdir()

    builder = CharmBuilder(
        charmdir=tmp_path,
        builddir=build_dir,
        entrypoint=pathlib.Path("whatever"),
        requirements=[],
    )

    with patch("charmcraft.charm_builder.subprocess.run") as mock_run:
        builder.handle_dependencies()

    mock_run.assert_not_called()
Exemplo n.º 17
0
def test_build_dependencies_virtualenv_error_basicpip(tmp_path):
    """Process is properly interrupted if using pip fails."""
    metadata = tmp_path / CHARM_METADATA
    metadata.write_text("name: crazycharm")
    build_dir = tmp_path / BUILD_DIRNAME
    build_dir.mkdir()

    builder = CharmBuilder(
        charmdir=tmp_path,
        builddir=build_dir,
        entrypoint=pathlib.Path("whatever"),
        requirements=["something"],
    )

    with patch("charmcraft.charm_builder._process_run") as mock:
        mock.return_value = -7
        with pytest.raises(CommandError, match="problems using pip"):
            builder.handle_dependencies()
Exemplo n.º 18
0
def test_build_dependencies_virtualenv_error_installing(tmp_path):
    """Process is properly interrupted if virtualenv creation fails."""
    metadata = tmp_path / CHARM_METADATA
    metadata.write_text("name: crazycharm")
    build_dir = tmp_path / BUILD_DIRNAME
    build_dir.mkdir()

    builder = CharmBuilder(
        charmdir=tmp_path,
        builddir=build_dir,
        entrypoint=pathlib.Path("whatever"),
        requirements=["something"],
    )

    with patch("charmcraft.charm_builder._process_run") as mock:
        mock.side_effect = [0, -7]
        with pytest.raises(CommandError,
                           match="problems installing dependencies"):
            builder.handle_dependencies()
Exemplo n.º 19
0
def test_build_dependencies_no_reused_missing_hash_file(tmp_path, emitter):
    """Dependencies are built again because previous hash file was not found."""
    build_dir = tmp_path / BUILD_DIRNAME
    build_dir.mkdir()

    builder = CharmBuilder(
        charmdir=tmp_path,
        builddir=build_dir,
        entrypoint=pathlib.Path("whatever"),
        binary_python_packages=[],
        python_packages=["ops"],
        requirements=[],
    )
    staging_venv_dir = tmp_path / STAGING_VENV_DIRNAME

    # patch the dependencies installation method so it skips all subprocessing but actually
    # creates the directory, to simplify testing
    builder._install_dependencies = lambda dirpath: dirpath.mkdir(exist_ok=True
                                                                  )

    # first run!
    with patch("shutil.copytree") as mock_copytree:
        builder.handle_dependencies()
    emitter.assert_trace("Handling dependencies")
    emitter.assert_trace("Dependencies directory not found")
    emitter.assert_progress("Installing dependencies")

    # directory created and packages installed
    assert staging_venv_dir.exists()

    # installation directory copied to the build directory
    site_packages_dir = charm_builder._find_venv_site_packages(
        pathlib.Path(STAGING_VENV_DIRNAME))
    assert mock_copytree.mock_calls == [
        call(site_packages_dir, build_dir / VENV_DIRNAME)
    ]

    # remove the hash file
    (tmp_path / DEPENDENCIES_HASH_FILENAME).unlink()

    # second run!
    emitter.interactions.clear()
    with patch("shutil.copytree") as mock_copytree:
        builder.handle_dependencies()
    emitter.assert_trace("Handling dependencies")
    emitter.assert_trace("Dependencies hash file not found")
    emitter.assert_progress("Installing dependencies")

    # directory created and packages installed *again*
    assert staging_venv_dir.exists()

    # installation directory copied *again* to the build directory
    site_packages_dir = charm_builder._find_venv_site_packages(
        pathlib.Path(STAGING_VENV_DIRNAME))
    assert mock_copytree.mock_calls == [
        call(site_packages_dir, build_dir / VENV_DIRNAME)
    ]
Exemplo n.º 20
0
def test_build_dependencies_virtualenv_none(tmp_path, emitter):
    """The virtualenv is NOT created if no needed."""
    build_dir = tmp_path / BUILD_DIRNAME
    build_dir.mkdir()

    builder = CharmBuilder(
        charmdir=tmp_path,
        builddir=build_dir,
        entrypoint=pathlib.Path("whatever"),
        binary_python_packages=[],
        python_packages=[],
        requirements=[],
    )

    with patch("charmcraft.charm_builder.subprocess.run") as mock_run:
        builder.handle_dependencies()

    mock_run.assert_not_called()
    emitter.assert_trace("Handling dependencies")
    emitter.assert_trace("No dependencies to handle")
Exemplo n.º 21
0
def test_build_dispatcher_modern_dispatch_respected(tmp_path):
    """The already included dispatcher script is left untouched."""
    metadata = tmp_path / CHARM_METADATA
    metadata.write_text("name: crazycharm")
    build_dir = tmp_path / BUILD_DIRNAME
    build_dir.mkdir()

    already_present_dispatch = build_dir / DISPATCH_FILENAME
    with already_present_dispatch.open("wb") as fh:
        fh.write(b"abc")

    builder = CharmBuilder(
        charmdir=tmp_path,
        builddir=build_dir,
        entrypoint=pathlib.Path("whatever"),
    )
    builder.handle_dispatcher("whatever")

    with already_present_dispatch.open("rb") as fh:
        assert fh.read() == b"abc"
Exemplo n.º 22
0
def test_build_dispatcher_modern_dispatch_created(tmp_path):
    """The dispatcher script is properly built."""
    metadata = tmp_path / CHARM_METADATA
    metadata.write_text("name: crazycharm")
    build_dir = tmp_path / BUILD_DIRNAME
    build_dir.mkdir()

    linked_entrypoint = build_dir / "somestuff.py"

    builder = CharmBuilder(
        charmdir=tmp_path,
        builddir=build_dir,
        entrypoint=pathlib.Path("whatever"),
    )
    builder.handle_dispatcher(linked_entrypoint)

    included_dispatcher = build_dir / DISPATCH_FILENAME
    with included_dispatcher.open("rt", encoding="utf8") as fh:
        dispatcher_code = fh.read()
    assert dispatcher_code == DISPATCH_CONTENT.format(entrypoint_relative_path="somestuff.py")
Exemplo n.º 23
0
def test_build_dependencies_virtualenv_simple(tmp_path):
    """A virtualenv is created with the specified requirements file."""
    metadata = tmp_path / CHARM_METADATA
    metadata.write_text("name: crazycharm")
    build_dir = tmp_path / BUILD_DIRNAME
    build_dir.mkdir()

    builder = CharmBuilder(
        charmdir=tmp_path,
        builddir=build_dir,
        entrypoint=pathlib.Path("whatever"),
        requirements=["reqs.txt"],
    )

    with patch("charmcraft.charm_builder.subprocess.run") as mock_run:
        mock_run.return_value.returncode = 1
        with patch("charmcraft.charm_builder._process_run") as mock:
            mock.return_value = 0
            builder.handle_dependencies()

    envpath = build_dir / VENV_DIRNAME
    assert mock.mock_calls == [
        call(["pip3", "--version"]),
        call([
            "pip3", "install", "--target={}".format(envpath),
            "--requirement=reqs.txt"
        ]),
    ]
    assert mock_run.mock_calls == [
        call(
            [
                "python3",
                "-c",
                ("from pip.commands.install import InstallCommand; "
                 'assert InstallCommand().cmd_opts.get_option("--system") is not None'
                 ),
            ],
            stdout=-3,
            stderr=-3,
        ),
    ]
Exemplo n.º 24
0
def test_build_dependencies_reused(tmp_path, emitter):
    """Happy case to reuse dependencies from last run."""
    build_dir = tmp_path / BUILD_DIRNAME
    build_dir.mkdir()

    reqs_file = tmp_path / "reqs.txt"
    reqs_file.touch()

    builder = CharmBuilder(
        charmdir=tmp_path,
        builddir=build_dir,
        entrypoint=pathlib.Path("whatever"),
        binary_python_packages=[],
        python_packages=[],
        requirements=[reqs_file],
    )
    staging_venv_dir = tmp_path / STAGING_VENV_DIRNAME

    # patch the dependencies installation method so it skips all subprocessing but actually
    # creates the directory, to simplify testing; note that we specifically are calling mkdir
    # to fail if the directory is already there, so we ensure it is called once
    builder._install_dependencies = lambda dirpath: dirpath.mkdir(exist_ok=
                                                                  False)

    # first run!
    with patch("shutil.copytree") as mock_copytree:
        builder.handle_dependencies()
    emitter.assert_trace("Handling dependencies")
    emitter.assert_trace("Dependencies directory not found")
    emitter.assert_progress("Installing dependencies")

    # directory created and packages installed
    assert staging_venv_dir.exists()

    # installation directory copied to the build directory
    site_packages_dir = charm_builder._find_venv_site_packages(
        pathlib.Path(STAGING_VENV_DIRNAME))
    assert mock_copytree.mock_calls == [
        call(site_packages_dir, build_dir / VENV_DIRNAME)
    ]

    # second run!
    emitter.interactions.clear()
    with patch("shutil.copytree") as mock_copytree:
        builder.handle_dependencies()
    emitter.assert_trace("Handling dependencies")
    emitter.assert_trace(
        "Reusing installed dependencies, they are equal to last run ones")

    # installation directory copied *again* to the build directory (this is always done as
    # buildpath is cleaned)
    site_packages_dir = charm_builder._find_venv_site_packages(
        pathlib.Path(STAGING_VENV_DIRNAME))
    assert mock_copytree.mock_calls == [
        call(site_packages_dir, build_dir / VENV_DIRNAME)
    ]
Exemplo n.º 25
0
def test_builder_with_jujuignore(tmp_path):
    """With a .jujuignore we will include additional ignores."""
    metadata = tmp_path / CHARM_METADATA
    metadata.write_text("name: crazycharm")
    build_dir = tmp_path / BUILD_DIRNAME
    build_dir.mkdir()
    with (tmp_path / ".jujuignore").open("w", encoding="utf-8") as ignores:
        ignores.write("*.py\n" "/h\xef.txt\n")

    builder = CharmBuilder(
        charmdir=tmp_path,
        builddir=build_dir,
        entrypoint=pathlib.Path("whatever"),
    )
    ignore = builder._load_juju_ignore()
    assert ignore.match("/.git", is_dir=True)
    assert ignore.match("/build", is_dir=True)
    assert ignore.match("myfile.py", is_dir=False)
    assert not ignore.match("hi.txt", is_dir=False)
    assert ignore.match("h\xef.txt", is_dir=False)
    assert not ignore.match("myfile.c", is_dir=False)
Exemplo n.º 26
0
def test_build_generics_simple_dir(tmp_path):
    """Check transferred any directory, with proper permissions."""
    build_dir = tmp_path / BUILD_DIRNAME
    build_dir.mkdir()
    entrypoint = tmp_path / "crazycharm.py"
    entrypoint.touch()
    metadata = tmp_path / CHARM_METADATA
    metadata.write_text("name: crazycharm")

    somedir = tmp_path / "somedir"
    somedir.mkdir(mode=0o700)

    builder = CharmBuilder(
        charmdir=tmp_path,
        builddir=build_dir,
        entrypoint=entrypoint,
    )
    builder.handle_generic_paths()

    built_dir = build_dir / "somedir"
    assert built_dir.is_dir()
    assert built_dir.stat().st_mode & 0xFFF == 0o700
Exemplo n.º 27
0
def test_build_dispatcher_classic_hooks_mandatory_created(tmp_path):
    """The mandatory classic hooks are implemented ok if not present."""
    metadata = tmp_path / CHARM_METADATA
    metadata.write_text("name: crazycharm")
    build_dir = tmp_path / BUILD_DIRNAME
    build_dir.mkdir()

    linked_entrypoint = build_dir / "somestuff.py"
    included_dispatcher = build_dir / DISPATCH_FILENAME

    builder = CharmBuilder(
        charmdir=tmp_path,
        builddir=build_dir,
        entrypoint=pathlib.Path("whatever"),
    )
    with patch("charmcraft.charm_builder.MANDATORY_HOOK_NAMES", {"testhook"}):
        builder.handle_dispatcher(linked_entrypoint)

    test_hook = build_dir / "hooks" / "testhook"
    assert test_hook.is_symlink()
    assert test_hook.resolve() == included_dispatcher
    real_link = os.readlink(str(test_hook))
    assert real_link == os.path.join("..", DISPATCH_FILENAME)
Exemplo n.º 28
0
def test_build_generics_symlink_file(tmp_path):
    """Respects a symlinked file."""
    build_dir = tmp_path / BUILD_DIRNAME
    build_dir.mkdir()

    metadata = tmp_path / CHARM_METADATA
    metadata.write_text("name: crazycharm")
    entrypoint = tmp_path / "crazycharm.py"
    entrypoint.touch()
    the_symlink = tmp_path / "somehook.py"
    the_symlink.symlink_to(entrypoint)

    builder = CharmBuilder(
        charmdir=tmp_path,
        builddir=build_dir,
        entrypoint=entrypoint,
    )
    builder.handle_generic_paths()

    built_symlink = build_dir / "somehook.py"
    assert built_symlink.is_symlink()
    assert built_symlink.resolve() == build_dir / "crazycharm.py"
    real_link = os.readlink(str(built_symlink))
    assert real_link == "crazycharm.py"
Exemplo n.º 29
0
def test_build_dependencies_no_reused_problematic_hash_file(tmp_path, emitter):
    """Dependencies are built again because having problems to read the previous hash file."""
    build_dir = tmp_path / BUILD_DIRNAME
    build_dir.mkdir()

    builder = CharmBuilder(
        charmdir=tmp_path,
        builddir=build_dir,
        entrypoint=pathlib.Path("whatever"),
        binary_python_packages=[],
        python_packages=["ops"],
        requirements=[],
    )
    staging_venv_dir = tmp_path / STAGING_VENV_DIRNAME

    # patch the dependencies installation method so it skips all subprocessing but actually
    # creates the directory, to simplify testing
    builder._install_dependencies = lambda dirpath: dirpath.mkdir(exist_ok=True
                                                                  )

    # first run!
    with patch("shutil.copytree") as mock_copytree:
        builder.handle_dependencies()
    emitter.assert_trace("Handling dependencies")
    emitter.assert_trace("Dependencies directory not found")
    emitter.assert_progress("Installing dependencies")

    # directory created and packages installed
    assert staging_venv_dir.exists()

    # installation directory copied to the build directory
    site_packages_dir = charm_builder._find_venv_site_packages(
        pathlib.Path(STAGING_VENV_DIRNAME))
    assert mock_copytree.mock_calls == [
        call(site_packages_dir, build_dir / VENV_DIRNAME)
    ]

    # avoid the file to be read succesfully
    (tmp_path / DEPENDENCIES_HASH_FILENAME).write_bytes(
        b"\xc3\x28")  # invalid UTF8

    # second run!
    emitter.interactions.clear()
    with patch("shutil.copytree") as mock_copytree:
        builder.handle_dependencies()
    emitter.assert_trace("Handling dependencies")
    emitter.assert_trace(
        "Problems reading the dependencies hash file: "
        "'utf-8' codec can't decode byte 0xc3 in position 0: invalid continuation byte"
    )
    emitter.assert_progress("Installing dependencies")

    # directory created and packages installed *again*
    assert staging_venv_dir.exists()

    # installation directory copied *again* to the build directory
    site_packages_dir = charm_builder._find_venv_site_packages(
        pathlib.Path(STAGING_VENV_DIRNAME))
    assert mock_copytree.mock_calls == [
        call(site_packages_dir, build_dir / VENV_DIRNAME)
    ]
Exemplo n.º 30
0
def _test_build_generics_tree(tmp_path, *, expect_hardlinks):
    build_dir = tmp_path / BUILD_DIRNAME
    build_dir.mkdir()

    # create this structure:
    # ├─ crazycharm.py  (entrypoint)
    # ├─ file1.txt
    # ├─ dir1
    # │  └─ dir3  (ignored!)
    # └─ dir2
    #    ├─ file2.txt
    #    ├─ file3.txt  (ignored!)
    #    ├─ dir4  (ignored!)
    #    │   └─ file4.txt
    #    └─ dir5
    entrypoint = tmp_path / "crazycharm.py"
    entrypoint.touch()
    metadata = tmp_path / CHARM_METADATA
    metadata.write_text("name: crazycharm")
    file1 = tmp_path / "file1.txt"
    file1.touch()
    dir1 = tmp_path / "dir1"
    dir1.mkdir()
    dir3 = dir1 / "dir3"
    dir3.mkdir()
    dir2 = tmp_path / "dir2"
    dir2.mkdir()
    file2 = dir2 / "file2.txt"
    file2.touch()
    file3 = dir2 / "file3.txt"
    file3.touch()
    dir4 = dir2 / "dir4"
    dir4.mkdir()
    file4 = dir4 / "file4.txt"
    file4.touch()
    dir5 = dir2 / "dir5"
    dir5.mkdir()

    builder = CharmBuilder(
        charmdir=tmp_path,
        builddir=build_dir,
        entrypoint=entrypoint,
    )

    # set it up to ignore some stuff and make it work
    builder.ignore_rules.extend_patterns(
        [
            "dir1/dir3",
            "dir2/file3.txt",
            "dir2/dir4",
        ]
    )
    builder.handle_generic_paths()

    assert (build_dir / "crazycharm.py").exists()
    assert (build_dir / "file1.txt").exists()
    assert (build_dir / "dir1").exists()
    assert not (build_dir / "dir1" / "dir3").exists()
    assert (build_dir / "dir2").exists()
    assert (build_dir / "dir2" / "file2.txt").exists()
    assert not (build_dir / "dir2" / "file3.txt").exists()
    assert not (build_dir / "dir2" / "dir4").exists()
    assert (build_dir / "dir2" / "dir5").exists()

    for (p1, p2) in [
        (build_dir / "crazycharm.py", entrypoint),
        (build_dir / "file1.txt", file1),
        (build_dir / "dir2" / "file2.txt", file2),
    ]:
        if expect_hardlinks:
            # they're hard links
            assert p1.samefile(p2)
        else:
            # they're *not* hard links
            assert not p1.samefile(p2)
            # but they're essentially the same
            assert filecmp.cmp(str(p1), str(p2), shallow=False)
            assert p1.stat().st_mode == p2.stat().st_mode
            assert p1.stat().st_size == p2.stat().st_size
            assert p1.stat().st_atime == pytest.approx(p2.stat().st_atime)
            assert p1.stat().st_mtime == pytest.approx(p2.stat().st_mtime)