Skip to content

Commit

Permalink
Add tests for Docker user mapping inspection
Browse files Browse the repository at this point in the history
  • Loading branch information
rmartin16 committed Jul 5, 2023
1 parent d219035 commit 5ed3010
Show file tree
Hide file tree
Showing 11 changed files with 230 additions and 72 deletions.
44 changes: 42 additions & 2 deletions tests/integrations/docker/conftest.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import subprocess
from unittest.mock import MagicMock
from pathlib import PurePosixPath
from unittest.mock import MagicMock, call

import pytest

from briefcase.config import AppConfig
from briefcase.integrations.base import ToolCache
from briefcase.integrations.docker import DockerAppContext
from briefcase.integrations.docker import Path as PathForMockingDocker


@pytest.fixture
def mock_tools(mock_tools) -> ToolCache:
def mock_tools(mock_tools, tmp_path, monkeypatch) -> ToolCache:
# Mock stdlib subprocess module
mock_tools.subprocess._subprocess = MagicMock(spec_set=subprocess)

Expand All @@ -19,6 +21,9 @@ def mock_tools(mock_tools) -> ToolCache:
mock_tools.os.getuid.return_value = "37"
mock_tools.os.getgid.return_value = "42"

# Set the cwd for Docker to pytest's temp dir for user mapping inspection
monkeypatch.setattr(PathForMockingDocker, "cwd", MagicMock(return_value=tmp_path))

# Mock return values for run
run_result = MagicMock(spec=subprocess.CompletedProcess, returncode=3)
mock_tools.subprocess._subprocess.run.return_value = run_result
Expand Down Expand Up @@ -66,3 +71,38 @@ def mock_docker_app_context(tmp_path, my_app, mock_tools) -> DockerAppContext:
mock_docker_app_context.tools.subprocess._subprocess.Popen.reset_mock()

return mock_docker_app_context


@pytest.fixture
def user_mapping_run_calls(tmp_path) -> list:
return [
call(
[
"docker",
"run",
"--rm",
"--volume",
f"{tmp_path / 'build'}:/host_write_test:z",
"alpine",
"touch",
PurePosixPath("/host_write_test/container_write_test"),
],
check=True,
stream_output=False,
),
call(
[
"docker",
"run",
"--rm",
"--volume",
f"{tmp_path / 'build'}:/host_write_test:z",
"alpine",
"rm",
"-f",
PurePosixPath("/host_write_test/container_write_test"),
],
check=True,
stream_output=False,
),
]
12 changes: 8 additions & 4 deletions tests/integrations/docker/test_DockerAppContext__verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def test_success(mock_tools, first_app_config, verify_kwargs):
def test_docker_verify_fail(mock_tools, first_app_config, verify_kwargs):
"""Failure if Docker cannot be verified."""
mock_tools.subprocess = MagicMock(spec_set=Subprocess)
# Mock the existence of Docker.
# Mock the absence of Docker
mock_tools.subprocess.check_output.side_effect = FileNotFoundError

with pytest.raises(BriefcaseCommandError, match="Briefcase requires Docker"):
Expand All @@ -99,9 +99,13 @@ def test_docker_image_build_fail(mock_tools, first_app_config, verify_kwargs):
"github.com/docker/buildx v0.10.2 00ed17d\n",
]

mock_tools.subprocess.run.side_effect = subprocess.CalledProcessError(
returncode=80, cmd=["docker" "build"]
)
mock_tools.subprocess.run.side_effect = [
# Mock the user mapping inspection calls
"",
"",
# Mock the image build failing
subprocess.CalledProcessError(returncode=80, cmd=["docker" "build"]),
]

