Skip to content

Commit

Permalink
feat: Support non-session scoped containers.
Browse files Browse the repository at this point in the history
  • Loading branch information
DanCardin committed Apr 28, 2022
1 parent ec5d2b8 commit 0d1474a
Show file tree
Hide file tree
Showing 18 changed files with 175 additions and 55 deletions.
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "pytest-mock-resources"
version = "2.2.7"
version = "2.3.0"
description = "A pytest plugin for easily instantiating reproducible mock resources."
authors = [
"Omar Khan <oakhan3@gmail.com>",
Expand Down Expand Up @@ -119,6 +119,7 @@ filterwarnings = [
"ignore:urllib.parse.splitnport.*:DeprecationWarning",
"ignore:distutils Version classes are deprecated. Use packaging.version instead.:DeprecationWarning",
"ignore::ResourceWarning",
"ignore::UserWarning",
]

[build-system]
Expand Down
11 changes: 6 additions & 5 deletions src/pytest_mock_resources/__init__.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,29 @@
# flake8: noqa
from pytest_mock_resources.container import (
get_container,
MongoConfig,
MysqlConfig,
PostgresConfig,
RedisConfig,
RedshiftConfig,
)
from pytest_mock_resources.fixture.database import (
_mongo_container,
_mysql_container,
_postgres_container,
_redis_container,
_redshift_container,
create_mongo_fixture,
create_mysql_fixture,
create_postgres_fixture,
create_redis_fixture,
create_redshift_fixture,
create_sqlite_fixture,
pmr_mongo_config,
pmr_mongo_container,
pmr_mysql_config,
pmr_mysql_container,
pmr_postgres_config,
pmr_postgres_container,
pmr_redis_config,
pmr_redis_container,
pmr_redshift_config,
pmr_redshift_container,
Rows,
Statements,
)
Expand Down
23 changes: 18 additions & 5 deletions src/pytest_mock_resources/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,8 @@ def wrapper(self):
if value is not None:
return value

value = getattr(self, "_{attr}".format(attr=attr), None)
if value is not None:
return value
if self.has(attr):
return self.get(attr)

try:
return fn(self)
Expand All @@ -54,8 +53,10 @@ class DockerContainerConfig:
_fields_defaults: Dict = {}

def __init__(self, **kwargs):
for field in self._fields:
value = kwargs.get(field)
for field, value in kwargs.items():
if field not in self._fields:
continue

attr = "_{}".format(field)
setattr(self, attr, value)

Expand All @@ -68,6 +69,18 @@ def __repr__(self):
),
)

def has(self, attr):
attr_name = "_{attr}".format(attr=attr)
return hasattr(self, attr_name)

def get(self, attr):
attr_name = "_{attr}".format(attr=attr)
return getattr(self, attr_name)

def set(self, attr, value):
attr_name = "_{attr}".format(attr=attr)
return setattr(self, attr_name, value)

@fallback
def image(self):
raise NotImplementedError()
Expand Down
1 change: 1 addition & 0 deletions src/pytest_mock_resources/container/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# flake8: noqa
from pytest_mock_resources.container.base import get_container
from pytest_mock_resources.container.mongo import MongoConfig
from pytest_mock_resources.container.mysql import MysqlConfig
from pytest_mock_resources.container.postgres import PostgresConfig
Expand Down
20 changes: 16 additions & 4 deletions src/pytest_mock_resources/container/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import contextlib
import json
import pathlib
import socket
import time

