[py-dev] Performance tests with py.test

holger krekel holger at merlinux.eu
Mon Jun 4 11:10:55 CEST 2012


Hi Bogdan,

sorry for taking a while ...

On Thu, May 03, 2012 at 11:18 +1000, Bogdan Opanchuk wrote:
> For some reason this message has not shown up in the list archives, so
> I'll resend it again.
> 
> On Sun, Apr 1, 2012 at 12:21 PM, holger krekel <holger at merlinux.eu> wrote:
> 
> > do you want to consider performance regressions as a failure?
> 
> Not really. I just need some table with performance results that I
> could get for different systems/versions and compare them. Besides,
> performance regressions can be implemented using existing
> functionality, because they do not have some continuous result
> associated with them — only pass/fail.
> 
> > Could you maybe provide a simple example test file and make up
> > some example output that you'd like to see?
> 
> Sure. Consider the following test file:
> 
> -----
> import pytest
> 
> def test_matrixmul():
>    pass
> 
> @pytest.mark.perf('seconds')
> def test_reduce():
>    # <some lengthy preparation that I do not want to time>
>    # <actual work>
>    return 1.0
> 
> @pytest.mark.perf('GFLOPS')
> def test_fft():
>    # <again, some lengthy preparation that I do not want to time>
>    # <actual work>
>    return 1.0, 1e10
> -----
> 
> Here "test_matrixmul()" is a normal pass/fail test, "test_reduce()" is
> marked as performance test that returns execution time, and
> "test_fft()" is marked as a test that returns execution time + the
> number of operations (thus allowing us to calculate GFLOPS value).
> 
> I have put together a clunky solution (see the end of this letter)
> using existing hooks that gives me more or less what I want to see:
> 
> $ py.test -v
> ...
> test_test.py:3: test_matrixmul PASSED
> test_test.py:6: test_reduce 1.0 s
> test_test.py:10: test_fft 10.0 GFLOPS
> ...
> 
> The only problem here is that I have to explicitly increase verbosity
> level. I'd prefer 'perf' marked tests show their result even for
> default verbosity, but I haven't found a way to do it yet.

not sure it helps but are you aware that you can put 

    [pytest]
    addopts = -v

in a pytest.ini file?

> > Meanwhile, if you haven't already you might want to look at the output
> > of "py.test --durations=10" and see about its implementation (mostly
> > contained in _pytest/runner.py, grep for 'duration').
> 
> Yes, I know about it, but it is not quite what I need:
> - it measures the time of the whole testcase, while I usually need to
> time only specific part

right.  It differentiates between setup and runtest phases for a test, though.

> - it does not allow me to measure anything more complicated (e.g.
> GFLOPS, as another variant I may want to see the error value)
> - it prints its report after all the tests are finished, while it is
> much more convenient to see testcase result as soon as it is finished
> (my performance tests may run for quite a long time)

right.

> So, the solution I have now is shown below. "pytest_pyfunc_call()"
> implementation annoys me the most, because I had to copy-paste it from
> python.py, so it exposes some py.test internals and can easily break
> when something (seemingly hidden) inside the library is changed.

That's true.  If you want to open an issue about this (pytest_pyfunc_call
to return test function return value), i can see to care about it.

> -----
> def pytest_configure(config):
>    config.pluginmanager.register(PerfPlugin(config), '_perf')
> 
> class PerfPlugin(object):
> 
>    def __init__(self, config):
>        pass
> 
>    def pytest_pyfunc_call(self, __multicall__, pyfuncitem):
>        # collect testcase return result
>        testfunction = pyfuncitem.obj
>        if pyfuncitem._isyieldedfunction():
>            res = testfunction(*pyfuncitem._args)
>        else:
>            funcargs = pyfuncitem.funcargs
>            res = testfunction(**funcargs)
>        pyfuncitem.result = res
> 
>    def pytest_report_teststatus(self, __multicall__, report):
>        outcome, letter, msg = __multicall__.execute()
> 
>        # if we have some result attached to the testcase, print it
> instead of 'PASSED'
>        if hasattr(report, 'result'):
>            msg = report.result
> 
>        return outcome, letter, msg
> 
>    def pytest_runtest_makereport(self, __multicall__, item, call):
>        report = __multicall__.execute()
> 
>        # if the testcase has passed, and has 'perf' marker, process its results
>        if call.when == 'call' and report.passed and
> hasattr(item.function, 'perf'):
>            perf = item.function.perf
>            perftype = perf.args[0]
>            if perftype == 'seconds':
>                report.result = str(item.result) + " s"
>            else:
>                seconds, operations = item.result
>                report.result = str(operations / seconds / 1e9) + " GFLOPS"
> 
>        return report
> -----

We could also think about a convention that would allow setting
the short/longform (letter,msg) on the report object so that could
also get rid of the report_teststatus hook.  I am slightly less
inclined to go for this because the above teststatus bit seems
nice enough.

best,
holger






More information about the Pytest-dev mailing list