diff --git a/core/testcontainers/core/waiting_utils.py b/core/testcontainers/core/waiting_utils.py index 4eb7ad89..cc3351d1 100644 --- a/core/testcontainers/core/waiting_utils.py +++ b/core/testcontainers/core/waiting_utils.py @@ -15,7 +15,7 @@ import re import time import traceback -from typing import TYPE_CHECKING, Any, Callable, Optional, Union +from typing import TYPE_CHECKING, Any, Callable, Union import wrapt @@ -78,7 +78,7 @@ def wait_for(condition: Callable[..., bool]) -> bool: def wait_for_logs( - container: "DockerContainer", predicate: Union[Callable, str], timeout: Optional[float] = None, interval: float = 1 + container: "DockerContainer", predicate: Union[Callable, str], timeout: float = config.timeout, interval: float = 1 ) -> float: """ Wait for the container to emit logs satisfying the predicate. @@ -103,6 +103,6 @@ def wait_for_logs( stderr = container.get_logs()[1].decode() if predicate(stdout) or predicate(stderr): return duration - if timeout and duration > timeout: + if duration > timeout: raise TimeoutError(f"Container did not emit logs satisfying predicate in {timeout:.3f} " "seconds") time.sleep(interval) diff --git a/modules/mssql/testcontainers/mssql/__init__.py b/modules/mssql/testcontainers/mssql/__init__.py index 3bfe861b..6cee3681 100644 --- a/modules/mssql/testcontainers/mssql/__init__.py +++ b/modules/mssql/testcontainers/mssql/__init__.py @@ -3,6 +3,7 @@ from testcontainers.core.generic import DbContainer from testcontainers.core.utils import raise_for_deprecated_parameter +from testcontainers.core.waiting_utils import wait_container_is_ready class SqlServerContainer(DbContainer): @@ -16,7 +17,7 @@ class SqlServerContainer(DbContainer): >>> import sqlalchemy >>> from testcontainers.mssql import SqlServerContainer - >>> with SqlServerContainer() as mssql: + >>> with SqlServerContainer("mcr.microsoft.com/mssql/server:2022-CU12-ubuntu-22.04") as mssql: ... engine = sqlalchemy.create_engine(mssql.get_connection_url()) ... with engine.begin() as connection: ... result = connection.execute(sqlalchemy.text("select @@VERSION")) @@ -49,6 +50,11 @@ def _configure(self) -> None: self.with_env("SQLSERVER_DBNAME", self.dbname) self.with_env("ACCEPT_EULA", "Y") + @wait_container_is_ready(AssertionError) + def _connect(self) -> None: + status, _ = self.exec(f"/opt/mssql-tools/bin/sqlcmd -U {self.username} -P {self.password} -Q 'SELECT 1'") + assert status == 0, "Cannot run 'SELECT 1': container is not ready" + def get_connection_url(self) -> str: return super()._create_connection_url( dialect=self.dialect, username=self.username, password=self.password, dbname=self.dbname, port=self.port diff --git a/modules/mssql/tests/test_mssql.py b/modules/mssql/tests/test_mssql.py index 6f48f0a1..e7273042 100644 --- a/modules/mssql/tests/test_mssql.py +++ b/modules/mssql/tests/test_mssql.py @@ -1,19 +1,23 @@ +import pytest import sqlalchemy +from testcontainers.core.utils import is_arm from testcontainers.mssql import SqlServerContainer -def test_docker_run_mssql(): - image = "mcr.microsoft.com/azure-sql-edge" - dialect = "mssql+pymssql" - with SqlServerContainer(image, dialect=dialect) as mssql: +@pytest.mark.skipif(is_arm(), reason="mssql container not available for ARM") +@pytest.mark.parametrize("version", ["2022-CU12-ubuntu-22.04", "2019-CU25-ubuntu-20.04"]) +def test_docker_run_mssql(version: str): + with SqlServerContainer(f"mcr.microsoft.com/mssql/server:{version}", password="1Secure*Password2") as mssql: engine = sqlalchemy.create_engine(mssql.get_connection_url()) with engine.begin() as connection: result = connection.execute(sqlalchemy.text("select @@servicename")) for row in result: assert row[0] == "MSSQLSERVER" - with SqlServerContainer(image, password="1Secure*Password2", dialect=dialect) as mssql: + +def test_docker_run_azure_sql_edge(): + with SqlServerContainer("mcr.microsoft.com/azure-sql-edge:1.0.7") as mssql: engine = sqlalchemy.create_engine(mssql.get_connection_url()) with engine.begin() as connection: result = connection.execute(sqlalchemy.text("select @@servicename")) diff --git a/modules/mysql/testcontainers/mysql/__init__.py b/modules/mysql/testcontainers/mysql/__init__.py index a5b83927..1b0751bc 100644 --- a/modules/mysql/testcontainers/mysql/__init__.py +++ b/modules/mysql/testcontainers/mysql/__init__.py @@ -10,11 +10,13 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. +import re from os import environ from typing import Optional from testcontainers.core.generic import DbContainer from testcontainers.core.utils import raise_for_deprecated_parameter +from testcontainers.core.waiting_utils import wait_for_logs class MySqlContainer(DbContainer): @@ -74,6 +76,12 @@ def _configure(self) -> None: self.with_env("MYSQL_USER", self.username) self.with_env("MYSQL_PASSWORD", self.password) + def _connect(self) -> None: + wait_for_logs( + self, + re.compile(".*: ready for connections.*: ready for connections.*", flags=re.DOTALL | re.MULTILINE).search, + ) + def get_connection_url(self) -> str: return super()._create_connection_url( dialect="mysql+pymysql", username=self.username, password=self.password, dbname=self.dbname, port=self.port diff --git a/modules/mysql/tests/test_mysql.py b/modules/mysql/tests/test_mysql.py index 3506960b..40eb536b 100644 --- a/modules/mysql/tests/test_mysql.py +++ b/modules/mysql/tests/test_mysql.py @@ -8,41 +8,41 @@ from testcontainers.mysql import MySqlContainer -@pytest.mark.skipif(is_arm(), reason="mysql container not available for ARM") def test_docker_run_mysql(): - config = MySqlContainer("mysql:5.7.17") + config = MySqlContainer("mysql:8.3.0") with config as mysql: engine = sqlalchemy.create_engine(mysql.get_connection_url()) with engine.begin() as connection: result = connection.execute(sqlalchemy.text("select version()")) for row in result: - assert row[0].startswith("5.7.17") + assert row[0].startswith("8.3.0") @pytest.mark.skipif(is_arm(), reason="mysql container not available for ARM") -def test_docker_run_mysql_8(): - config = MySqlContainer("mysql:8") +def test_docker_run_legacy_mysql(): + config = MySqlContainer("mysql:5.7.44") with config as mysql: engine = sqlalchemy.create_engine(mysql.get_connection_url()) with engine.begin() as connection: result = connection.execute(sqlalchemy.text("select version()")) for row in result: - assert row[0].startswith("8") + assert row[0].startswith("5.7.44") -def test_docker_run_mariadb(): - with MySqlContainer("mariadb:10.6.5").maybe_emulate_amd64() as mariadb: +@pytest.mark.parametrize("version", ["11.3.2", "10.11.7"]) +def test_docker_run_mariadb(version: str): + with MySqlContainer(f"mariadb:{version}") as mariadb: engine = sqlalchemy.create_engine(mariadb.get_connection_url()) with engine.begin() as connection: result = connection.execute(sqlalchemy.text("select version()")) for row in result: - assert row[0].startswith("10.6.5") + assert row[0].startswith(version) def test_docker_env_variables(): with ( mock.patch.dict("os.environ", MYSQL_USER="demo", MYSQL_DATABASE="custom_db"), - MySqlContainer("mariadb:10.6.5").with_bind_ports(3306, 32785).maybe_emulate_amd64() as container, + MySqlContainer("mariadb:10.6.5").with_bind_ports(3306, 32785) as container, ): url = container.get_connection_url() pattern = r"mysql\+pymysql:\/\/demo:test@[\w,.]+:(3306|32785)\/custom_db" diff --git a/modules/oracle-free/testcontainers/oracle/__init__.py b/modules/oracle-free/testcontainers/oracle/__init__.py index 03f525a7..781be428 100644 --- a/modules/oracle-free/testcontainers/oracle/__init__.py +++ b/modules/oracle-free/testcontainers/oracle/__init__.py @@ -3,6 +3,7 @@ from typing import Optional from testcontainers.core.generic import DbContainer +from testcontainers.core.waiting_utils import wait_for_logs class OracleDbContainer(DbContainer): @@ -57,6 +58,9 @@ def get_connection_url(self) -> str: ) + "/?service_name={}".format(self.dbname or "FREEPDB1") # Default DB is "FREEPDB1" + def _connect(self) -> None: + wait_for_logs(self, "DATABASE IS READY TO USE!") + def _configure(self) -> None: # if self.oracle_password is not None: # self.with_env("ORACLE_PASSWORD", self.oracle_password)