How to print material in py.test finalizer

I am testing a function that writes to a log file (it doesnโ€™t matter what it writes to the log file specifically, it can do something, this is just what generated this question)

Something like that:

def do_stuff(): with open('/tmp/mylogs.txt', 'a') as f: f.write(str(time.time())) f.write(' stuff done! \n') return 42 

And I can check it something like this:

 def test_doing_stuff(watch_logs): assert do_stuff() == 42 assert do_stuff() == 43 

For debugging purposes, when the test fails, I want to be able to print new log entries - I can make the device a bit as follows:

 @pytest.fixture() def watch_logs(request): with open('/tmp/mylogs.txt') as f: log_before = f.read() def get_new_logs(): with open('/tmp/mylogs.txt') as f: log_after = f.read() return log_after.replace(log_before, '') return get_new_logs 

Great - now I can check the contents of the log at any time in my tests:

 def test_doing_stuff(watch_logs): assert do_stuff() == 42 print(watch_logs()) assert do_stuff() == 43 print(watch_logs()) 

Hm - ah, but this second print will not work, it is after a test failure.

What if my test fixture always printed logs at the end of the test? Then the pytest stdout capture will show it to me when it fails, but not when it passes!

 @pytest.fixture() def watch_logs(request): with open('/tmp/mylogs.txt') as f: log_before = f.read() def get_new_logs(): with open('/tmp/mylogs.txt') as f: log_after = f.read() return log_after.replace(log_before, '') def print_new_logs(): print('~' * 20 + ' logs ' + '~' * 20) print(get_new_logs()) print('~' * 50) request.addfinalizer(print_new_logs) return get_new_logs 

Oh, but it doesnโ€™t work because the capture of the pytests log does not occur during test finalizers.

So the question is: how can I make a test finalizer that can print material?

Here's the ultra-minimal meaning of no (non-essential) material for logging: https://gist.github.com/hjwp/5154ec40a476a5c01ba6

+5
source share
2 answers

Thanks to the help of Holger himself (thanks @ hpk42!), I have something that works. Only slightly magic / hacks.

The solution is to use a py.test hook called pytest_pyfunc_call along with a decorator called hookwrapper . They give me a way to hook some code both before and after the test, but are also not at risk of capturing stdout.

Define a new function in conftest.py:

 # conftest.py @pytest.mark.hookwrapper def pytest_pyfunc_call(pyfuncitem): yield print('this happens after the test runs') if 'watch_logs' in pyfuncitem.funcargs: print(pyfuncitem.funcargs['watch_logs']()) 

And now, if pytest drops any test that uses the watch_logs device, it will print its result after the test runs.

Full example: https://gist.github.com/hjwp/4294f0acbef5345a7d46

+2
source

There is no documentary or pure way to achieve it, but here's a hack:

 # conftest.py def pytest_runtest_call(item): if hasattr(item, "_request"): if hasattr(item._request, "_addoutput_on_failure"): item._request._addoutput_on_failure() # test_x.py import pytest @pytest.fixture def print_on_fail(request): def add(): print ("test failed") request._parent_request._addoutput_on_failure = add def test_doing_stuff(print_on_fail): assert False 

We might think about the correct request.addcall_on_failure(callback) API.

Doing work with the yield_fixture tag requires some internal, probably nontrivial refactoring.

+3
source

All Articles