Refactor the tests to use --run-docker and --run-podman

Simplified test setup using a parameterised fixture. The VFS test
is now just a single test that handles its own clean up.
This commit is contained in:
Joe Rickerby
2022-06-27 16:45:27 +01:00
parent d9b4644286
commit 6a371aca73
2 changed files with 138 additions and 226 deletions
+1 -14
View File
@@ -8,20 +8,7 @@ MOCK_PACKAGE_DIR = Path("some_package_dir")
def pytest_addoption(parser): def pytest_addoption(parser):
parser.addoption("--run-docker", action="store_true", default=False, help="run docker tests") parser.addoption("--run-docker", action="store_true", default=False, help="run docker tests")
parser.addoption("--run-podman", action="store_true", default=False, help="run podman tests")
def pytest_configure(config):
config.addinivalue_line("markers", "docker: mark test requiring docker to run")
def pytest_collection_modifyitems(config, items):
if config.getoption("--run-docker"):
# --run-docker given in cli: do not skip container tests
return
skip_docker = pytest.mark.skip(reason="need --run-docker option to run")
for item in items:
if "docker" in item.keywords:
item.add_marker(skip_docker)
@pytest.fixture @pytest.fixture
+137 -212
View File
@@ -4,6 +4,7 @@ import platform
import random import random
import shutil import shutil
import subprocess import subprocess
from tabnanny import check
import tempfile import tempfile
import textwrap import textwrap
from pathlib import Path, PurePath, PurePosixPath from pathlib import Path, PurePath, PurePosixPath
@@ -14,6 +15,8 @@ import toml
from cibuildwheel.docker_container import DockerContainer from cibuildwheel.docker_container import DockerContainer
from cibuildwheel.environment import EnvironmentAssignmentBash from cibuildwheel.environment import EnvironmentAssignmentBash
# Test utilities
# for these tests we use manylinux2014 images, because they're available on # for these tests we use manylinux2014 images, because they're available on
# multi architectures and include python3.8 # multi architectures and include python3.8
pm = platform.machine() pm = platform.machine()
@@ -28,191 +31,44 @@ elif pm == "s390x":
else: else:
DEFAULT_IMAGE = "" DEFAULT_IMAGE = ""
# These globals will be manipulated
temp_test_dir = None @pytest.fixture(params=["docker", "podman"])
using_podman = False def container_engine(request):
if request.param == "docker" and not request.config.getoption("--run-docker"):
pytest.skip("need --run-docker option to run")
if request.param == "podman" and not request.config.getoption("--run-podman"):
pytest.skip("need --run-podman option to run")
return request.param
# @atexit.register # Tests
def _cleanup_podman_vfs_tempdir():
"""
Cleans up any configuration written by :func:`basis_container_kwargs`.
For podman tests, the user is not given write permissions by default in new
directories. As a workaround chown them before trying to delete them.
It may be possible to handle this more cleanly in pytest itself, but using
atexit works well enough for now.
The reason why permission errors occur on podman is documented in
[PodmanStoragePerms]_.
References:
.. [PodmanStoragePerms] https://podman.io/blogs/2018/10/03/podman-remove-content-homedir.html
"""
global temp_test_dir
global using_podman
if temp_test_dir is not None:
# When podman creates special directories, they can't be cleaned up
# unless you fake a UID of 0. The package rootlesskit helps with that.
if using_podman: # type: ignore[unreachable]
subprocess.call(["podman", "unshare", "rm", "-rf", temp_test_dir.name])
temp_test_dir = None
def basis_container_kwargs(): def test_simple(container_engine):
""" with DockerContainer(
Generate keyword args that can be passed to to :class:`DockerContainer`. container_engine=container_engine, docker_image=DEFAULT_IMAGE
) as container:
This is used with :func:`pytest.mark.parametrize` to run each test with
different configurations of each supported containers engine.
For docker we test the default configuration.
For podman we test the default configuration and a configuration with VFS
(virtual file system) enabled as the storage driver.
Yields:
Dict: a configuration passed as ``container_kwargs`` to each
parameterized test.
"""
global temp_test_dir
global using_podman
# TODO: Pytest should be aware of if we are trying to test docker / podman
# or not
HAVE_DOCKER = bool(shutil.which("docker"))
HAVE_PODMAN = bool(shutil.which("podman"))
REQUESTED_DOCKER = HAVE_DOCKER
REQUESTED_PODMAN = HAVE_PODMAN
if temp_test_dir is None:
# Only setup the temp directory once for all tests
temp_test_dir = tempfile.TemporaryDirectory(prefix="cibw_test_")
if REQUESTED_PODMAN:
# Register the special cleanup hook after the temp directory is
# created to ensure that it runs before the temp directory logic
# runs (which will not handle cases where there is a fake root
# UID).
atexit.register(_cleanup_podman_vfs_tempdir)
if REQUESTED_DOCKER:
# Basic podman configuration
yield {"container_engine": "docker", "docker_image": DEFAULT_IMAGE}
if REQUESTED_PODMAN:
# Basic podman usage
using_podman = True
yield {"container_engine": "podman", "docker_image": DEFAULT_IMAGE}
# VFS Podman usage (for the podman in docker use-case)
oci_environ = _setup_podman_vfs(temp_test_dir.name)
yield {
"container_engine": "podman",
"docker_image": DEFAULT_IMAGE,
"env": oci_environ,
}
def _setup_podman_vfs(dpath):
"""
Setup the filesystem and environment variables for the VFS podman test
"""
dpath = Path(dpath)
# This requires that we write configuration files and point to them
# with environment variables before we run podman
# https://github.com/containers/common/blob/main/docs/containers.conf.5.md
vfs_containers_conf_data = {
"containers": {
"default_capabilities": [
"CHOWN",
"DAC_OVERRIDE",
"FOWNER",
"FSETID",
"KILL",
"NET_BIND_SERVICE",
"SETFCAP",
"SETGID",
"SETPCAP",
"SETUID",
"SYS_CHROOT",
]
},
"engine": {"cgroup_manager": "cgroupfs", "events_logger": "file"},
}
# https://github.com/containers/storage/blob/main/docs/containers-storage.conf.5.md
storage_root = dpath / ".local/share/containers/vfs-storage"
run_root = dpath / ".local/share/containers/vfs-runroot"
storage_root.mkdir(parents=True, exist_ok=True)
run_root.mkdir(parents=True, exist_ok=True)
vfs_containers_storage_conf_data = {
"storage": {
"driver": "vfs",
"graphroot": os.fspath(storage_root),
"runroot": os.fspath(run_root),
"rootless_storage_path": os.fspath(storage_root),
"options": {
# "remap-user": "containers",
"aufs": {"mountopt": "rw"},
"overlay": {"mountopt": "rw", "force_mask": "shared"},
# "vfs": {"ignore_chown_errors": "true"},
},
}
}
vfs_containers_conf_fpath = dpath / "temp_vfs_containers.conf"
vfs_containers_storage_conf_fpath = dpath / "temp_vfs_containers_storage.conf"
with open(vfs_containers_conf_fpath, "w") as file:
toml.dump(vfs_containers_conf_data, file)
with open(vfs_containers_storage_conf_fpath, "w") as file:
toml.dump(vfs_containers_storage_conf_data, file)
oci_environ = os.environ.copy()
oci_environ.update(
{
"CONTAINERS_CONF": os.fspath(vfs_containers_conf_fpath),
"CONTAINERS_STORAGE_CONF": os.fspath(vfs_containers_storage_conf_fpath),
}
)
return oci_environ
@pytest.mark.docker
@pytest.mark.parametrize("container_kwargs", basis_container_kwargs())
def test_simple(container_kwargs, monkeypatch):
for k, v in container_kwargs.pop("env", {}).items():
monkeypatch.setenv(k, v)
with DockerContainer(**container_kwargs) as container:
assert container.call(["echo", "hello"], capture_output=True) == "hello\n" assert container.call(["echo", "hello"], capture_output=True) == "hello\n"
@pytest.mark.docker def test_no_lf(container_engine):
@pytest.mark.parametrize("container_kwargs", basis_container_kwargs()) with DockerContainer(
def test_no_lf(container_kwargs, monkeypatch): container_engine=container_engine, docker_image=DEFAULT_IMAGE
for k, v in container_kwargs.pop("env", {}).items(): ) as container:
monkeypatch.setenv(k, v)
with DockerContainer(**container_kwargs) as container:
assert container.call(["printf", "hello"], capture_output=True) == "hello" assert container.call(["printf", "hello"], capture_output=True) == "hello"
@pytest.mark.docker def test_debug_info(container_engine):
@pytest.mark.parametrize("container_kwargs", basis_container_kwargs()) container = DockerContainer(container_engine=container_engine, docker_image=DEFAULT_IMAGE)
def test_debug_info(container_kwargs, monkeypatch):
for k, v in container_kwargs.pop("env", {}).items():
monkeypatch.setenv(k, v)
container = DockerContainer(**container_kwargs)
print(container.debug_info()) print(container.debug_info())
with container: with container:
pass pass
@pytest.mark.docker def test_environment(container_engine):
@pytest.mark.parametrize("container_kwargs", basis_container_kwargs()) with DockerContainer(
def test_environment(container_kwargs, monkeypatch): container_engine=container_engine, docker_image=DEFAULT_IMAGE
for k, v in container_kwargs.pop("env", {}).items(): ) as container:
monkeypatch.setenv(k, v)
with DockerContainer(**container_kwargs) as container:
assert ( assert (
container.call( container.call(
["sh", "-c", "echo $TEST_VAR"], env={"TEST_VAR": "1"}, capture_output=True ["sh", "-c", "echo $TEST_VAR"], env={"TEST_VAR": "1"}, capture_output=True
@@ -221,22 +77,20 @@ def test_environment(container_kwargs, monkeypatch):
) )
@pytest.mark.docker def test_cwd(container_engine):
@pytest.mark.parametrize("container_kwargs", basis_container_kwargs()) with DockerContainer(
def test_cwd(container_kwargs, monkeypatch): container_engine=container_engine,
for k, v in container_kwargs.pop("env", {}).items(): docker_image=DEFAULT_IMAGE,
monkeypatch.setenv(k, v) cwd="/cibuildwheel/working_directory",
with DockerContainer(cwd="/cibuildwheel/working_directory", **container_kwargs) as container: ) as container:
assert container.call(["pwd"], capture_output=True) == "/cibuildwheel/working_directory\n" assert container.call(["pwd"], capture_output=True) == "/cibuildwheel/working_directory\n"
assert container.call(["pwd"], capture_output=True, cwd="/opt") == "/opt\n" assert container.call(["pwd"], capture_output=True, cwd="/opt") == "/opt\n"
@pytest.mark.docker def test_container_removed(container_engine):
@pytest.mark.parametrize("container_kwargs", basis_container_kwargs()) with DockerContainer(
def test_container_removed(container_kwargs, monkeypatch): container_engine=container_engine, docker_image=DEFAULT_IMAGE
for k, v in container_kwargs.pop("env", {}).items(): ) as container:
monkeypatch.setenv(k, v)
with DockerContainer(**container_kwargs) as container:
docker_containers_listing = subprocess.run( docker_containers_listing = subprocess.run(
f"{container.container_engine} container ls", f"{container.container_engine} container ls",
shell=True, shell=True,
@@ -258,11 +112,7 @@ def test_container_removed(container_kwargs, monkeypatch):
assert old_container_name not in docker_containers_listing assert old_container_name not in docker_containers_listing
@pytest.mark.docker def test_large_environment(container_engine):
@pytest.mark.parametrize("container_kwargs", basis_container_kwargs())
def test_large_environment(container_kwargs, monkeypatch):
for k, v in container_kwargs.pop("env", {}).items():
monkeypatch.setenv(k, v)
# max environment variable size is 128kB # max environment variable size is 128kB
long_env_var_length = 127 * 1024 long_env_var_length = 127 * 1024
large_environment = { large_environment = {
@@ -272,7 +122,9 @@ def test_large_environment(container_kwargs, monkeypatch):
"d": "0" * long_env_var_length, "d": "0" * long_env_var_length,
} }
with DockerContainer(**container_kwargs) as container: with DockerContainer(
container_engine=container_engine, docker_image=DEFAULT_IMAGE
) as container:
# check the length of d # check the length of d
assert ( assert (
container.call(["sh", "-c", "echo ${#d}"], env=large_environment, capture_output=True) container.call(["sh", "-c", "echo ${#d}"], env=large_environment, capture_output=True)
@@ -280,12 +132,10 @@ def test_large_environment(container_kwargs, monkeypatch):
) )
@pytest.mark.docker def test_binary_output(container_engine):
@pytest.mark.parametrize("container_kwargs", basis_container_kwargs()) with DockerContainer(
def test_binary_output(container_kwargs, monkeypatch): container_engine=container_engine, docker_image=DEFAULT_IMAGE
for k, v in container_kwargs.pop("env", {}).items(): ) as container:
monkeypatch.setenv(k, v)
with DockerContainer(**container_kwargs) as container:
# note: the below embedded snippets are in python2 # note: the below embedded snippets are in python2
# check that we can pass though arbitrary binary data without erroring # check that we can pass though arbitrary binary data without erroring
@@ -334,12 +184,10 @@ def test_binary_output(container_kwargs, monkeypatch):
assert output == binary_data_string assert output == binary_data_string
@pytest.mark.docker def test_file_operation(tmp_path: Path, container_engine):
@pytest.mark.parametrize("container_kwargs", basis_container_kwargs()) with DockerContainer(
def test_file_operation(tmp_path: Path, container_kwargs, monkeypatch): container_engine=container_engine, docker_image=DEFAULT_IMAGE
for k, v in container_kwargs.pop("env", {}).items(): ) as container:
monkeypatch.setenv(k, v)
with DockerContainer(**container_kwargs) as container:
# test copying a file in # test copying a file in
test_binary_data = bytes(random.randrange(256) for _ in range(1000)) test_binary_data = bytes(random.randrange(256) for _ in range(1000))
original_test_file = tmp_path / "test.dat" original_test_file = tmp_path / "test.dat"
@@ -353,12 +201,10 @@ def test_file_operation(tmp_path: Path, container_kwargs, monkeypatch):
assert test_binary_data == bytes(output, encoding="utf8", errors="surrogateescape") assert test_binary_data == bytes(output, encoding="utf8", errors="surrogateescape")
@pytest.mark.docker def test_dir_operations(tmp_path: Path, container_engine):
@pytest.mark.parametrize("container_kwargs", basis_container_kwargs()) with DockerContainer(
def test_dir_operations(tmp_path: Path, container_kwargs, monkeypatch): container_engine=container_engine, docker_image=DEFAULT_IMAGE
for k, v in container_kwargs.pop("env", {}).items(): ) as container:
monkeypatch.setenv(k, v)
with DockerContainer(**container_kwargs) as container:
test_binary_data = bytes(random.randrange(256) for _ in range(1000)) test_binary_data = bytes(random.randrange(256) for _ in range(1000))
original_test_file = tmp_path / "test.dat" original_test_file = tmp_path / "test.dat"
original_test_file.write_bytes(test_binary_data) original_test_file.write_bytes(test_binary_data)
@@ -386,11 +232,90 @@ def test_dir_operations(tmp_path: Path, container_kwargs, monkeypatch):
assert test_binary_data == (new_test_dir / "test.dat").read_bytes() assert test_binary_data == (new_test_dir / "test.dat").read_bytes()
@pytest.mark.docker def test_environment_executor(container_engine):
@pytest.mark.parametrize("container_kwargs", basis_container_kwargs()) with DockerContainer(
def test_environment_executor(container_kwargs, monkeypatch): container_engine=container_engine, docker_image=DEFAULT_IMAGE
for k, v in container_kwargs.pop("env", {}).items(): ) as container:
monkeypatch.setenv(k, v)
with DockerContainer(**container_kwargs) as container:
assignment = EnvironmentAssignmentBash("TEST=$(echo 42)") assignment = EnvironmentAssignmentBash("TEST=$(echo 42)")
assert assignment.evaluated_value({}, container.environment_executor) == "42" assert assignment.evaluated_value({}, container.environment_executor) == "42"
def test_podman_vfs(container_engine, tmp_path: Path, monkeypatch):
# Tests podman VFS, for the podman in docker use-case
if container_engine != "podman":
pytest.skip("podman is the only supported container engine for this test")
# create the VFS configuration
vfs_path = tmp_path / "podman_vfs"
vfs_path.mkdir()
# This requires that we write configuration files and point to them
# with environment variables before we run podman
# https://github.com/containers/common/blob/main/docs/containers.conf.5.md
vfs_containers_conf_data = {
"containers": {
"default_capabilities": [
"CHOWN",
"DAC_OVERRIDE",
"FOWNER",
"FSETID",
"KILL",
"NET_BIND_SERVICE",
"SETFCAP",
"SETGID",
"SETPCAP",
"SETUID",
"SYS_CHROOT",
]
},
"engine": {"cgroup_manager": "cgroupfs", "events_logger": "file"},
}
# https://github.com/containers/storage/blob/main/docs/containers-storage.conf.5.md
storage_root = vfs_path / ".local/share/containers/vfs-storage"
run_root = vfs_path / ".local/share/containers/vfs-runroot"
storage_root.mkdir(parents=True, exist_ok=True)
run_root.mkdir(parents=True, exist_ok=True)
vfs_containers_storage_conf_data = {
"storage": {
"driver": "vfs",
"graphroot": os.fspath(storage_root),
"runroot": os.fspath(run_root),
"rootless_storage_path": os.fspath(storage_root),
"options": {
# "remap-user": "containers",
"aufs": {"mountopt": "rw"},
"overlay": {"mountopt": "rw", "force_mask": "shared"},
# "vfs": {"ignore_chown_errors": "true"},
},
}
}
vfs_containers_conf_fpath = vfs_path / "temp_vfs_containers.conf"
vfs_containers_storage_conf_fpath = vfs_path / "temp_vfs_containers_storage.conf"
with open(vfs_containers_conf_fpath, "w") as file:
toml.dump(vfs_containers_conf_data, file)
with open(vfs_containers_storage_conf_fpath, "w") as file:
toml.dump(vfs_containers_storage_conf_data, file)
monkeypatch.setenv("CONTAINERS_CONF", str(vfs_containers_conf_fpath))
monkeypatch.setenv("CONTAINERS_STORAGE_CONF", str(vfs_containers_storage_conf_fpath))
with DockerContainer(
container_engine=container_engine, docker_image=DEFAULT_IMAGE
) as container:
# test running a command
assert container.call(["echo", "hello"], capture_output=True) == "hello\n"
# test copying a file into the container
(tmp_path / "some_file.txt").write_text("1234")
container.copy_into(tmp_path / "some_file.txt", PurePosixPath("some_file.txt"))
assert container.call(["cat", "some_file.txt"], capture_output=True) == "1234"
# Clean up
# When using the VFS, user is not given write permissions by default in
# new directories. As a workaround we use 'podman unshare' to delete them
# as UID 0. The reason why permission errors occur on podman is documented
# in https://podman.io/blogs/2018/10/03/podman-remove-content-homedir.html
subprocess.run(["podman", "unshare", "rm", "-rf", vfs_path], check=True)