Files
cibuildwheel/unit_test/docker_container_test.py
T

396 lines
14 KiB
Python
Raw Normal View History

2022-06-16 18:11:59 -04:00
import atexit
import os
import platform
2020-07-10 12:46:32 +01:00
import random
import shutil
2020-06-26 21:37:02 +01:00
import subprocess
2022-06-16 18:11:59 -04:00
import tempfile
2020-06-26 21:37:02 +01:00
import textwrap
2022-05-24 17:35:46 -06:00
from pathlib import Path, PurePath, PurePosixPath
2022-06-26 17:22:44 -04:00
from typing import Optional
2020-06-26 21:37:02 +01:00
2020-06-26 21:41:00 +01:00
import pytest
2022-06-26 17:21:59 -04:00
import toml
2020-06-26 21:41:00 +01:00
from cibuildwheel.docker_container import DockerContainer
2021-11-21 15:19:45 -05:00
from cibuildwheel.environment import EnvironmentAssignmentBash
2020-06-26 21:41:00 +01:00
2020-07-08 22:56:33 +01:00
# for these tests we use manylinux2014 images, because they're available on
# multi architectures and include python3.8
pm = platform.machine()
if pm == "x86_64":
2021-05-03 11:45:43 -04:00
DEFAULT_IMAGE = "quay.io/pypa/manylinux2014_x86_64:2020-05-17-2f8ac3b"
elif pm == "aarch64":
2021-05-03 11:45:43 -04:00
DEFAULT_IMAGE = "quay.io/pypa/manylinux2014_aarch64:2020-05-17-2f8ac3b"
elif pm == "ppc64le":
2021-05-03 11:45:43 -04:00
DEFAULT_IMAGE = "quay.io/pypa/manylinux2014_ppc64le:2020-05-17-2f8ac3b"
elif pm == "s390x":
2021-05-03 11:45:43 -04:00
DEFAULT_IMAGE = "quay.io/pypa/manylinux2014_s390x:2020-05-17-2f8ac3b"
2022-06-16 18:11:59 -04:00
else:
DEFAULT_IMAGE = ""
2022-06-26 17:21:59 -04:00
# These globals will be manipulated
temp_test_dir: Optional[tempfile.TemporaryDirectory[str]] = None
using_podman = False
2022-06-16 18:11:59 -04:00
2022-06-26 15:53:44 -04:00
# @atexit.register
2022-06-26 17:10:02 -04:00
def _cleanup_podman_vfs_tempdir():
2022-06-16 18:11:59 -04:00
"""
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.
2022-06-26 15:53:44 -04:00
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
2022-06-16 18:11:59 -04:00
"""
2022-06-26 17:21:59 -04:00
global temp_test_dir
global using_podman
2022-06-16 18:11:59 -04:00
if temp_test_dir is not None:
2022-06-26 17:21:59 -04:00
# When podman creates special directories, they can't be cleaned up
2022-06-26 15:53:44 -04:00
# unless you fake a UID of 0. The package rootlesskit helps with that.
2022-06-26 17:21:59 -04:00
if using_podman:
subprocess.call(["podman", "unshare", "rm", "-rf", temp_test_dir.name])
2022-06-26 17:21:59 -04:00
temp_test_dir = None
2022-06-16 18:11:59 -04:00
def basis_container_kwargs():
"""
Generate keyword args that can be passed to to :class:`DockerContainer`.
This is used with :func:`pytest.mark.parametrize` to run each test with
2022-06-26 15:53:44 -04:00
different configurations of each supported containers engine.
2022-06-16 18:11:59 -04:00
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.
2022-06-26 15:53:44 -04:00
Yields:
Dict: a configuration passed as ``container_kwargs`` to each
parameterized test.
2022-06-16 18:11:59 -04:00
"""
2022-06-26 17:21:59 -04:00
global temp_test_dir
global using_podman
2022-06-16 18:11:59 -04:00
2022-06-26 17:10:02 -04:00
# TODO: Pytest should be aware of if we are trying to test docker / podman
# or not
2022-06-16 18:11:59 -04:00
HAVE_DOCKER = bool(shutil.which("docker"))
HAVE_PODMAN = bool(shutil.which("podman"))
2022-06-26 17:10:02 -04:00
REQUESTED_DOCKER = HAVE_DOCKER
REQUESTED_PODMAN = HAVE_PODMAN
2022-06-26 17:21:59 -04:00
if temp_test_dir is None:
2022-06-26 17:10:02 -04:00
# Only setup the temp directory once for all tests
2022-06-26 17:21:59 -04:00
temp_test_dir = tempfile.TemporaryDirectory(prefix="cibw_test_")
2022-06-26 17:10:02 -04:00
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:
2022-06-16 18:11:59 -04:00
# Basic podman configuration
yield {"container_engine": "docker", "docker_image": DEFAULT_IMAGE}
2022-06-26 17:10:02 -04:00
if REQUESTED_PODMAN:
2022-06-16 18:11:59 -04:00
# Basic podman usage
2022-06-26 17:21:59 -04:00
using_podman = True
2022-06-16 18:11:59 -04:00
yield {"container_engine": "podman", "docker_image": DEFAULT_IMAGE}
# VFS Podman usage (for the podman in docker use-case)
2022-06-26 15:53:44 -04:00
oci_environ = _setup_podman_vfs(temp_test_dir.name)
2022-06-16 18:11:59 -04:00
yield {
"container_engine": "podman",
"docker_image": DEFAULT_IMAGE,
"env": oci_environ,
}
2020-06-26 21:37:02 +01:00
2020-06-26 21:41:00 +01:00
2022-06-26 15:53:44 -04:00
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
2022-06-16 18:11:59 -04:00
@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)
2022-06-16 18:11:59 -04:00
with DockerContainer(**container_kwargs) as container:
2021-05-03 11:45:43 -04:00
assert container.call(["echo", "hello"], capture_output=True) == "hello\n"
2020-06-26 21:37:02 +01:00
2020-06-26 21:41:00 +01:00
@pytest.mark.docker
2022-06-16 18:11:59 -04:00
@pytest.mark.parametrize("container_kwargs", basis_container_kwargs())
def test_no_lf(container_kwargs, monkeypatch):
for k, v in container_kwargs.pop("env", {}).items():
monkeypatch.setenv(k, v)
2022-06-16 18:11:59 -04:00
with DockerContainer(**container_kwargs) as container:
2021-05-03 11:45:43 -04:00
assert container.call(["printf", "hello"], capture_output=True) == "hello"
2020-06-26 21:37:02 +01:00
2020-06-26 21:41:00 +01:00
@pytest.mark.docker
2022-06-16 18:11:59 -04:00
@pytest.mark.parametrize("container_kwargs", basis_container_kwargs())
def test_debug_info(container_kwargs, monkeypatch):
for k, v in container_kwargs.pop("env", {}).items():
monkeypatch.setenv(k, v)
2022-06-16 18:11:59 -04:00
container = DockerContainer(**container_kwargs)
print(container.debug_info())
with container:
pass
@pytest.mark.docker
@pytest.mark.parametrize("container_kwargs", basis_container_kwargs())
def test_environment(container_kwargs, monkeypatch):
for k, v in container_kwargs.pop("env", {}).items():
monkeypatch.setenv(k, v)
2022-06-16 18:11:59 -04:00
with DockerContainer(**container_kwargs) as container:
2021-04-30 17:56:34 -04:00
assert (
container.call(
2021-05-03 11:45:43 -04:00
["sh", "-c", "echo $TEST_VAR"], env={"TEST_VAR": "1"}, capture_output=True
2021-04-30 17:56:34 -04:00
)
2021-05-03 11:45:43 -04:00
== "1\n"
2021-04-30 17:56:34 -04:00
)
2020-06-26 21:37:02 +01:00
2020-06-26 21:41:00 +01:00
2020-07-20 01:04:26 +02:00
@pytest.mark.docker
2022-06-16 18:11:59 -04:00
@pytest.mark.parametrize("container_kwargs", basis_container_kwargs())
def test_cwd(container_kwargs):
with DockerContainer(cwd="/cibuildwheel/working_directory", **container_kwargs) as container:
2021-05-03 11:45:43 -04:00
assert container.call(["pwd"], capture_output=True) == "/cibuildwheel/working_directory\n"
assert container.call(["pwd"], capture_output=True, cwd="/opt") == "/opt\n"
2020-07-20 01:04:26 +02:00
@pytest.mark.docker
2022-06-16 18:11:59 -04:00
@pytest.mark.parametrize("container_kwargs", basis_container_kwargs())
def test_container_removed(container_kwargs, monkeypatch):
for k, v in container_kwargs.pop("env", {}).items():
monkeypatch.setenv(k, v)
2022-06-16 18:11:59 -04:00
with DockerContainer(**container_kwargs) as container:
2021-04-30 17:56:34 -04:00
docker_containers_listing = subprocess.run(
2022-06-16 18:11:59 -04:00
f"{container.container_engine} container ls",
2021-04-30 17:56:34 -04:00
shell=True,
check=True,
stdout=subprocess.PIPE,
universal_newlines=True,
).stdout
2021-06-12 14:02:06 -04:00
assert container.name is not None
2020-06-26 21:37:02 +01:00
assert container.name in docker_containers_listing
old_container_name = container.name
2021-04-30 17:56:34 -04:00
docker_containers_listing = subprocess.run(
2022-06-16 18:11:59 -04:00
f"{container.container_engine} container ls",
2021-04-30 17:56:34 -04:00
shell=True,
check=True,
stdout=subprocess.PIPE,
universal_newlines=True,
).stdout
2020-06-26 21:37:02 +01:00
assert old_container_name not in docker_containers_listing
2020-06-26 21:41:00 +01:00
@pytest.mark.docker
2022-06-16 18:11:59 -04:00
@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)
2020-06-26 21:37:02 +01:00
# max environment variable size is 128kB
2021-04-30 17:56:34 -04:00
long_env_var_length = 127 * 1024
2020-06-26 21:37:02 +01:00
large_environment = {
2021-05-03 11:45:43 -04:00
"a": "0" * long_env_var_length,
"b": "0" * long_env_var_length,
"c": "0" * long_env_var_length,
"d": "0" * long_env_var_length,
2020-06-26 21:37:02 +01:00
}
2022-06-16 18:11:59 -04:00
with DockerContainer(**container_kwargs) as container:
2020-06-26 21:37:02 +01:00
# check the length of d
2021-04-30 17:56:34 -04:00
assert (
2021-05-03 11:45:43 -04:00
container.call(["sh", "-c", "echo ${#d}"], env=large_environment, capture_output=True)
== f"{long_env_var_length}\n"
2021-04-30 17:56:34 -04:00
)
2020-06-26 21:37:02 +01:00
2020-06-26 21:41:00 +01:00
@pytest.mark.docker
2022-06-16 18:11:59 -04:00
@pytest.mark.parametrize("container_kwargs", basis_container_kwargs())
def test_binary_output(container_kwargs, monkeypatch):
for k, v in container_kwargs.pop("env", {}).items():
monkeypatch.setenv(k, v)
2022-06-16 18:11:59 -04:00
with DockerContainer(**container_kwargs) as container:
2020-07-08 22:56:33 +01:00
# note: the below embedded snippets are in python2
2020-06-26 21:37:02 +01:00
# check that we can pass though arbitrary binary data without erroring
2021-04-30 17:56:34 -04:00
container.call(
[
2021-05-03 11:45:43 -04:00
"/usr/bin/python2",
"-c",
2021-04-30 17:56:34 -04:00
textwrap.dedent(
2021-05-03 11:45:43 -04:00
"""
2021-05-02 16:37:38 +02:00
import sys
sys.stdout.write(''.join(chr(n) for n in range(0, 256)))
2021-05-03 11:45:43 -04:00
"""
2021-04-30 17:56:34 -04:00
),
]
)
2020-06-26 21:37:02 +01:00
# check that we can capture arbitrary binary data
2021-04-30 17:56:34 -04:00
output = container.call(
[
2021-05-03 11:45:43 -04:00
"/usr/bin/python2",
"-c",
2021-04-30 17:56:34 -04:00
textwrap.dedent(
2021-05-03 11:45:43 -04:00
"""
2021-05-02 16:37:38 +02:00
import sys
sys.stdout.write(''.join(chr(n % 256) for n in range(0, 512)))
2021-05-03 11:45:43 -04:00
"""
2021-04-30 17:56:34 -04:00
),
],
capture_output=True,
)
2020-06-26 21:37:02 +01:00
2021-05-03 11:45:43 -04:00
data = bytes(output, encoding="utf8", errors="surrogateescape")
2020-06-26 21:37:02 +01:00
for i in range(512):
2020-06-26 21:37:02 +01:00
assert data[i] == i % 256
# check that environment variables can carry binary data, except null characters
# (https://www.gnu.org/software/libc/manual/html_node/Environment-Variables.html)
binary_data = bytes(n for n in range(1, 256))
2021-05-03 11:45:43 -04:00
binary_data_string = str(binary_data, encoding="utf8", errors="surrogateescape")
2020-06-26 21:37:02 +01:00
output = container.call(
2021-05-03 11:45:43 -04:00
["python2", "-c", 'import os, sys; sys.stdout.write(os.environ["TEST_VAR"])'],
env={"TEST_VAR": binary_data_string},
2020-06-26 21:37:02 +01:00
capture_output=True,
)
assert output == binary_data_string
2020-07-08 22:56:33 +01:00
@pytest.mark.docker
2022-06-16 18:11:59 -04:00
@pytest.mark.parametrize("container_kwargs", basis_container_kwargs())
def test_file_operation(tmp_path: Path, container_kwargs, monkeypatch):
for k, v in container_kwargs.pop("env", {}).items():
monkeypatch.setenv(k, v)
2022-06-16 18:11:59 -04:00
with DockerContainer(**container_kwargs) as container:
2020-07-08 22:56:33 +01:00
# test copying a file in
2020-07-10 12:46:32 +01:00
test_binary_data = bytes(random.randrange(256) for _ in range(1000))
2021-05-03 11:45:43 -04:00
original_test_file = tmp_path / "test.dat"
2020-07-08 22:56:33 +01:00
original_test_file.write_bytes(test_binary_data)
2021-05-03 11:45:43 -04:00
dst_file = PurePath("/tmp/test.dat")
2020-07-08 22:56:33 +01:00
container.copy_into(original_test_file, dst_file)
2021-05-03 11:45:43 -04:00
output = container.call(["cat", dst_file], capture_output=True)
assert test_binary_data == bytes(output, encoding="utf8", errors="surrogateescape")
2020-07-08 22:56:33 +01:00
2020-07-10 13:04:50 +01:00
2020-07-10 12:46:32 +01:00
@pytest.mark.docker
2022-06-16 18:11:59 -04:00
@pytest.mark.parametrize("container_kwargs", basis_container_kwargs())
def test_dir_operations(tmp_path: Path, container_kwargs, monkeypatch):
for k, v in container_kwargs.pop("env", {}).items():
monkeypatch.setenv(k, v)
2022-06-16 18:11:59 -04:00
with DockerContainer(**container_kwargs) as container:
2020-07-10 12:46:32 +01:00
test_binary_data = bytes(random.randrange(256) for _ in range(1000))
2021-05-03 11:45:43 -04:00
original_test_file = tmp_path / "test.dat"
2020-07-10 12:46:32 +01:00
original_test_file.write_bytes(test_binary_data)
2020-07-08 22:56:33 +01:00
# test copying a dir in
2021-05-03 11:45:43 -04:00
test_dir = tmp_path / "test_dir"
2020-07-10 12:46:32 +01:00
test_dir.mkdir()
2021-05-03 11:45:43 -04:00
test_file = test_dir / "test.dat"
2020-07-10 12:46:32 +01:00
shutil.copyfile(original_test_file, test_file)
2020-07-08 22:56:33 +01:00
2022-05-24 17:35:46 -06:00
dst_dir = PurePosixPath("/tmp/test_dir")
2021-05-03 11:45:43 -04:00
dst_file = dst_dir / "test.dat"
2020-07-10 12:46:32 +01:00
container.copy_into(test_dir, dst_dir)
2021-05-03 11:45:43 -04:00
output = container.call(["cat", dst_file], capture_output=True)
assert test_binary_data == bytes(output, encoding="utf8", errors="surrogateescape")
2020-07-10 12:46:32 +01:00
# test glob
2021-05-03 11:45:43 -04:00
assert container.glob(dst_dir, "*.dat") == [dst_file]
2020-07-10 12:46:32 +01:00
# test copy dir out
2021-05-03 11:45:43 -04:00
new_test_dir = tmp_path / "test_dir_new"
2020-07-10 12:46:32 +01:00
container.copy_out(dst_dir, new_test_dir)
2021-05-03 11:45:43 -04:00
assert test_binary_data == (new_test_dir / "test.dat").read_bytes()
@pytest.mark.docker
2022-06-16 18:11:59 -04:00
@pytest.mark.parametrize("container_kwargs", basis_container_kwargs())
def test_environment_executor(container_kwargs, monkeypatch):
for k, v in container_kwargs.pop("env", {}).items():
monkeypatch.setenv(k, v)
2022-06-16 18:11:59 -04:00
with DockerContainer(**container_kwargs) as container:
2021-11-21 15:19:45 -05:00
assignment = EnvironmentAssignmentBash("TEST=$(echo 42)")
assert assignment.evaluated_value({}, container.environment_executor) == "42"