Completed
Push — master ( 7b504c...b6f606 )
by Ionel Cristian
01:16
created

src.pytest_benchmark.pytest_benchmark_group_stats()   F

Complexity

Conditions 15

Size

Total Lines 27

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 15
dl 0
loc 27
rs 2.7451

How to fix   Complexity   

Complexity

Complex classes like src.pytest_benchmark.pytest_benchmark_group_stats() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
from __future__ import division
2
from __future__ import print_function
3
4
import argparse
5
import operator
6
import platform
7
import sys
8
import traceback
9
from collections import defaultdict
10
from datetime import datetime
11
12
import pytest
13
14
from . import __version__
15
from .fixture import BenchmarkFixture
16
from .session import BenchmarkSession
17
from .session import PerformanceRegression
18
from .timers import default_timer
19
from .utils import NameWrapper, parse_name_format
20
from .utils import format_dict
21
from .utils import get_commit_info
22
from .utils import get_current_time
23
from .utils import get_tag
24
from .utils import parse_columns
25
from .utils import parse_compare_fail
26
from .utils import parse_rounds
27
from .utils import parse_save
28
from .utils import parse_seconds
29
from .utils import parse_sort
30
from .utils import parse_timer
31
from .utils import parse_warmup
32
33
34
def pytest_report_header(config):
35
    bs = config._benchmarksession
36
37
    return ("benchmark: {version} (defaults:"
38
            " timer={timer}"
39
            " disable_gc={0[disable_gc]}"
40
            " min_rounds={0[min_rounds]}"
41
            " min_time={0[min_time]}"
42
            " max_time={0[max_time]}"
43
            " calibration_precision={0[calibration_precision]}"
44
            " warmup={0[warmup]}"
45
            " warmup_iterations={0[warmup_iterations]}"
46
            ")").format(
47
        bs.options,
48
        version=__version__,
49
        timer=bs.options.get("timer"),
50
    )
51
52
53
def add_display_options(addoption, prefix="benchmark-"):
54
    addoption(
55
        "--{0}sort".format(prefix),
56
        metavar="COL", type=parse_sort, default="min",
57
        help="Column to sort on. Can be one of: 'min', 'max', 'mean', 'stddev', "
58
             "'name', 'fullname'. Default: %(default)r"
59
    )
60
    addoption(
61
        "--{0}group-by".format(prefix),
62
        metavar="LABEL", default="group",
63
        help="How to group tests. Can be one of: 'group', 'name', 'fullname', 'func', 'fullfunc', "
64
             "'param' or 'param:NAME', where NAME is the name passed to @pytest.parametrize."
65
             " Default: %(default)r"
66
    )
67
    addoption(
68
        "--{0}columns".format(prefix),
69
        metavar="LABELS", type=parse_columns,
70
        default="min, max, mean, stddev, median, iqr, outliers, rounds, iterations",
71
        help="Comma-separated list of columns to show in the result table. Default: %(default)r"
72
    )
73
    addoption(
74
        "--{0}name".format(prefix),
75
        metavar="FORMAT", type=parse_name_format,
76
        default="normal",
77
        help="How to format names in results. Can be one of 'short', 'normal', 'long'. Default: %(default)r"
78
    )
79
80
81
def add_histogram_options(addoption, prefix="benchmark-"):
82
    filename_prefix = "benchmark_%s" % get_current_time()
83
    addoption(
84
        "--{0}histogram".format(prefix),
85
        action="append", metavar="FILENAME-PREFIX", nargs="?", default=[], const=filename_prefix,
86
        help="Plot graphs of min/max/avg/stddev over time in FILENAME-PREFIX-test_name.svg. If FILENAME-PREFIX contains"
87
             " slashes ('/') then directories will be created. Default: %r" % filename_prefix
88
    )
89
90
91
def add_csv_options(addoption, prefix="benchmark-"):
92
    filename_prefix = "benchmark_%s" % get_current_time()