with pytest.raises(
BriefcaseCommandError,
Expand Down
71 changes: 71 additions & 0 deletions tests/integrations/docker/test_Docker__cache_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import subprocess
from unittest.mock import MagicMock, call

import pytest

from briefcase.exceptions import BriefcaseCommandError
from briefcase.integrations.base import ToolCache
from briefcase.integrations.docker import Docker
from briefcase.integrations.subprocess import Subprocess


@pytest.fixture
def mock_tools(mock_tools) -> ToolCache:
mock_tools.subprocess = MagicMock(spec_set=Subprocess)
mock_tools.docker = Docker(mock_tools)
return mock_tools


def test_cache_image(mock_tools, user_mapping_run_calls):
"""A Docker image can be cached."""
# mock image not being cached in Docker
mock_tools.subprocess.check_output.return_value = ""

# Cache an image
mock_tools.docker.cache_image("ubuntu:jammy")

# Confirms that image is not available
mock_tools.subprocess.check_output.assert_called_once_with(
["docker", "images", "-q", "ubuntu:jammy"]
)

# Image is pulled and cached
mock_tools.subprocess.run.assert_has_calls(
user_mapping_run_calls + [call(["docker", "pull", "ubuntu:jammy"], check=True)]
)


def test_cache_image_already_cached(mock_tools, user_mapping_run_calls):
"""A Docker image is not pulled if it is already cached."""
# mock image already cached in Docker
mock_tools.subprocess.check_output.return_value = "99284ca6cea0"

# Cache an image
mock_tools.docker.cache_image("ubuntu:jammy")

# Confirms that image is not available
mock_tools.subprocess.check_output.assert_called_once_with(
["docker", "images", "-q", "ubuntu:jammy"]
)

# Image is not pulled and cached
mock_tools.subprocess.run.assert_has_calls(user_mapping_run_calls)


def test_cache_bad_image(mock_tools):
"""If an image is invalid, an exception raised."""
# mock image not being cached in Docker
mock_tools.subprocess.check_output.return_value = ""

# Mock a Docker failure due to a bad image
mock_tools.subprocess.run.side_effect = subprocess.CalledProcessError(
returncode=125,
cmd="docker...",
)

# Try to cache an image that doesn't exist:
with pytest.raises(
BriefcaseCommandError,
match=r"Unable to obtain the Docker image for ubuntu:does-not-exist.",
):
mock_tools.docker.cache_image("ubuntu:does-not-exist")
38 changes: 35 additions & 3 deletions tests/integrations/docker/test_Docker__check_output.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from unittest.mock import MagicMock
import subprocess
from unittest.mock import MagicMock, call

import pytest

Expand All @@ -16,10 +17,41 @@ def mock_tools(mock_tools) -> ToolCache:

def test_check_output(mock_tools):
"""A command can be invoked on a bare Docker image."""
# mock image already being cached in Docker
mock_tools.subprocess.check_output.side_effect = ["1ed313b0551f", "output"]

# Run the command in a container
mock_tools.docker.check_output(["cmd", "arg1", "arg2"], image_tag="ubuntu:jammy")

mock_tools.subprocess.check_output.assert_called_once_with(
["docker", "run", "--rm", "ubuntu:jammy", "cmd", "arg1", "arg2"]
mock_tools.subprocess.check_output.assert_has_calls(
[
# Verify image is cached in Docker
call(["docker", "images", "-q", "ubuntu:jammy"]),
# Run command in Docker using image
call(["docker", "run", "--rm", "ubuntu:jammy", "cmd", "arg1", "arg2"]),
]
)


def test_check_output_fail(mock_tools):
"""Any subprocess errors are passed back through directly."""
# mock image already being cached in Docker and check_output() call fails
mock_tools.subprocess.check_output.side_effect = [
"1ed313b0551f",
subprocess.CalledProcessError(returncode=1, cmd=["cmd", "arg1", "arg2"]),
]

# The CalledProcessError surfaces from Docker().check_output()
with pytest.raises(subprocess.CalledProcessError):
mock_tools.docker.check_output(
["cmd", "arg1", "arg2"], image_tag="ubuntu:jammy"
)

mock_tools.subprocess.check_output.assert_has_calls(
[
# Verify image is cached in Docker
call(["docker", "images", "-q", "ubuntu:jammy"]),
# Command errors in Docker using image
call(["docker", "run", "--rm", "ubuntu:jammy", "cmd", "arg1", "arg2"]),
]
)
52 changes: 0 additions & 52 deletions tests/integrations/docker/test_Docker__prepare.py

This file was deleted.

14 changes: 11 additions & 3 deletions tests/integrations/docker/test_Docker__verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ def test_docker_install_url(host_os):
assert host_os in Docker.DOCKER_INSTALL_URL


def test_docker_exists(mock_tools, valid_docker_version, capsys):
def test_docker_exists(
mock_tools, valid_docker_version, user_mapping_run_calls, capsys, tmp_path
):
"""If docker exists, the Docker wrapper is returned."""
# Mock the return value of Docker Version
mock_tools.subprocess.check_output.side_effect = [
Expand All @@ -70,6 +72,8 @@ def test_docker_exists(mock_tools, valid_docker_version, capsys):
]
)

mock_tools.subprocess.run.assert_has_calls(user_mapping_run_calls)

# No console output
output = capsys.readouterr()
assert output.out == ""
Expand All @@ -89,7 +93,7 @@ def test_docker_doesnt_exist(mock_tools):
mock_tools.subprocess.check_output.assert_called_with(["docker", "--version"])


def test_docker_failure(mock_tools, capsys):
def test_docker_failure(mock_tools, user_mapping_run_calls, capsys):
"""If docker failed during execution, the Docker wrapper is returned with a
warning."""
# Mock the return value of Docker Version
Expand All @@ -116,6 +120,8 @@ def test_docker_failure(mock_tools, capsys):
]
)

mock_tools.subprocess.run.assert_has_calls(user_mapping_run_calls)

# console output
output = capsys.readouterr()
assert "** WARNING: Unable to determine if Docker is installed" in output.out
Expand All @@ -136,7 +142,7 @@ def test_docker_bad_version(mock_tools, capsys):
Docker.verify(mock_tools)


def test_docker_unknown_version(mock_tools, capsys):
def test_docker_unknown_version(mock_tools, user_mapping_run_calls, capsys):
"""If docker exists but the version string doesn't make sense, the Docker wrapper is
returned with a warning."""
# Mock a bad return value of `docker --version`
Expand All @@ -156,6 +162,8 @@ def test_docker_unknown_version(mock_tools, capsys):
]
)

mock_tools.subprocess.run.assert_has_calls(user_mapping_run_calls)

# console output
output = capsys.readouterr()
assert "** WARNING: Unable to determine the version of Docker" in output.out
Expand Down
Loading

0 comments on commit 5ed3010

Please sign in to comment.