Skip to content

Commit

Permalink
Merge branch 'main' into with_env_file
Browse files Browse the repository at this point in the history
  • Loading branch information
Tranquility2 authored Sep 14, 2024
2 parents a5a6d5e + 8f1165d commit d4754e5
Show file tree
Hide file tree
Showing 17 changed files with 289 additions and 97 deletions.
2 changes: 1 addition & 1 deletion .github/.release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "4.8.0"
".": "4.8.1"
}
1 change: 1 addition & 0 deletions .github/workflows/ci-core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: "coverage-artifact-${{ matrix.python-version}}"
include-hidden-files: true
path: ".coverage.*"
retention-days: 1
- name: Run doctests
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Changelog

## [4.8.1](https://github.com/testcontainers/testcontainers-python/compare/testcontainers-v4.8.0...testcontainers-v4.8.1) (2024-08-18)


### Bug Fixes

* **generic:** Update the FastAPI install on genric module doctest samples ([#686](https://github.com/testcontainers/testcontainers-python/issues/686)) ([5216b02](https://github.com/testcontainers/testcontainers-python/commit/5216b0241a27afe3419f5c4a6d500dc27154ddd4))
* **mssql:** use glob to find mssql-tools folder since it moves ([#685](https://github.com/testcontainers/testcontainers-python/issues/685)) ([4912725](https://github.com/testcontainers/testcontainers-python/commit/4912725c2a54a9edce046416fbf11e089cc03cb0)), closes [#666](https://github.com/testcontainers/testcontainers-python/issues/666)
* wait_for_logs can now fail early when the container stops ([#682](https://github.com/testcontainers/testcontainers-python/issues/682)) ([925329d](https://github.com/testcontainers/testcontainers-python/commit/925329d8d2df78437a491a29b707d5ac97e7b734))


### Documentation

* Add a more advance usecase documentation for ServerContainer ([#688](https://github.com/testcontainers/testcontainers-python/issues/688)) ([2cf5a9f](https://github.com/testcontainers/testcontainers-python/commit/2cf5a9fbe6db3fa4254a5bb54e67412ec2d08488))

## [4.8.0](https://github.com/testcontainers/testcontainers-python/compare/testcontainers-v4.7.2...testcontainers-v4.8.0) (2024-08-14)


Expand Down
15 changes: 9 additions & 6 deletions core/testcontainers/core/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import json as json
from collections import namedtuple
from logging import warning
from typing import Optional
from typing import Any, Optional

DockerAuthInfo = namedtuple("DockerAuthInfo", ["registry", "username", "password"])

Expand All @@ -12,7 +12,7 @@
}


def process_docker_auth_config_encoded(auth_config_dict: dict) -> list[DockerAuthInfo]:
def process_docker_auth_config_encoded(auth_config_dict: dict[str, dict[str, dict[str, Any]]]) -> list[DockerAuthInfo]:
"""
Process the auths config.
Expand All @@ -30,16 +30,19 @@ def process_docker_auth_config_encoded(auth_config_dict: dict) -> list[DockerAut
auth_info: list[DockerAuthInfo] = []

auths = auth_config_dict.get("auths")
if not auths:
raise KeyError("No auths found in the docker auth config")

for registry, auth in auths.items():
auth_str = auth.get("auth")
auth_str = str(auth.get("auth"))
auth_str = base64.b64decode(auth_str).decode("utf-8")
username, password = auth_str.split(":")
auth_info.append(DockerAuthInfo(registry, username, password))

return auth_info


def process_docker_auth_config_cred_helpers(auth_config_dict: dict) -> None:
def process_docker_auth_config_cred_helpers(auth_config_dict: dict[str, Any]) -> None:
"""
Process the credHelpers config.
Expand All @@ -56,7 +59,7 @@ def process_docker_auth_config_cred_helpers(auth_config_dict: dict) -> None:
warning(_AUTH_WARNINGS.pop("credHelpers"))


def process_docker_auth_config_store(auth_config_dict: dict) -> None:
def process_docker_auth_config_store(auth_config_dict: dict[str, Any]) -> None:
"""
Process the credsStore config.
Expand All @@ -74,7 +77,7 @@ def process_docker_auth_config_store(auth_config_dict: dict) -> None:
def parse_docker_auth_config(auth_config: str) -> Optional[list[DockerAuthInfo]]:
"""Parse the docker auth config from a string and handle the different formats."""
try:
auth_config_dict: dict = json.loads(auth_config)
auth_config_dict: dict[str, Any] = json.loads(auth_config)
if "credHelpers" in auth_config:
process_docker_auth_config_cred_helpers(auth_config_dict)
if "credsStore" in auth_config:
Expand Down
8 changes: 4 additions & 4 deletions core/testcontainers/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def read_tc_properties() -> dict[str, str]:
tc_files = [item for item in [TC_GLOBAL] if exists(item)]
if not tc_files:
return {}
settings = {}
settings: dict[str, str] = {}

for file in tc_files:
with open(file) as contents:
Expand Down Expand Up @@ -60,14 +60,14 @@ class TestcontainersConfiguration:
"""

@property
def docker_auth_config(self):
def docker_auth_config(self) -> Optional[str]:
config = self._docker_auth_config
if config and "DOCKER_AUTH_CONFIG" in _WARNINGS:
warning(_WARNINGS.pop("DOCKER_AUTH_CONFIG"))
return config

@docker_auth_config.setter
def docker_auth_config(self, value: str):
def docker_auth_config(self, value: str) -> None:
if "DOCKER_AUTH_CONFIG" in _WARNINGS:
warning(_WARNINGS.pop("DOCKER_AUTH_CONFIG"))
self._docker_auth_config = value
Expand All @@ -76,7 +76,7 @@ def tc_properties_get_tc_host(self) -> Union[str, None]:
return self.tc_properties.get("tc.host")

@property
def timeout(self):
def timeout(self) -> int:
return self.max_tries * self.sleep_time


Expand Down
9 changes: 6 additions & 3 deletions core/testcontainers/core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import platform
import subprocess
import sys
from typing import Any, Optional

LINUX = "linux"
MAC = "mac"
Expand All @@ -18,14 +19,15 @@ def setup_logger(name: str) -> logging.Logger:
return logger


def os_name() -> str:
def os_name() -> Optional[str]:
pl = sys.platform
if pl == "linux" or pl == "linux2":
return LINUX
elif pl == "darwin":
return MAC
elif pl == "win32":
return WIN
return None


def is_mac() -> bool:
Expand Down Expand Up @@ -53,7 +55,7 @@ def inside_container() -> bool:
return os.path.exists("/.dockerenv")


def default_gateway_ip() -> str:
def default_gateway_ip() -> Optional[str]:
"""
Returns gateway IP address of the host that testcontainer process is
running on
Expand All @@ -66,11 +68,12 @@ def default_gateway_ip() -> str:
ip_address = process.communicate()[0]
if ip_address and process.returncode == 0:
return ip_address.decode("utf-8").strip().strip("\n")
return None
except subprocess.SubprocessError:
return None


def raise_for_deprecated_parameter(kwargs: dict, name: str, replacement: str) -> dict:
def raise_for_deprecated_parameter(kwargs: dict[Any, Any], name: str, replacement: str) -> dict[Any, Any]:
"""
Raise an error if a dictionary of keyword arguments contains a key and suggest the replacement.
"""
Expand Down
2 changes: 2 additions & 0 deletions core/tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
def test_parse_docker_auth_config_encoded():
auth_config_json = '{"auths":{"https://index.docker.io/v1/":{"auth":"dXNlcm5hbWU6cGFzc3dvcmQ="}}}'
auth_info = parse_docker_auth_config(auth_config_json)
assert auth_info
assert len(auth_info) == 1
assert auth_info[0] == DockerAuthInfo(
registry="https://index.docker.io/v1/",
Expand Down Expand Up @@ -37,6 +38,7 @@ def test_parse_docker_auth_config_encoded_multiple():
}
auth_config_json = json.dumps(auth_dict)
auth_info = parse_docker_auth_config(auth_config_json)
assert auth_info
assert len(auth_info) == 3
assert auth_info[0] == DockerAuthInfo(
registry="localhost:5000",
Expand Down
62 changes: 62 additions & 0 deletions core/tests/test_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from testcontainers.core.config import TestcontainersConfiguration as TCC, TC_FILE

from pytest import MonkeyPatch, mark, LogCaptureFixture

import logging
import tempfile


def test_read_tc_properties(monkeypatch: MonkeyPatch) -> None:
with tempfile.TemporaryDirectory() as tmpdirname:
file = f"{tmpdirname}/{TC_FILE}"
with open(file, "w") as f:
f.write("tc.host=some_value\n")

monkeypatch.setattr("testcontainers.core.config.TC_GLOBAL", file)

config = TCC()
assert config.tc_properties == {"tc.host": "some_value"}


@mark.parametrize("docker_auth_config_env", ["key=value", ""])
@mark.parametrize("warning_dict", [{}, {"key": "value"}, {"DOCKER_AUTH_CONFIG": "TEST"}])
@mark.parametrize("warning_dict_post", [{}, {"key": "value"}, {"DOCKER_AUTH_CONFIG": "TEST"}])
def test_docker_auth_config(
caplog: LogCaptureFixture,
monkeypatch: MonkeyPatch,
docker_auth_config_env: str,
warning_dict: dict[str, str],
warning_dict_post: dict[str, str],
) -> None:
monkeypatch.setattr("testcontainers.core.config._WARNINGS", warning_dict)
monkeypatch.setenv("DOCKER_AUTH_CONFIG", docker_auth_config_env)
caplog.set_level(logging.WARNING)

config = TCC()
if not docker_auth_config_env:
assert config.docker_auth_config == ""
assert caplog.text == ""
else:
assert config.docker_auth_config == docker_auth_config_env

if "DOCKER_AUTH_CONFIG" in warning_dict:
assert warning_dict["DOCKER_AUTH_CONFIG"] in caplog.text

if warning_dict == {}:
monkeypatch.setattr("testcontainers.core.config._WARNINGS", warning_dict_post)

config.docker_auth_config = "new_value"
assert config.docker_auth_config == "new_value"


def test_tc_properties_get_tc_host() -> None:
config = TCC()
config.tc_properties = {"tc.host": "some_value"}
assert config.tc_properties_get_tc_host() == "some_value"


def test_timeout() -> None:
config = TCC()
config.max_tries = 2
config.sleep_time = 3
assert config.timeout == 6
79 changes: 2 additions & 77 deletions core/tests/test_core.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,4 @@
import pytest
import tempfile
import random
import os

from pathlib import Path
from typing import Optional

from testcontainers.core.container import DockerContainer
from testcontainers.core.image import DockerImage
from testcontainers.core.waiting_utils import wait_for_logs


def test_timeout_is_raised_when_waiting_for_logs():
with pytest.raises(TimeoutError), DockerContainer("alpine").with_command("sleep 2") as container:
wait_for_logs(container, "Hello from Docker!", timeout=1e-3)


def test_garbage_collection_is_defensive():
Expand All @@ -26,72 +11,12 @@ def test_garbage_collection_is_defensive():
del container


def test_wait_for_hello():
def test_get_logs():
with DockerContainer("hello-world") as container:
wait_for_logs(container, "Hello from Docker!")


def test_can_get_logs():
with DockerContainer("hello-world") as container:
wait_for_logs(container, "Hello from Docker!")
stdout, stderr = container.get_logs()
assert isinstance(stdout, bytes)
assert isinstance(stderr, bytes)
assert stdout, "There should be something on stdout"


@pytest.mark.parametrize("test_cleanup", [True, False])
@pytest.mark.parametrize("test_image_tag", [None, "test-image:latest"])
def test_docker_image(test_image_tag: Optional[str], test_cleanup: bool, check_for_image):
with tempfile.TemporaryDirectory() as temp_directory:
# It's important to use a random string to avoid image caching
random_string = "Hello from Docker Image! " + str(random.randint(0, 1000))
with open(f"{temp_directory}/Dockerfile", "w") as f:
f.write(
f"""
FROM alpine:latest
CMD echo "{random_string}"
"""
)
with DockerImage(path=temp_directory, tag=test_image_tag, clean_up=test_cleanup) as image:
image_short_id = image.short_id
assert image.tag is test_image_tag, f"Expected {test_image_tag}, got {image.tag}"
assert image.short_id is not None, "Short ID should not be None"
logs = image.get_logs()
assert isinstance(logs, list), "Logs should be a list"
assert logs[0] == {"stream": "Step 1/2 : FROM alpine:latest"}
assert logs[3] == {"stream": f'Step 2/2 : CMD echo "{random_string}"'}
with DockerContainer(str(image)) as container:
assert container._container.image.short_id.endswith(image_short_id), "Image ID mismatch"
assert container.get_logs() == ((random_string + "\n").encode(), b""), "Container logs mismatch"

check_for_image(image_short_id, test_cleanup)


@pytest.mark.parametrize("dockerfile_path", [None, Path("subdir/my.Dockerfile")])
def test_docker_image_with_custom_dockerfile_path(dockerfile_path: Optional[Path]):
with tempfile.TemporaryDirectory() as temp_directory:
temp_dir_path = Path(temp_directory)
if dockerfile_path:
os.makedirs(temp_dir_path / dockerfile_path.parent, exist_ok=True)
dockerfile_rel_path = dockerfile_path
dockerfile_kwargs = {"dockerfile_path": dockerfile_path}
else:
dockerfile_rel_path = Path("Dockerfile") # default
dockerfile_kwargs = {}

with open(temp_dir_path / dockerfile_rel_path, "x") as f:
f.write(
f"""
FROM alpine:latest
CMD echo "Hello world!"
"""
)
with DockerImage(path=temp_directory, tag="test", clean_up=True, no_cache=True, **dockerfile_kwargs) as image:
image_short_id = image.short_id
with DockerContainer(str(image)) as container:
assert container._container.image.short_id.endswith(image_short_id), "Image ID mismatch"
assert container.get_logs() == (("Hello world!\n").encode(), b""), "Container logs mismatch"
assert "Hello from Docker".encode() in stdout, "There should be something on stdout"


def test_docker_container_with_env_file():
Expand Down
Loading

0 comments on commit d4754e5

Please sign in to comment.