Files
cibuildwheel/unit_test/docker_container_test.py
T

369 lines
13 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
2020-06-26 21:37:02 +01:00
2020-06-26 21:41:00 +01:00
import pytest
2022-06-16 18:11:59 -04:00
import toml # type: ignore[import]
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 = ""
temp_test_dir = None
@atexit.register
def _cleanup_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.
"""
import stat
global temp_test_dir
if temp_test_dir is not None:
print(f"CLEANUP temp_test_dir = {temp_test_dir!r}") # type: ignore[unreachable]
for r, ds, fs in os.walk(temp_test_dir.name):
for d in ds:
dpath = os.path.join(r, d)
if not os.path.islink(dpath):
perms = os.lstat(dpath).st_mode
try:
os.chmod(dpath, stat.S_IWUSR | perms)
except Exception as ex:
print(f"issue with dpath = {dpath!r}, {ex!r}")
for f in fs:
fpath = os.path.join(r, f)
if not os.path.islink(fpath):
perms = os.lstat(fpath).st_mode
try:
os.chmod(fpath, stat.S_IWUSR | perms)
except Exception as ex:
print(f"issue with fpath = {fpath!r}, {ex!r}")
else:
os.unlink(fpath)
try:
temp_test_dir.cleanup()
except Exception as ex:
print(f"Issue cleaning up ex = {ex!r}")
temp_test_dir = None
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
different configuraions 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.
"""
global temp_test_dir
if temp_test_dir is None:
# Only setup the temp directory once for all tests
temp_test_dir = tempfile.TemporaryDirectory(prefix="cibw_test_")
HAVE_DOCKER = bool(shutil.which("docker"))
HAVE_PODMAN = bool(shutil.which("podman"))
if HAVE_DOCKER:
# Basic podman configuration
yield {"container_engine": "docker", "docker_image": DEFAULT_IMAGE}
if HAVE_PODMAN:
# Basic podman usage
yield {"container_engine": "podman", "docker_image": DEFAULT_IMAGE}
# VFS Podman usage (for the podman in docker use-case)
dpath = Path(temp_test_dir.name)
# 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": str(storage_root),
"runroot": str(run_root),
"rootless_storage_path": str(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": str(vfs_containers_conf_fpath),
"CONTAINERS_STORAGE_CONF": str(vfs_containers_storage_conf_fpath),
}
)
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
@pytest.mark.docker
2022-06-16 18:11:59 -04:00
@pytest.mark.parametrize("container_kwargs", basis_container_kwargs())
def test_simple(container_kwargs):
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):
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):
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):
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):
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,
2022-06-16 18:11:59 -04:00
env=container.env,
2021-04-30 17:56:34 -04:00
).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,
2022-06-16 18:11:59 -04:00
env=container.env,
2021-04-30 17:56:34 -04:00
).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):
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):
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):
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):
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):
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"