Skip to content

Commit fefb9d0

Browse files
fix(dependencies): remove usage of sqlalchemy in DB extras. Add default wait timeout for wait_for_logs (#525)
Removes usage of `sqlalchemy`, as part of the work described in #526. - Adds default timeout to the `wait_for_logs` waiting strategy, the same timeout used by default in the `wait_container_is_ready` strategy. - Changes wait strategy for `mysql` container to wait for logs indicating that the DB engine is ready to accept connections (MySQL performs a restart as part of its startup procedure, so the logs will always appear twice. - Add More tests for different `mysql` and `mariadb` versions to ensure consistency in wait strategy. - Remove x86 emulation for ARM devices for MariaDB, as it MariaDB images support ARM architectures already. - Change wait strategy for `oracle-free`, as the images produce a consistent `DATABASE IS READY TO USE!` log message on startup. Next steps will be to remove `sqlalchemy` as a bundled dependency entirely, but I have not included it in this PR as I consider it a bigger change than just changing wait strategies as an internal implementation detail. I plan to do this as part of a bigger rework where i remove the `DbContainer` class and standardize configuration hooks and wait strategies across containers (not just DB containers, all containers in need of a configuration and readiness step). See #527 for WIP. --------- Co-authored-by: David Ankin <[email protected]>
1 parent 11964de commit fefb9d0

File tree

6 files changed

+41
-19
lines changed

6 files changed

+41
-19
lines changed

core/testcontainers/core/waiting_utils.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import re
1616
import time
1717
import traceback
18-
from typing import TYPE_CHECKING, Any, Callable, Optional, Union
18+
from typing import TYPE_CHECKING, Any, Callable, Union
1919

2020
import wrapt
2121

@@ -78,7 +78,7 @@ def wait_for(condition: Callable[..., bool]) -> bool:
7878

7979

8080
def wait_for_logs(
81-
container: "DockerContainer", predicate: Union[Callable, str], timeout: Optional[float] = None, interval: float = 1
81+
container: "DockerContainer", predicate: Union[Callable, str], timeout: float = config.timeout, interval: float = 1
8282
) -> float:
8383
"""
8484
Wait for the container to emit logs satisfying the predicate.
@@ -103,6 +103,6 @@ def wait_for_logs(
103103
stderr = container.get_logs()[1].decode()
104104
if predicate(stdout) or predicate(stderr):
105105
return duration
106-
if timeout and duration > timeout:
106+
if duration > timeout:
107107
raise TimeoutError(f"Container did not emit logs satisfying predicate in {timeout:.3f} " "seconds")
108108
time.sleep(interval)

modules/mssql/testcontainers/mssql/__init__.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from testcontainers.core.generic import DbContainer
55
from testcontainers.core.utils import raise_for_deprecated_parameter
6+
from testcontainers.core.waiting_utils import wait_container_is_ready
67

78

89
class SqlServerContainer(DbContainer):
@@ -16,7 +17,7 @@ class SqlServerContainer(DbContainer):
1617
>>> import sqlalchemy
1718
>>> from testcontainers.mssql import SqlServerContainer
1819
19-
>>> with SqlServerContainer() as mssql:
20+
>>> with SqlServerContainer("mcr.microsoft.com/mssql/server:2022-CU12-ubuntu-22.04") as mssql:
2021
... engine = sqlalchemy.create_engine(mssql.get_connection_url())
2122
... with engine.begin() as connection:
2223
... result = connection.execute(sqlalchemy.text("select @@VERSION"))
@@ -49,6 +50,11 @@ def _configure(self) -> None:
4950
self.with_env("SQLSERVER_DBNAME", self.dbname)
5051
self.with_env("ACCEPT_EULA", "Y")
5152

53+
@wait_container_is_ready(AssertionError)
54+
def _connect(self) -> None:
55+
status, _ = self.exec(f"/opt/mssql-tools/bin/sqlcmd -U {self.username} -P {self.password} -Q 'SELECT 1'")
56+
assert status == 0, "Cannot run 'SELECT 1': container is not ready"
57+
5258
def get_connection_url(self) -> str:
5359
return super()._create_connection_url(
5460
dialect=self.dialect, username=self.username, password=self.password, dbname=self.dbname, port=self.port

modules/mssql/tests/test_mssql.py

+9-5
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
1+
import pytest
12
import sqlalchemy
23

4+
from testcontainers.core.utils import is_arm
35
from testcontainers.mssql import SqlServerContainer
46

57

6-
def test_docker_run_mssql():
7-
image = "mcr.microsoft.com/azure-sql-edge"
8-
dialect = "mssql+pymssql"
9-
with SqlServerContainer(image, dialect=dialect) as mssql:
8+
@pytest.mark.skipif(is_arm(), reason="mssql container not available for ARM")
9+
@pytest.mark.parametrize("version", ["2022-CU12-ubuntu-22.04", "2019-CU25-ubuntu-20.04"])
10+
def test_docker_run_mssql(version: str):
11+
with SqlServerContainer(f"mcr.microsoft.com/mssql/server:{version}", password="1Secure*Password2") as mssql:
1012
engine = sqlalchemy.create_engine(mssql.get_connection_url())
1113
with engine.begin() as connection:
1214
result = connection.execute(sqlalchemy.text("select @@servicename"))
1315
for row in result:
1416
assert row[0] == "MSSQLSERVER"
1517

16-
with SqlServerContainer(image, password="1Secure*Password2", dialect=dialect) as mssql:
18+
19+
def test_docker_run_azure_sql_edge():
20+
with SqlServerContainer("mcr.microsoft.com/azure-sql-edge:1.0.7") as mssql:
1721
engine = sqlalchemy.create_engine(mssql.get_connection_url())
1822
with engine.begin() as connection:
1923
result = connection.execute(sqlalchemy.text("select @@servicename"))

modules/mysql/testcontainers/mysql/__init__.py

+8
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@
1010
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
1111
# License for the specific language governing permissions and limitations
1212
# under the License.
13+
import re
1314
from os import environ
1415
from typing import Optional
1516

1617
from testcontainers.core.generic import DbContainer
1718
from testcontainers.core.utils import raise_for_deprecated_parameter
19+
from testcontainers.core.waiting_utils import wait_for_logs
1820

1921

2022
class MySqlContainer(DbContainer):
@@ -74,6 +76,12 @@ def _configure(self) -> None:
7476
self.with_env("MYSQL_USER", self.username)
7577
self.with_env("MYSQL_PASSWORD", self.password)
7678

79+
def _connect(self) -> None:
80+
wait_for_logs(
81+
self,
82+
re.compile(".*: ready for connections.*: ready for connections.*", flags=re.DOTALL | re.MULTILINE).search,
83+
)
84+
7785
def get_connection_url(self) -> str:
7886
return super()._create_connection_url(
7987
dialect="mysql+pymysql", username=self.username, password=self.password, dbname=self.dbname, port=self.port

modules/mysql/tests/test_mysql.py

+10-10
Original file line numberDiff line numberDiff line change
@@ -8,41 +8,41 @@
88
from testcontainers.mysql import MySqlContainer
99

1010

11-
@pytest.mark.skipif(is_arm(), reason="mysql container not available for ARM")
1211
def test_docker_run_mysql():
13-
config = MySqlContainer("mysql:5.7.17")
12+
config = MySqlContainer("mysql:8.3.0")
1413
with config as mysql:
1514
engine = sqlalchemy.create_engine(mysql.get_connection_url())
1615
with engine.begin() as connection:
1716
result = connection.execute(sqlalchemy.text("select version()"))
1817
for row in result:
19-
assert row[0].startswith("5.7.17")
18+
assert row[0].startswith("8.3.0")
2019

2120

2221
@pytest.mark.skipif(is_arm(), reason="mysql container not available for ARM")
23-
def test_docker_run_mysql_8():
24-
config = MySqlContainer("mysql:8")
22+
def test_docker_run_legacy_mysql():
23+
config = MySqlContainer("mysql:5.7.44")
2524
with config as mysql:
2625
engine = sqlalchemy.create_engine(mysql.get_connection_url())
2726
with engine.begin() as connection:
2827
result = connection.execute(sqlalchemy.text("select version()"))
2928
for row in result:
30-
assert row[0].startswith("8")
29+
assert row[0].startswith("5.7.44")
3130

3231

33-
def test_docker_run_mariadb():
34-
with MySqlContainer("mariadb:10.6.5").maybe_emulate_amd64() as mariadb:
32+
@pytest.mark.parametrize("version", ["11.3.2", "10.11.7"])
33+
def test_docker_run_mariadb(version: str):
34+
with MySqlContainer(f"mariadb:{version}") as mariadb:
3535
engine = sqlalchemy.create_engine(mariadb.get_connection_url())
3636
with engine.begin() as connection:
3737
result = connection.execute(sqlalchemy.text("select version()"))
3838
for row in result:
39-
assert row[0].startswith("10.6.5")
39+
assert row[0].startswith(version)
4040

4141

4242
def test_docker_env_variables():
4343
with (
4444
mock.patch.dict("os.environ", MYSQL_USER="demo", MYSQL_DATABASE="custom_db"),
45-
MySqlContainer("mariadb:10.6.5").with_bind_ports(3306, 32785).maybe_emulate_amd64() as container,
45+
MySqlContainer("mariadb:10.6.5").with_bind_ports(3306, 32785) as container,
4646
):
4747
url = container.get_connection_url()
4848
pattern = r"mysql\+pymysql:\/\/demo:test@[\w,.]+:(3306|32785)\/custom_db"

modules/oracle-free/testcontainers/oracle/__init__.py

+4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import Optional
44

55
from testcontainers.core.generic import DbContainer
6+
from testcontainers.core.waiting_utils import wait_for_logs
67

78

89
class OracleDbContainer(DbContainer):
@@ -57,6 +58,9 @@ def get_connection_url(self) -> str:
5758
) + "/?service_name={}".format(self.dbname or "FREEPDB1")
5859
# Default DB is "FREEPDB1"
5960

61+
def _connect(self) -> None:
62+
wait_for_logs(self, "DATABASE IS READY TO USE!")
63+
6064
def _configure(self) -> None:
6165
# if self.oracle_password is not None:
6266
# self.with_env("ORACLE_PASSWORD", self.oracle_password)

0 commit comments

Comments
 (0)