smarie / python-pytest-harvest

Store data created during your `pytest` tests execution, and retrieve it at the end of the session, e.g. for applicative benchmarking purposes.
https://smarie.github.io/python-pytest-harvest/
BSD 3-Clause "New" or "Revised" License
63 stars 9 forks source link

Explicitly handle session- and module- scoped fixtures with `@saved_fixture` #17

Closed smarie closed 5 years ago

smarie commented 5 years ago

As of today @saved_fixture can only be used for function-scoped fixtures, but the error message is cryptic:

"Internal Error - This fixture 'xxxxxxx' was already stored for test id ''"

We could at least detect this and raise a better exception.

We could also go further and propose specific ways to store session- or module-scoped fixtures. The main problem is that session-scoped fixtures are not guaranteed to be unique by session, and module-scoped fixtures are not guaranteed to be unique for an unique parameter, in pytest. See https://github.com/pytest-dev/pytest/issues/2846

Sup3rGeo commented 5 years ago

Hi @smarie! Just a feedback that I was so happy that pytest-harvest was exactly what we need for one use case we have, until I hit the message (already improved it seems) that @saved_fixtures only work for function-scoped fixtures, which was then a deal breaker and led us to implement this manually.

module-scoped fixtures are not guaranteed to be unique for an unique parameter

hmm I think they definitely should, at least theoretically.

Sup3rGeo commented 5 years ago

I was giving this a little more thought, and I think we could do at least some of the following things. Either:

I think that I would prefer alternative 2, because then pytest-harvest can still be useful and is up to the user to understand how pytest works.

smarie commented 5 years ago

Sorry for the delay in answering, I was away - nice proposal indeed.

hmm I think they definitely should, at least theoretically.

Yes I was surprised too but apparently there is a "good reason" : if a fixture has two parameters (a, b) and the test order uses a, then b, then a. When current test requires parameter b, then the previously created fixture for parameter a is torn down, even if it is used afterwards again by the following test in the same module.

You have probably seen this answer. I personally do not like this idea but their philosophy seems that a fixture should not be "alive" in memory if current test does not use it. That's why the fixture for param 'a' is torn down then recreated in the above example. We should maybe ask for an option to "leave the session and module fixtures alive even if they are not used". That (and only that) will guarantee uniqueness. I mention it at the end of https://github.com/pytest-dev/pytest/issues/3393.

smarie commented 5 years ago

I suggest to allow session and module scoped fixtures to be stored but to store them exactly the same way than function-scope: with an inner key equal to the test id they where created for.

That mean that if you have a fixture with session scope used in two tests but not recreated, only the first test id will appear. However if it is recreated, it will appear under each test id where it was recreated.

Would that suit your need ?

smarie commented 5 years ago

Example 1: if the fixture does not change only a single test entry will appear:

@pytest.fixture(scope='session')
@saved_fixture
def my_fix(request):
    return 1

def test_foo(my_fix):
    assert my_fix == 1

def test_bar(my_fix):
    assert my_fix == 1

def test_synthesis(fixture_store):
    print(fixture_store['my_fix'])

The print should contain only one entry with name test_foo:

{'pytest_harvest/tests_raw/test_saved_fixture_session_no_params.py::test_foo': 1}
smarie commented 5 years ago

Example 2: re-creation. If the fixture is instantiated by pytest several times (whatever the reason),there will be an entry for each time it was setup.

@pytest.fixture(scope='module', params=[1, 2])
@saved_fixture
def my_fix():
    return 1

@pytest.fixture(scope='module', params=['a', 'b'])
@saved_fixture
def my_fix2():
    return 1

def test_foo(my_fix):
    assert my_fix == 1

def test_bar(my_fix, my_fix2):
    assert my_fix == 1

def test_synthesis(fixture_store):
    print(list(fixture_store['my_fix'].keys()))
    print(list(fixture_store['my_fix2'].keys()))

Yields the following "strange but true" pytest execution order as explained by this post:

test_saved_fixture_module_params.py::test_foo[1] 
test_saved_fixture_module_params.py::test_bar[1-a] 
test_saved_fixture_module_params.py::test_bar[2-a] 
test_saved_fixture_module_params.py::test_foo[2] 
test_saved_fixture_module_params.py::test_bar[2-b] 
test_saved_fixture_module_params.py::test_bar[1-b]

And as proposed, the fixtures are only stored when they are re-created:

(this is my_fix, that is created 3 times for parameter 1, 2, 1)
['pytest_harvest/tests_raw/test_saved_fixture_module_params.py::test_foo[1]', 
 'pytest_harvest/tests_raw/test_saved_fixture_module_params.py::test_bar[2-a]', 
 'pytest_harvest/tests_raw/test_saved_fixture_module_params.py::test_bar[1-b]']

(this is my_fix2 that is created 2 times for parameter a, b)
['pytest_harvest/tests_raw/test_saved_fixture_module_params.py::test_bar[1-a]', 
'pytest_harvest/tests_raw/test_saved_fixture_module_params.py::test_bar[2-b]']

Let me know if that is ok for you. In that case, I'll commit and push this in a new release.

Sup3rGeo commented 5 years ago

Hi @smarie, glad you are back! Yes, it would work for me like this. In fact it is probably the best way of handling this situation. Waiting to test it :)