93
    addoption(
94
        "--{0}csv".format(prefix),
95
        action="append", metavar="FILENAME", nargs="?", default=[], const=filename_prefix,
96
        help="Save a csv report. If FILENAME contains"
97
             " slashes ('/') then directories will be created. Default: %r" % filename_prefix
98
    )
99
100
101
def add_global_options(addoption, prefix="benchmark-"):
102
    addoption(
103
        "--{0}storage".format(prefix),
104
        metavar="STORAGE-PATH", default="./.benchmarks",
105
        help="Specify a different path to store the runs (when --benchmark-save or --benchmark-autosave are used). "
106
             "Default: %(default)r",
107
    )
108
    addoption(
109
        "--{0}verbose".format(prefix),
110
        action="store_true", default=False,
111
        help="Dump diagnostic and progress information."
112
    )
113
114
115
def pytest_addoption(parser):
116
    group = parser.getgroup("benchmark")
117
    group.addoption(
118
        "--benchmark-min-time",
119
        metavar="SECONDS", type=parse_seconds, default="0.000005",
120
        help="Minimum time per round in seconds. Default: %(default)r"
121
    )
122
    group.addoption(
123
        "--benchmark-max-time",
124
        metavar="SECONDS", type=parse_seconds, default="1.0",
125
        help="Maximum run time per test - it will be repeated until this total time is reached. It may be "
126
             "exceeded if test function is very slow or --benchmark-min-rounds is large (it takes precedence). "
127
             "Default: %(default)r"
128
    )
129
    group.addoption(
130
        "--benchmark-min-rounds",
131
        metavar="NUM", type=parse_rounds, default=5,
132
        help="Minimum rounds, even if total time would exceed `--max-time`. Default: %(default)r"
133
    )
134
    group.addoption(
135
        "--benchmark-timer",
136
        metavar="FUNC", type=parse_timer, default=str(NameWrapper(default_timer)),
137
        help="Timer to use when measuring time. Default: %(default)r"
138
    )
139
    group.addoption(
140
        "--benchmark-calibration-precision",
141
        metavar="NUM", type=int, default=10,
142
        help="Precision to use when calibrating number of iterations. Precision of 10 will make the timer look 10 times"
143
             " more accurate, at a cost of less precise measure of deviations. Default: %(default)r"
144
    )
145
    group.addoption(
146
        "--benchmark-warmup",
147
        metavar="KIND", nargs="?", default=parse_warmup("auto"), type=parse_warmup,
148
        help="Activates warmup. Will run the test function up to number of times in the calibration phase. "
149
             "See `--benchmark-warmup-iterations`. Note: Even the warmup phase obeys --benchmark-max-time. "
150
             "Available KIND: 'auto', 'off', 'on'. Default: 'auto' (automatically activate on PyPy)."
151
    )
152
    group.addoption(
153
        "--benchmark-warmup-iterations",
154
        metavar="NUM", type=int, default=100000,
155
        help="Max number of iterations to run in the warmup phase. Default: %(default)r"
156
    )
157
    group.addoption(
158
        "--benchmark-disable-gc",
159
        action="store_true", default=False,
160
        help="Disable GC during benchmarks."
161
    )
162
    group.addoption(
163
        "--benchmark-skip",
164
        action="store_true", default=False,
165
        help="Skip running any tests that contain benchmarks."
166
    )
167
    group.addoption(
168
        "--benchmark-disable",
169
        action="store_true", default=False,
170
        help="Disable benchmarks. Benchmarked functions are only ran once and no stats are reported. Use this is you "
171
             "want to run the test but don't do any benchmarking."
172
    )
173
    group.addoption(
174
        "--benchmark-enable",
175
        action="store_true", default=False,
176
        help="Forcibly enable benchmarks. Use this option to override --benchmark-disable (in case you have it in "
177
             "pytest configuration)."
178
    )
179
    group.addoption(
180
        "--benchmark-only",
181
        action="store_true", default=False,
182
        help="Only run benchmarks."
183
    )
184
    group.addoption(
185
        "--benchmark-save",
186
        metavar="NAME", type=parse_save,
187
        help="Save the current run into 'STORAGE-PATH/counter_NAME.json'."
188
    )