import responses
Expand Down Expand Up @@ -48,8 +49,11 @@ def get_container(pytestconfig, config, *, retries=DEFAULT_RETRIES, interval=DEF
# we will need to know whether it's been created already or not.
container = None

if config.port is None:
config.set('port', unused_tcp_port())

run_kwargs = dict(
ports=config.ports(), environment=config.environment(), name=container_name(config.name)
ports=config.ports(), environment=config.environment(), name=container_name(config.name, config.port)
)

try:
Expand Down Expand Up @@ -82,7 +86,7 @@ def get_container(pytestconfig, config, *, retries=DEFAULT_RETRIES, interval=DEF
interval=interval,
)

yield
yield container
finally:
cleanup_container = get_pytest_flag(pytestconfig, "pmr_cleanup_container", default=True)
if cleanup_container and container and not multiprocess_safe_mode:
Expand Down Expand Up @@ -125,8 +129,8 @@ def wait_for_container(
return None


def container_name(name: str) -> str:
return f"pmr_{name}"
def container_name(name: str, port: int) -> str:
return f"pmr_{name}_{port}"


def record_container_creation(pytestconfig, container):
Expand Down Expand Up @@ -165,3 +169,11 @@ def load_container_lockfile(path: pathlib.Path):
yield json.load(f)
else:
yield []


def unused_tcp_port():
"""Find an unused localhost TCP port from 1024-65535 and return it.
"""
with contextlib.closing(socket.socket()) as sock:
sock.bind(("127.0.0.1", 0))
return sock.getsockname()[1]
10 changes: 5 additions & 5 deletions src/pytest_mock_resources/fixture/database/__init__.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
# flake8: noqa
from pytest_mock_resources.fixture.database.mongo import (
_mongo_container,
create_mongo_fixture,
pmr_mongo_config,
pmr_mongo_container,
)
from pytest_mock_resources.fixture.database.redis import (
_redis_container,
create_redis_fixture,
pmr_redis_config,
pmr_redis_container,
)
from pytest_mock_resources.fixture.database.relational import (
_mysql_container,
_postgres_container,
_redshift_container,
create_mysql_fixture,
create_postgres_fixture,
create_redshift_fixture,
create_sqlite_fixture,
pmr_mysql_config,
pmr_mysql_container,
pmr_postgres_config,
pmr_postgres_container,
pmr_redshift_config,
pmr_redshift_container,
Rows,
Statements,
)
4 changes: 2 additions & 2 deletions src/pytest_mock_resources/fixture/database/mongo.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def pmr_mongo_config():


@pytest.fixture(scope="session")
def _mongo_container(pytestconfig, pmr_mongo_config):
def pmr_mongo_container(pytestconfig, pmr_mongo_config):
yield from get_container(pytestconfig, pmr_mongo_config)


Expand All @@ -34,7 +34,7 @@ def create_mongo_fixture(scope="function"):
"""

@pytest.fixture(scope=scope)
def _(_mongo_container, pmr_mongo_config):
def _(pmr_mongo_container, pmr_mongo_config):
return _create_clean_database(pmr_mongo_config)

return _
Expand Down
4 changes: 2 additions & 2 deletions src/pytest_mock_resources/fixture/database/redis.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def pmr_redis_config():


@pytest.fixture(scope="session")
def _redis_container(pytestconfig, pmr_redis_config):
def pmr_redis_container(pytestconfig, pmr_redis_config):
yield from get_container(pytestconfig, pmr_redis_config)


Expand Down Expand Up @@ -50,7 +50,7 @@ def create_redis_fixture(scope="function"):
"""

@pytest.fixture(scope=scope)
def _(request, _redis_container, pmr_redis_config):
def _(request, pmr_redis_container, pmr_redis_config):
database_number = 0
if hasattr(request.config, "workerinput"):
worker_input = request.config.workerinput
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
# flake8: noqa
from pytest_mock_resources.fixture.database.relational.generic import Rows, Statements
from pytest_mock_resources.fixture.database.relational.mysql import (
_mysql_container,
create_mysql_fixture,
pmr_mysql_config,
pmr_mysql_container,
)
from pytest_mock_resources.fixture.database.relational.postgresql import (
_postgres_container,
create_postgres_fixture,
pmr_postgres_config,
pmr_postgres_container,
)
from pytest_mock_resources.fixture.database.relational.redshift import (
_redshift_container,
create_redshift_fixture,
pmr_redshift_config,
pmr_redshift_container,
)
from pytest_mock_resources.fixture.database.relational.sqlite import create_sqlite_fixture
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def pmr_mysql_config():


@pytest.fixture(scope="session")
def _mysql_container(pytestconfig, pmr_mysql_config):
def pmr_mysql_container(pytestconfig, pmr_mysql_config):
yield from get_container(pytestconfig, pmr_mysql_config)


Expand All @@ -41,7 +41,7 @@ def create_mysql_fixture(*ordered_actions, scope="function", tables=None, sessio
"""

@pytest.fixture(scope=scope)
def _(_mysql_container, pmr_mysql_config):
def _(pmr_mysql_container, pmr_mysql_config):
database_name = _create_clean_database(pmr_mysql_config)
engine = get_sqlalchemy_engine(pmr_mysql_config, database_name)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def pmr_postgres_config():


@pytest.fixture(scope="session")
def _postgres_container(pytestconfig, pmr_postgres_config: PostgresConfig):
def pmr_postgres_container(pytestconfig, pmr_postgres_config: PostgresConfig):
yield from get_container(pytestconfig, pmr_postgres_config)


Expand Down Expand Up @@ -59,12 +59,12 @@ def create_postgres_fixture(
)

@pytest.fixture(scope=scope)
def _sync(_postgres_container, pmr_postgres_config):
def _sync(pmr_postgres_container, pmr_postgres_config):
engine_manager = create_engine_manager(pmr_postgres_config, **engine_manager_kwargs)
yield from engine_manager.manage_sync(session=session)

@pytest.fixture(scope=scope)
async def _async(_postgres_container, pmr_postgres_config):
async def _async(pmr_postgres_container, pmr_postgres_config):
engine_manager = create_engine_manager(pmr_postgres_config, **engine_manager_kwargs)
async for engine in engine_manager.manage_async(session=session):
yield engine
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def pmr_redshift_config():


@pytest.fixture(scope="session")
def _redshift_container(pytestconfig, pmr_redshift_config):
def pmr_redshift_container(pytestconfig, pmr_redshift_config):
yield from get_container(pytestconfig, pmr_redshift_config)


Expand Down Expand Up @@ -68,7 +68,7 @@ def create_redshift_fixture(
)

@pytest.fixture(scope=scope)
def _sync(_redshift_container, pmr_redshift_config):
def _sync(pmr_redshift_container, pmr_redshift_config):
engine_manager = create_engine_manager(pmr_redshift_config, **engine_manager_kwargs)
database_name = engine_manager.engine.url.database

Expand All @@ -78,7 +78,7 @@ def _sync(_redshift_container, pmr_redshift_config):
yield engine

@pytest.fixture(scope=scope)
async def _async(_redshift_container, pmr_redshift_config):
async def _async(pmr_redshift_container, pmr_redshift_config):
engine_manager = create_engine_manager(pmr_redshift_config, **engine_manager_kwargs)
database_name = engine_manager.engine.url.database

Expand Down
41 changes: 21 additions & 20 deletions src/pytest_mock_resources/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def pytest_itemcollected(item):

fixturenames = set(item.fixturenames)
for resource_kind in _resource_kinds:
resource_fixture = "_{}_container".format(resource_kind)
resource_fixture = "pmr_{}_container".format(resource_kind)
if resource_fixture in fixturenames:
item.add_marker(resource_kind)

Expand Down Expand Up @@ -97,24 +97,25 @@ def pytest_sessionfinish(session, exitstatus):
# PMR runs failed to clean up their container, subsequent runs. Ironically
# this might also lead to literal concurrent runs of unrelated PMR-enabled
# pytest runs to clobber one another...:shrug:.
fn = get_tmp_root(session.config)
with load_container_lockfile(fn) as containers:
if not containers:
return

version = get_env_config("docker", "api_version", "auto")
client = docker.from_env(version=version)
while containers:
container_id = containers.pop(0)

try:
container = client.containers.get(container_id)
except Exception:
warnings.warn(f"Unrecognized container {container_id}")
else:
roots = [get_tmp_root(session.config), get_tmp_root(session.config, parent=True)]
for fn in roots:
with load_container_lockfile(fn) as containers:
if not containers:
continue

version = get_env_config("docker", "api_version", "auto")
client = docker.from_env(version=version)
while containers:
container_id = containers.pop(0)

try:
container.kill()
container = client.containers.get(container_id)
except Exception:
warnings.warn(f"Failed to kill container {container_id}")

fn.unlink()
warnings.warn(f"Unrecognized container {container_id}")
else:
try:
container.kill()
except Exception:
warnings.warn(f"Failed to kill container {container_id}")

fn.unlink()
7 changes: 7 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import os

import pytest

from pytest_mock_resources.compat import sqlalchemy
Expand All @@ -6,3 +8,8 @@
sqlalchemy.version.startswith("1.4") or sqlalchemy.version.startswith("2."),
reason="Incompatible with sqlalchemy 2 behavior",
)

skip_if_ci = pytest.mark.skipif(
os.environ.get("CI") == "true",
reason="Incompatible with CI behavior",
)
Empty file.
Loading

0 comments on commit 0d1474a

Please sign in to comment.