Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(dependencies): remove usage of sqlalchemy in DB extras. Add default wait timeout for wait_for_logs #525

Merged
merged 15 commits into from
Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions core/testcontainers/core/waiting_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand All @@ -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)
8 changes: 7 additions & 1 deletion modules/mssql/testcontainers/mssql/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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"))
Expand Down Expand Up @@ -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
Expand Down
14 changes: 9 additions & 5 deletions modules/mssql/tests/test_mssql.py
Original file line number Diff line number Diff line change
@@ -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"))
Expand Down
8 changes: 8 additions & 0 deletions modules/mysql/testcontainers/mysql/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down
20 changes: 10 additions & 10 deletions modules/mysql/tests/test_mysql.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 4 additions & 0 deletions modules/oracle-free/testcontainers/oracle/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand Down