189
    tag = get_tag()
190
    group.addoption(
191
        "--benchmark-autosave",
192
        action='store_const', const=tag,
193
        help="Autosave the current run into 'STORAGE-PATH/counter_%s.json" % tag,
194
    )
195
    group.addoption(
196
        "--benchmark-save-data",
197
        action="store_true",
198
        help="Use this to make --benchmark-save and --benchmark-autosave include all the timing data,"
199
             " not just the stats.",
200
    )
201
    group.addoption(
202
        "--benchmark-json",
203
        metavar="PATH", type=argparse.FileType('wb'),
204
        help="Dump a JSON report into PATH. "
205
             "Note that this will include the complete data (all the timings, not just the stats)."
206
    )
207
    group.addoption(
208
        "--benchmark-compare",
209
        metavar="NUM", nargs="?", default=[], const=True,
210
        help="Compare the current run against run NUM or the latest saved run if unspecified."
211
    )
212
    group.addoption(
213
        "--benchmark-compare-fail",
214
        metavar="EXPR", nargs="+", type=parse_compare_fail,
215
        help="Fail test if performance regresses according to given EXPR"
216
             " (eg: min:5%% or mean:0.001 for number of seconds). Can be used multiple times."
217
    )
218
    add_global_options(group.addoption)
219
    add_display_options(group.addoption)
220
    add_histogram_options(group.addoption)
221
222
223
def pytest_addhooks(pluginmanager):
224
    from . import hookspec
225
226
    method = getattr(pluginmanager, "add_hookspecs", None)
227
    if method is None:
228
        method = pluginmanager.addhooks
229
    method(hookspec)
230
231
232
def pytest_benchmark_compare_machine_info(config, benchmarksession, machine_info, compared_benchmark):
233
    machine_info = format_dict(machine_info)
234
    compared_machine_info = format_dict(compared_benchmark["machine_info"])
235
236
    if compared_machine_info != machine_info:
237
        benchmarksession.logger.warn(
238
            "BENCHMARK-C6",
239
            "Benchmark machine_info is different. Current: %s VS saved: %s." % (
240
                machine_info,
241
                compared_machine_info,
242
            ),
243
            fslocation=benchmarksession.storage.location
244
        )
245
246
if hasattr(pytest, 'hookimpl'):
247
    _hookwrapper = pytest.hookimpl(hookwrapper=True)
248
else:
249
    _hookwrapper = pytest.mark.hookwrapper
250
251
252
@_hookwrapper
253
def pytest_runtest_call(item):
254
    bs = item.config._benchmarksession
255
    fixure = hasattr(item, "funcargs") and item.funcargs.get("benchmark")
256
    if isinstance(fixure, BenchmarkFixture):
257
        if bs.skip:
258
            pytest.skip("Skipping benchmark (--benchmark-skip active).")
259
        else:
260
            yield
261
    else:
262
        if bs.only:
263
            pytest.skip("Skipping non-benchmark (--benchmark-only active).")
264
        else:
265
            yield
266
267
268
def pytest_benchmark_group_stats(config, benchmarks, group_by):
269
    groups = defaultdict(list)
270
    for bench in benchmarks:
271
        key = ()
272
        for grouping in group_by.split(','):
273
            if grouping == "group":
274
                key += bench["group"],
275
            elif grouping == "name":
276
                key += bench["name"],
277
            elif grouping == "func":
278
                key += bench["name"].split("[")[0],
279
            elif grouping == "fullname":
280
                key += bench["fullname"],
281
            elif grouping == "fullfunc":
282
                key += bench["fullname"].split("[")[0],
283
            elif grouping == "param":
284
                key += bench["param"],
285
            elif grouping.startswith("param:"):
286
                param_name = grouping[len("param:"):]
287
                key += '%s=%s' % (param_name, bench["params"][param_name]),
288
            else:
289
                raise NotImplementedError("Unsupported grouping %r." % group_by)
290
        groups[' '.join(str(p) for p in key if p) or None].append(bench)
291
292
    for grouped_benchmarks in groups.values():
