diff --git a/Pipfile b/Pipfile index 4c6bf222e..30c81ea15 100644 --- a/Pipfile +++ b/Pipfile @@ -15,5 +15,4 @@ docker = "*" pymongo = "*" selenium = "*" SQLAlchemy = "*" -PyYAML = "*" PyMySQL = "*" diff --git a/testcontainers/core/container.py b/testcontainers/core/container.py index 64cb5d025..5c4c0e2d8 100644 --- a/testcontainers/core/container.py +++ b/testcontainers/core/container.py @@ -1,7 +1,9 @@ import blindspin import crayons +from docker.models.containers import Container from testcontainers.core.docker_client import DockerClient +from testcontainers.core.exceptions import ContainerStartException from testcontainers.core.utils import is_windows @@ -12,12 +14,13 @@ def __init__(self, image): self._docker = DockerClient() self.image = image self._container = None + self._command = None def add_env(self, key, value): self.env[key] = value return self - def expose_port(self, container, host=None): + def bind_ports(self, container, host=None): self.ports[container] = host return self @@ -29,17 +32,18 @@ def start(self): print("") print("{} {}".format(crayons.yellow("Pulling image"), crayons.red(self.image))) with blindspin.spinner(): - self._container = self._docker.run(self.image, - detach=True, - environment=self.env, - ports=self.ports, - publish_all_ports=True) + self._container = self.get_docker_client().run(self.image, + command=self._command, + detach=True, + environment=self.env, + ports=self.ports, + publish_all_ports=True) print("") - print("Container started: ", crayons.yellow(self._container.id, bold=True)) + print("Container started: ", crayons.yellow(self._container.short_id, bold=True)) return self def stop(self): - self._container.remove(force=True) + self.get_wrapped_contaner().remove(force=True) def __enter__(self): return self.start() @@ -54,4 +58,18 @@ def get_container_host_ip(self) -> str: return "0.0.0.0" def get_exposed_port(self, port) -> str: - return self._docker.port(self._container.id, port) + return self.get_docker_client().port(self._container.id, port) + + def with_command(self, command): + self._command = command + + def get_wrapped_contaner(self) -> Container: + return self._container + + def get_docker_client(self) -> DockerClient: + return self._docker + + def exec(self, command): + if not self._container: + raise ContainerStartException("Container should be started before") + return self.get_wrapped_contaner().exec_run(command) diff --git a/testcontainers/core/generic.py b/testcontainers/core/generic.py index 75baac3d8..198f645da 100644 --- a/testcontainers/core/generic.py +++ b/testcontainers/core/generic.py @@ -17,17 +17,8 @@ class DbContainer(DockerContainer): - def __init__(self, image, dialect, - username, - password, - port, - db_name): + def __init__(self, image): super(DbContainer, self).__init__(image) - self.dialect = dialect - self.username = username - self.password = password - self.port = port - self.db_name = db_name @wait_container_is_ready() def _connect(self): @@ -39,14 +30,17 @@ def _connect(self): engine.connect() def get_connection_url(self): + raise NotImplementedError + + def _create_connection_url(self, dialect, username, password, port, db_name): return "{dialect}://{username}" \ ":{password}@{host}:" \ - "{port}/{db}".format(dialect=self.dialect, - username=self.username, - password=self.password, + "{port}/{db}".format(dialect=dialect, + username=username, + password=password, host=self.get_container_host_ip(), - port=self.get_exposed_port(self.port), - db=self.db_name) + port=self.get_exposed_port(port), + db=db_name) def start(self): super().start() @@ -55,3 +49,8 @@ def start(self): def _configure(self): raise NotImplementedError + + +class GenericContainer(DockerContainer): + def __init__(self, image): + super(GenericContainer, self).__init__(image) diff --git a/testcontainers/mysql.py b/testcontainers/mysql.py index dfeec3d26..3af362659 100644 --- a/testcontainers/mysql.py +++ b/testcontainers/mysql.py @@ -10,25 +10,33 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. +from os import environ + from testcontainers.core.generic import DbContainer class MySqlContainer(DbContainer): + MYSQL_ROOT_PASSWORD = environ.get("MYSQL_ROOT_PASSWORD", "test") + MYSQL_DATABASE = environ.get("MYSQL_DATABASE", "test") + MYSQL_USER = environ.get("MYSQL_USER", "test") + MYSQL_PASSWORD = environ.get("MYSQL_PASSWORD", "test") + def __init__(self, image="mysql:latest"): - super(MySqlContainer, self).__init__(image, - dialect="mysql+pymysql", - username="test", - password="test", - port=3306, - db_name="test") - self.root_password = "test" - self.host_port = 3306 + super(MySqlContainer, self).__init__(image) + self.port_to_expose = 3306 def _configure(self): - self.add_env("MYSQL_ROOT_PASSWORD", self.root_password) - self.add_env("MYSQL_DATABASE", self.db_name) - self.add_env("MYSQL_USER", self.username) - self.add_env("MYSQL_PASSWORD", self.password) + self.add_env("MYSQL_ROOT_PASSWORD", self.MYSQL_ROOT_PASSWORD) + self.add_env("MYSQL_DATABASE", self.MYSQL_DATABASE) + self.add_env("MYSQL_USER", self.MYSQL_USER) + self.add_env("MYSQL_PASSWORD", self.MYSQL_PASSWORD) + + def get_connection_url(self): + return super()._create_connection_url(dialect="mysql+pymysql", + username=self.MYSQL_USER, + password=self.MYSQL_PASSWORD, + db_name=self.MYSQL_DATABASE, + port=self.port_to_expose) class MariaDbContainer(MySqlContainer): diff --git a/testcontainers/postgres.py b/testcontainers/postgres.py index 83644f296..bf502ac52 100644 --- a/testcontainers/postgres.py +++ b/testcontainers/postgres.py @@ -10,21 +10,28 @@ # 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 os from testcontainers.core.generic import DbContainer class PostgresContainer(DbContainer): + POSTGRES_USER = os.environ.get("POSTGRES_USER", "test") + POSTGRES_PASSWORD = os.environ.get("POSTGRES_PASSWORD", "test") + POSTGRES_DB = os.environ.get("POSTGRES_DB", "test") + def __init__(self, image="postgres:latest"): - super(PostgresContainer, self).__init__(image=image, - dialect="postgresql+psycopg2", - username="test", - password="test", - db_name="test", - port=5432) - self.host_port = 5432 + super(PostgresContainer, self).__init__(image=image) + self.port_to_expose = 5432 def _configure(self): - self.add_env("POSTGRES_USER", self.username) - self.add_env("POSTGRES_PASSWORD", self.password) - self.add_env("POSTGRES_DB", self.db_name) + self.add_env("POSTGRES_USER", self.POSTGRES_USER) + self.add_env("POSTGRES_PASSWORD", self.POSTGRES_PASSWORD) + self.add_env("POSTGRES_DB", self.POSTGRES_DB) + + def get_connection_url(self): + return super()._create_connection_url(dialect="postgresql+psycopg2", + username=self.POSTGRES_USER, + password=self.POSTGRES_PASSWORD, + db_name=self.POSTGRES_DB, + port=self.port_to_expose) diff --git a/tests/test_db_containers.py b/tests/test_db_containers.py index 3aa3c3348..b9fa78ee0 100644 --- a/tests/test_db_containers.py +++ b/tests/test_db_containers.py @@ -2,6 +2,7 @@ from pymongo import MongoClient from testcontainers.core.container import DockerContainer +from testcontainers.core.generic import GenericContainer from testcontainers.core.waiting_utils import wait_container_is_ready from testcontainers.mysql import MySqlContainer, MariaDbContainer from testcontainers.postgres import PostgresContainer @@ -35,8 +36,8 @@ def test_docker_run_mariadb(): def test_docker_generic_db(): - mongo_container = DockerContainer("mongo:latest") - mongo_container.expose_port(27017, 27017) + mongo_container = GenericContainer("mongo:latest") + mongo_container.bind_ports(27017, 27017) with mongo_container: @wait_container_is_ready() diff --git a/tests/test_new_docker_api.py b/tests/test_new_docker_api.py index ebf4ea3d7..fd0b6dd1c 100644 --- a/tests/test_new_docker_api.py +++ b/tests/test_new_docker_api.py @@ -1,14 +1,36 @@ +import os + from selenium import webdriver from webdriver_manager.chrome import ChromeDriverManager -from testcontainers.core.container import DockerContainer +from testcontainers import mysql + +from testcontainers.core.generic import GenericContainer + +from importlib import reload + + +def setup_module(m): + os.environ["MYSQL_USER"] = "demo" + os.environ["MYSQL_DATABASE"] = "custom_db" def test_docker_custom_image(): - container = DockerContainer("spirogov/video_service:latest").expose_port(8086, 8086) + container = GenericContainer("spirogov/video_service:latest") + container.bind_ports(8086, 8086) with container: driver = webdriver.Chrome(ChromeDriverManager().install()) driver.implicitly_wait(10) driver.get("http://localhost:8086") - driver.find_element_by_css_selector("#inputEmail3").send_keys("admin") \ No newline at end of file + driver.find_element_by_css_selector("#inputEmail3").send_keys("admin") + + +def test_docker_env_variables(): + reload(mysql) + + db = mysql.MySqlContainer() + db.bind_ports(3306, 32785) + with db: + url = db.get_connection_url() + assert url == 'mysql+pymysql://demo:test@0.0.0.0:32785/custom_db'