From 745231ab17efa03aa249e8271c54ac0730c80b84 Mon Sep 17 00:00:00 2001 From: Chris Maeda Date: Fri, 25 Oct 2024 12:27:41 -1000 Subject: [PATCH] Reproducible test case for issue 707 * pytest fixture builds a custom mysql 8.0 image with the stuff table * test_mysql_custom_image/test_docker_run_mysql_8_custom gets a timeout error * test_mysql_custom_image/test_docker_run_mysql_8_custom_overwrite_connect_method passes --- modules/mysql/tests/conftest.py | 13 ++++++ modules/mysql/tests/seeds/Dockerfile | 28 ++++++++++++ .../mysql/tests/test_mysql_custom_image.py | 44 +++++++++++++++++++ 3 files changed, 85 insertions(+) create mode 100644 modules/mysql/tests/conftest.py create mode 100644 modules/mysql/tests/seeds/Dockerfile create mode 100644 modules/mysql/tests/test_mysql_custom_image.py diff --git a/modules/mysql/tests/conftest.py b/modules/mysql/tests/conftest.py new file mode 100644 index 000000000..2db894066 --- /dev/null +++ b/modules/mysql/tests/conftest.py @@ -0,0 +1,13 @@ +from pathlib import Path +import pytest +import docker + + +# build a custom MySQL image +@pytest.fixture(scope="session") +def mysql_custom_image() -> str: + tag = "mysql-custom:8.0" + client = docker.from_env() + DOCKERFILE_PATH = (Path(__file__).parent / "seeds").absolute().as_posix() + image, _ = client.images.build(path=DOCKERFILE_PATH, tag=tag) + return tag diff --git a/modules/mysql/tests/seeds/Dockerfile b/modules/mysql/tests/seeds/Dockerfile new file mode 100644 index 000000000..5e524905f --- /dev/null +++ b/modules/mysql/tests/seeds/Dockerfile @@ -0,0 +1,28 @@ +# +# custom mysql image +# +ARG MYSQL_VERSION="8.0" +FROM mysql:${MYSQL_VERSION} as builder + +# That file does the DB initialization but also runs mysql daemon, by removing the last line it will only init +RUN ["sed", "-i", "s/exec \"$@\"/echo \"not running $@\"/", "/usr/local/bin/docker-entrypoint.sh"] + +# needed for intialization +ENV MYSQL_ROOT_PASSWORD=root +ENV MYSQL_DATABASE=test MYSQL_USER=test MYSQL_PASSWORD=test + +COPY *.sql /docker-entrypoint-initdb.d/ + +# Need to change the datadir to something other than /var/lib/mysql because the parent docker file defines it as a volume. +# https://docs.docker.com/engine/reference/builder/#volume : +# Changing the volume from within the Dockerfile: If any build steps change the data within the volume after +# it has been declared, those changes will be discarded. +RUN ["/usr/local/bin/docker-entrypoint.sh", "mysqld", "--datadir", "/initialized-db" ] + +# +# create final container +# +FROM mysql:${MYSQL_VERSION} + +# copy datadir from builder +COPY --from=builder /initialized-db /var/lib/mysql diff --git a/modules/mysql/tests/test_mysql_custom_image.py b/modules/mysql/tests/test_mysql_custom_image.py new file mode 100644 index 000000000..82ae15d9a --- /dev/null +++ b/modules/mysql/tests/test_mysql_custom_image.py @@ -0,0 +1,44 @@ +import pytest +import re +import sqlalchemy + +from testcontainers.core.utils import is_arm +from testcontainers.mysql import MySqlContainer + +# imports for issue 707 +from testcontainers.core.waiting_utils import wait_for_logs +from types import MethodType + + +# Testcontainers-Python 4.8.2 / main branch: This method gets the following error: +# E TimeoutError: Container did not emit logs satisfying predicate in 120.000 seconds +@pytest.mark.skipif(is_arm(), reason="mysql container not available for ARM") +def test_docker_run_mysql_8_custom(mysql_custom_image): + config = MySqlContainer(mysql_custom_image) + with config as mysql: + engine = sqlalchemy.create_engine(mysql.get_connection_url()) + with engine.begin() as connection: + result = connection.execute(sqlalchemy.text("select * from stuff")) + assert len(list(result)) == 4, "Should have gotten all the stuff" + + +# Testcontainers-Python 4.8.2 / main branch: This method works +@pytest.mark.skipif(is_arm(), reason="mysql container not available for ARM") +def test_docker_run_mysql_8_custom_overwrite_connect_method(mysql_custom_image): + config = MySqlContainer(mysql_custom_image) + + # 20241025 patch the _connect method to change the wait_for_logs regex + def _connect(self) -> None: + wait_for_logs( + self, + re.compile(".* ready for connections.* ready for connections.*", flags=re.DOTALL | re.MULTILINE).search, + ) + + config._connect = MethodType(_connect, config) + + with config as mysql: + mysql_url = mysql.get_connection_url() + engine = sqlalchemy.create_engine(mysql_url) + with engine.begin() as connection: + result = connection.execute(sqlalchemy.text("select * from stuff")) + assert len(list(result)) == 4, "Should have gotten all the stuff"