[py-dev] Performance tests with py.test

Bogdan Opanchuk mantihor at gmail.com
Sun Apr 1 05:09:50 CEST 2012


Hi Holger,

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.

> 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
- 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)

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.

-----
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
-----



More information about the Pytest-dev mailing list