293
        grouped_benchmarks.sort(key=operator.itemgetter("fullname" if "full" in group_by else "name"))
294
    return sorted(groups.items(), key=lambda pair: pair[0] or "")
295
296
297
@_hookwrapper
298
def pytest_sessionfinish(session, exitstatus):
299
    session.config._benchmarksession.finish()
300
    yield
301
302
303
def pytest_terminal_summary(terminalreporter):
304
    try:
305
        terminalreporter.config._benchmarksession.display(terminalreporter)
306
    except PerformanceRegression:
307
        raise
308
    except Exception:
309
        terminalreporter.config._benchmarksession.logger.error("\n%s" % traceback.format_exc())
310
        raise
311
312
313
def pytest_benchmark_generate_machine_info():
314
    python_implementation = platform.python_implementation()
315
    python_implementation_version = platform.python_version()
316
    if python_implementation == 'PyPy':
317
        python_implementation_version = '%d.%d.%d' % sys.pypy_version_info[:3]
318
        if sys.pypy_version_info.releaselevel != 'final':
319
            python_implementation_version += '-%s%d' % sys.pypy_version_info[3:]
320
    return {
321
        "node": platform.node(),
322
        "processor": platform.processor(),
323
        "machine": platform.machine(),
324
        "python_compiler": platform.python_compiler(),
325
        "python_implementation": python_implementation,
326
        "python_implementation_version": python_implementation_version,
327
        "python_version": platform.python_version(),
328
        "python_build": platform.python_build(),
329
        "release": platform.release(),
330
        "system": platform.system()
331
    }
332
333
334
def pytest_benchmark_generate_commit_info(config):
335
    return get_commit_info()
336
337
338
def pytest_benchmark_generate_json(config, benchmarks, include_data, machine_info, commit_info):
339
    benchmarks_json = []
340
    output_json = {
341
        "machine_info": machine_info,
342
        "commit_info": commit_info,
343
        "benchmarks": benchmarks_json,
344
        "datetime": datetime.utcnow().isoformat(),
345
        "version": __version__,
346
    }
347
    for bench in benchmarks:
348
        if not bench.has_error:
349
            benchmarks_json.append(bench.as_dict(include_data=include_data))
350
    return output_json
351
352
353
@pytest.fixture(scope="function")
354
def benchmark(request):
355
    bs = request.config._benchmarksession
356
357
    if bs.skip:
358
        pytest.skip("Benchmarks are skipped (--benchmark-skip was used).")
359
    else:
360
        node = request.node
361
        marker = node.get_marker("benchmark")
362
        options = marker.kwargs if marker else {}
363
        if "timer" in options:
364
            options["timer"] = NameWrapper(options["timer"])
365
        fixture = BenchmarkFixture(
366
            node,
367
            add_stats=bs.benchmarks.append,
368
            logger=bs.logger,
369
            warner=request.node.warn,
370
            disabled=bs.disabled,
371
            **dict(bs.options, **options)
372
        )
373
        request.addfinalizer(fixture._cleanup)
374
        return fixture
375
376
377
@pytest.fixture(scope="function")
378
def benchmark_weave(benchmark):
379
    return benchmark.weave
380
381
382
def pytest_runtest_setup(item):
383
    marker = item.get_marker("benchmark")
384
    if marker:
385
        if marker.args:
386
            raise ValueError("benchmark mark can't have positional arguments.")
387
        for name in marker.kwargs:
388
            if name not in (
389
                    "max_time", "min_rounds", "min_time", "timer", "group", "disable_gc", "warmup",
390
                    "warmup_iterations", "calibration_precision"):
391
                raise ValueError("benchmark mark can't have %r keyword argument." % name)
392
393
394
@pytest.mark.trylast  # force the other plugins to initialise, fixes issue with capture not being properly initialised
395
def pytest_configure(config):
396
    config.addinivalue_line("markers", "benchmark: mark a test with custom benchmark settings.")
397
    config._benchmarksession = BenchmarkSession(config)
398
    config.pluginmanager.register(config._benchmarksession, "pytest-benchmark")
399