Create container with dynamic scope
LostInDarkMath opened this issue · 7 comments
Hi there!
I wonder if there is any way to create containers with dynamic scopes. I would like to do something like this:
my_container = container(
image='{my_image.id}',
ports={'3306/tcp': None},
network='{my_net.name}',
tmpfs={'/var/lib/my_dirl': ''},
environment={
'PASSWORD': 'pw',
},
scope=SCOPE # instead of 'session'
)
My problem is, that I read my_scope
from the pytest command line interface via:
def pytest_addoption(parser):
"""Add command line option to pytest. This is just a flag"""
parser.addoption(CLI_ARG_SCOPE_FUNCTION, action="store_true") # store_true implies default=False
def get_scope(fixture_name: str, config: Config) -> str:
"""https://docs.pytest.org/en/stable/fixture.html#dynamic-scope
Command "pytest maddoxtests" will start tests with scope session
Command "pytest maddoxtests --function-scope" will start tests with function scope
"""
scope = Scope.SESSION
if config.getoption(name=CLI_ARG_SCOPE_FUNCTION, default=None):
scope = Scope.FUNCTION
print(f'Started pytest with scope "{scope}"')
global SCOPE
SCOPE = scope
return scope
So the global variable SCOPE
is not set when the containers are created which leads to an error (session cannot be None or something like that).
So I tried to use a fixture do create my containers:
@pytest.fixture(scope=get_scope, name='con')
def my_container() -> Container:
assert SCOPE in [Scope.SESSION, Scope.FUNCTION]
yield container(
image='{my_image.id}',
ports={'8086/tcp': None},
network='{my_net.name}',
tmpfs={'/var/lib/my_dir': ''},
environment={
'PASSWORD': 'pw',
},
scope=SCOPE
)
@pytest.fixture(scope=get_scope, autouse=True)
def test_env_scope_session(con):
ip, port = con.get_addr('8086/tcp') # error
This leads to an error: AttributeError: 'function' object has no attribute 'get_addr'I expected
conto be an
Container` like before, but it is a Function.
Even ip, port = con().get_addr('8086/tcp')
does not work either. This produces the error Fixture "container" called directly. Fixtures are not meant to be called directly, but are created automatically when test functions request them as parameters.
Do you have any idea have I can do this?
Best, Willi
container
creates fixtures. It's a factory for fixtures that themselves return Container
s iirc, rather than a factory for Container
s. So I don't think the return value is something you can yield from another fixture, though its been a while since i worked on pytest stuff. 'con' is probably not actually a Container
and instead you yielded a fixture functionn that pytest won't know how to call. I think that explains all the errors you are getting.
I don't actually know how to do this in pytest full stop. If you can demonstrate it working with a plain pytest.fixture
(without pytest_docker_tools) and pytest_addoption
it might help.
I have something similar where the image param is different between my dev environment and my CI environment. Again i couldn't figure out how to use pytest_addoption
so i just used an environment variable instead.
The addoption
works fine for me and the global variable SCOPE
is set correctly. But it is set too late because it will be set if pytest calls test_env_scope_session
. So what I need is to find way to call container()
later. Now it is called when conftest.py
starts.
But container
is a factory for creatinng methods to create containers that are wrapped in a pytest.fixture
decorator. Calling container()
isn't creating a container, its creating a function that pytest calls to create a container. So I think it has to run when it does, otherwise it isn't registered as a fixture and pytest won't run it. This is why in your example a function is passed to your test instead of a container object.
Which is why i asked to see an example of this without involving pytest_docker_tools at all. Basically, if you can make this dummy fixture work:
@pytest.fixture(scope=SCOPE)
def myfixture():
print("Dummy fixture")
yield {"dummy": True}
print("Freeing resources")
def test_myfixture(myfixture):
print(myfixture)
def pytest_addoption(parser):
"""Add command line option to pytest. This is just a flag"""
parser.addoption(CLI_ARG_SCOPE_FUNCTION, action="store_true") # store_true implies default=False
Then i might be able to make pytest_docker_tools support it too. Possibly.
Yes, I can make this work:
from typing import Literal
import pytest
from _pytest.config import Config
class Scope:
# type annotations are necessary here to avoid PyCharm type checker warnings
FUNCTION: Literal['function'] = 'function'
CLASS: Literal['class'] = 'class'
MODULE: Literal['module'] = 'module'
PACKAGE: Literal['package'] = 'package'
SESSION: Literal['session'] = 'session'
SCOPE = None
CLI_ARG_SCOPE_FUNCTION = '--function-scope'
def pytest_addoption(parser):
"""Add command line option to pytest. This is just a flag"""
parser.addoption(CLI_ARG_SCOPE_FUNCTION, action="store_true") # store_true implies default=False
def get_scope(fixture_name: str, config: Config) -> str:
"""https://docs.pytest.org/en/stable/fixture.html#dynamic-scope
Command "pytest conftest.py" will start tests with scope session
Command "pytest conftest.py --function-scope" will start tests with function scope
"""
scope = Scope.SESSION
if config.getoption(name=CLI_ARG_SCOPE_FUNCTION, default=None):
scope = Scope.FUNCTION
print(f'Started pytest with scope "{scope}"')
global SCOPE
SCOPE = scope
return scope
@pytest.fixture(scope=get_scope)
def myfixture():
print("Dummy fixture: " + SCOPE)
yield {"dummy": True}
print("Freeing resources")
def test_myfixture(myfixture):
print(myfixture)
Then pytest conftest.py --function-scope
gives me Started pytest with scope "function"
and pytest conftest.py
gives me Started pytest with scope "session"
as desired.
So how can I make this work with containers?
Its possible to get this working by not using get_scope() but simply evaluating sys.argv
@LostInDarkMath sorry I didn't see the notification for this, and i also missed the link in your doc string.
The thing that you have to understand is that when you call container()
under the hood a new fixture is generated.
So if you write:
mycontainer = container(
image='redis:latest',
scope=SCOPE,
)
It's (broadly) like you wrote this:
@pytest.fixture(scope=SCOPE)
def mycontainer():
return Container(
name = "mycontainer",
image = "redis",
)
(I'm simplifying quite a bit).
So i'd expect it's actually much closer to the example in the docs you referenced:
def determine_scope(fixture_name, config):
if config.getoption("--keep-containers", None):
return "session"
return "function"
my_container = container(
image='{my_image.id}',
ports={'3306/tcp': None},
network='{my_net.name}',
tmpfs={'/var/lib/my_dirl': ''},
environment={
'PASSWORD': 'pw',
},
scope=detemine_scope
)
Whatever you pass to the scope
argument of pytest.fixture you should be able to pass to the scope
argument of container()
.
I've added a version of this to the README (which are also run as tests themselves, via pytest-markdown).
I'm going to close this now as i think we've met the initial requirements.