from __future__ import division from __future__ import print_function import argparse import operator import platform import sys import traceback from collections import defaultdict from datetime import datetime import pytest from . import __version__ from .fixture import BenchmarkFixture from .session import BenchmarkSession from .session import PerformanceRegression from .timers import default_timer from .utils import NameWrapper from .utils import format_dict from .utils import get_commit_info from .utils import get_current_time from .utils import get_tag from .utils import operations_unit from .utils import parse_columns from .utils import parse_compare_fail from .utils import parse_name_format from .utils import parse_rounds from .utils import parse_save from .utils import parse_seconds from .utils import parse_sort from .utils import parse_timer from .utils import parse_warmup from .utils import time_unit def pytest_report_header(config): bs = config._benchmarksession return ("benchmark: {version} (defaults:" " timer={timer}" " disable_gc={0[disable_gc]}" " min_rounds={0[min_rounds]}" " min_time={0[min_time]}" " max_time={0[max_time]}" " calibration_precision={0[calibration_precision]}" " warmup={0[warmup]}" " warmup_iterations={0[warmup_iterations]}" ")").format( bs.options, version=__version__, timer=bs.options.get("timer"), ) def add_display_options(addoption, prefix="benchmark-"): addoption( "--{0}sort".format(prefix), metavar="COL", type=parse_sort, default="min", help="Column to sort on. Can be one of: 'min', 'max', 'mean', 'stddev', " "'name', 'fullname'. Default: %(default)r" ) addoption( "--{0}group-by".format(prefix), metavar="LABEL", default="group", help="How to group tests. Can be one of: 'group', 'name', 'fullname', 'func', 'fullfunc', " "'param' or 'param:NAME', where NAME is the name passed to @pytest.parametrize." " Default: %(default)r" ) addoption( "--{0}columns".format(prefix), metavar="LABELS", type=parse_columns, default=["min", "max", "mean", "stddev", "median", "iqr", "outliers", "ops", "rounds", "iterations"], help="Comma-separated list of columns to show in the result table. Default: " "'min, max, mean, stddev, median, iqr, outliers, ops, rounds, iterations'" ) addoption( "--{0}name".format(prefix), metavar="FORMAT", type=parse_name_format, default="normal", help="How to format names in results. Can be one of 'short', 'normal', 'long', or 'trial'. Default: %(default)r" ) def add_histogram_options(addoption, prefix="benchmark-"): filename_prefix = "benchmark_%s" % get_current_time() addoption( "--{0}histogram".format(prefix), action="append", metavar="FILENAME-PREFIX", nargs="?", default=[], const=filename_prefix, help="Plot graphs of min/max/avg/stddev over time in FILENAME-PREFIX-test_name.svg. If FILENAME-PREFIX contains" " slashes ('/') then directories will be created. Default: %r" % filename_prefix ) def add_csv_options(addoption, prefix="benchmark-"): filename_prefix = "benchmark_%s" % get_current_time() addoption( "--{0}csv".format(prefix), action="append", metavar="FILENAME", nargs="?", default=[], const=filename_prefix, help="Save a csv report. If FILENAME contains" " slashes ('/') then directories will be created. Default: %r" % filename_prefix ) def add_global_options(addoption, prefix="benchmark-"): addoption( "--{0}storage".format(prefix), *[] if prefix else ['-s'], metavar="URI", default="file://./.benchmarks", help="Specify a path to store the runs as uri in form file://path or" " elasticsearch+http[s]://host1,host2/[index/doctype?project_name=Project] " "(when --benchmark-save or --benchmark-autosave are used). For backwards compatibility unexpected values " "are converted to file://<value>. Default: %(default)r." ) addoption( "--{0}netrc".format(prefix), nargs="?", default='', const='~/.netrc', help="Load elasticsearch credentials from a netrc file. Default: %(default)r.", ) addoption( "--{0}verbose".format(prefix), *[] if prefix else ['-v'], action="store_true", default=False, help="Dump diagnostic and progress information." ) def pytest_addoption(parser): group = parser.getgroup("benchmark") group.addoption( "--benchmark-min-time", metavar="SECONDS", type=parse_seconds, default="0.000005", help="Minimum time per round in seconds. Default: %(default)r" ) group.addoption( "--benchmark-max-time", metavar="SECONDS", type=parse_seconds, default="1.0", help="Maximum run time per test - it will be repeated until this total time is reached. It may be " "exceeded if test function is very slow or --benchmark-min-rounds is large (it takes precedence). " "Default: %(default)r" ) group.addoption( "--benchmark-min-rounds", metavar="NUM", type=parse_rounds, default=5, help="Minimum rounds, even if total time would exceed `--max-time`. Default: %(default)r" ) group.addoption( "--benchmark-timer", metavar="FUNC", type=parse_timer, default=str(NameWrapper(default_timer)), help="Timer to use when measuring time. Default: %(default)r" ) group.addoption( "--benchmark-calibration-precision", metavar="NUM", type=int, default=10, help="Precision to use when calibrating number of iterations. Precision of 10 will make the timer look 10 times" " more accurate, at a cost of less precise measure of deviations. Default: %(default)r" ) group.addoption( "--benchmark-warmup", metavar="KIND", nargs="?", default=parse_warmup("auto"), type=parse_warmup, help="Activates warmup. Will run the test function up to number of times in the calibration phase. " "See `--benchmark-warmup-iterations`. Note: Even the warmup phase obeys --benchmark-max-time. " "Available KIND: 'auto', 'off', 'on'. Default: 'auto' (automatically activate on PyPy)." ) group.addoption( "--benchmark-warmup-iterations", metavar="NUM", type=int, default=100000, help="Max number of iterations to run in the warmup phase. Default: %(default)r" ) group.addoption( "--benchmark-disable-gc", action="store_true", default=False, help="Disable GC during benchmarks." ) group.addoption( "--benchmark-skip", action="store_true", default=False, help="Skip running any tests that contain benchmarks." ) group.addoption( "--benchmark-disable", action="store_true", default=False, help="Disable benchmarks. Benchmarked functions are only ran once and no stats are reported. Use this is you " "want to run the test but don't do any benchmarking." ) group.addoption( "--benchmark-enable", action="store_true", default=False, help="Forcibly enable benchmarks. Use this option to override --benchmark-disable (in case you have it in " "pytest configuration)." ) group.addoption( "--benchmark-only", action="store_true", default=False, help="Only run benchmarks. This overrides --benchmark-skip." ) group.addoption( "--benchmark-save", metavar="NAME", type=parse_save, help="Save the current run into 'STORAGE-PATH/counter_NAME.json'." ) tag = get_tag() group.addoption( "--benchmark-autosave", action='store_const', const=tag, help="Autosave the current run into 'STORAGE-PATH/counter_%s.json" % tag, ) group.addoption( "--benchmark-save-data", action="store_true", help="Use this to make --benchmark-save and --benchmark-autosave include all the timing data," " not just the stats.", ) group.addoption( "--benchmark-json", metavar="PATH", type=argparse.FileType('wb'), help="Dump a JSON report into PATH. " "Note that this will include the complete data (all the timings, not just the stats)." ) group.addoption( "--benchmark-compare", metavar="NUM|_ID", nargs="?", default=[], const=True, help="Compare the current run against run NUM (or prefix of _id in elasticsearch) or the latest " "saved run if unspecified." ) group.addoption( "--benchmark-compare-fail", metavar="EXPR", nargs="+", type=parse_compare_fail, help="Fail test if performance regresses according to given EXPR" " (eg: min:5%% or mean:0.001 for number of seconds). Can be used multiple times." ) group.addoption( "--benchmark-cprofile", metavar="COLUMN", default=None, choices=['ncalls_recursion', 'ncalls', 'tottime', 'tottime_per', 'cumtime', 'cumtime_per', 'function_name'], help="If specified measure one run with cProfile and stores 25 top functions." " Argument is a column to sort by. Available columns: 'ncallls_recursion'," " 'ncalls', 'tottime', 'tottime_per', 'cumtime', 'cumtime_per', 'function_name'." ) add_global_options(group.addoption) add_display_options(group.addoption) add_histogram_options(group.addoption) def pytest_addhooks(pluginmanager): from . import hookspec method = getattr(pluginmanager, "add_hookspecs", None) if method is None: method = pluginmanager.addhooks method(hookspec) def pytest_benchmark_compare_machine_info(config, benchmarksession, machine_info, compared_benchmark): machine_info = format_dict(machine_info) compared_machine_info = format_dict(compared_benchmark["machine_info"]) if compared_machine_info != machine_info: benchmarksession.logger.warn( "Benchmark machine_info is different. Current: %s VS saved: %s (location: %s)." % ( machine_info, compared_machine_info, benchmarksession.storage.location, ) ) @pytest.hookimpl(hookwrapper=True) def pytest_runtest_call(item): bs = item.config._benchmarksession fixture = hasattr(item, "funcargs") and item.funcargs.get("benchmark") if isinstance(fixture, BenchmarkFixture): if bs.skip: pytest.skip("Skipping benchmark (--benchmark-skip active).") else: yield else: if bs.only: pytest.skip("Skipping non-benchmark (--benchmark-only active).") else: yield def pytest_benchmark_group_stats(config, benchmarks, group_by): groups = defaultdict(list) for bench in benchmarks: key = () for grouping in group_by.split(','): if grouping == "group": key += bench["group"], elif grouping == "name": key += bench["name"], elif grouping == "func": key += bench["name"].split("[")[0], elif grouping == "fullname": key += bench["fullname"], elif grouping == "fullfunc": key += bench["fullname"].split("[")[0], elif grouping == "param": key += bench["param"], elif grouping.startswith("param:"): param_name = grouping[len("param:"):] key += '%s=%s' % (param_name, bench["params"][param_name]), else: raise NotImplementedError("Unsupported grouping %r." % group_by) groups[' '.join(str(p) for p in key if p) or None].append(bench) for grouped_benchmarks in groups.values(): grouped_benchmarks.sort(key=operator.itemgetter("fullname" if "full" in group_by else "name")) return sorted(groups.items(), key=lambda pair: pair[0] or "") @pytest.hookimpl(hookwrapper=True) def pytest_sessionfinish(session, exitstatus): session.config._benchmarksession.finish() yield def pytest_terminal_summary(terminalreporter): try: terminalreporter.config._benchmarksession.display(terminalreporter) except PerformanceRegression: raise except Exception: terminalreporter.config._benchmarksession.logger.error("\n%s" % traceback.format_exc()) raise def get_cpu_info(): import cpuinfo all_info = cpuinfo.get_cpu_info() all_info = all_info or {} info = {} for key in ('vendor_id', 'hardware', 'brand'): info[key] = all_info.get(key, 'unknown') return info def pytest_benchmark_scale_unit(config, unit, benchmarks, best, worst, sort): if unit == 'seconds': time_unit_key = sort if sort in ("name", "fullname"): time_unit_key = "min" return time_unit(best.get(sort, benchmarks[0][time_unit_key])) elif unit == 'operations': return operations_unit(worst.get('ops', benchmarks[0]['ops'])) else: raise RuntimeError("Unexpected measurement unit %r" % unit) def pytest_benchmark_generate_machine_info(): python_implementation = platform.python_implementation() python_implementation_version = platform.python_version() if python_implementation == 'PyPy': python_implementation_version = '%d.%d.%d' % sys.pypy_version_info[:3] if sys.pypy_version_info.releaselevel != 'final': python_implementation_version += '-%s%d' % sys.pypy_version_info[3:] return { "node": platform.node(), "processor": platform.processor(), "machine": platform.machine(), "python_compiler": platform.python_compiler(), "python_implementation": python_implementation, "python_implementation_version": python_implementation_version, "python_version": platform.python_version(), "python_build": platform.python_build(), "release": platform.release(), "system": platform.system(), "cpu": get_cpu_info(), } def pytest_benchmark_generate_commit_info(config): return get_commit_info(config.getoption("benchmark_project_name", None)) def pytest_benchmark_generate_json(config, benchmarks, include_data, machine_info, commit_info): benchmarks_json = [] output_json = { "machine_info": machine_info, "commit_info": commit_info, "benchmarks": benchmarks_json, "datetime": datetime.utcnow().isoformat(), "version": __version__, } for bench in benchmarks: if not bench.has_error: benchmarks_json.append(bench.as_dict(include_data=include_data)) return output_json @pytest.fixture(scope="function") def benchmark(request): bs = request.config._benchmarksession if bs.skip: pytest.skip("Benchmarks are skipped (--benchmark-skip was used).") else: node = request.node marker = node.get_closest_marker("benchmark") options = dict(marker.kwargs) if marker else {} if "timer" in options: options["timer"] = NameWrapper(options["timer"]) fixture = BenchmarkFixture( node, add_stats=bs.benchmarks.append, logger=bs.logger, warner=request.node.warn, disabled=bs.disabled, **dict(bs.options, **options) ) request.addfinalizer(fixture._cleanup) return fixture @pytest.fixture(scope="function") def benchmark_weave(benchmark): return benchmark.weave def pytest_runtest_setup(item): marker = item.get_closest_marker("benchmark") if marker: if marker.args: raise ValueError("benchmark mark can't have positional arguments.") for name in marker.kwargs: if name not in ( "max_time", "min_rounds", "min_time", "timer", "group", "disable_gc", "warmup", "warmup_iterations", "calibration_precision", "cprofile"): raise ValueError("benchmark mark can't have %r keyword argument." % name) @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item, call): outcome = yield fixture = hasattr(item, "funcargs") and item.funcargs.get("benchmark") if fixture: fixture.skipped = outcome.get_result().outcome == 'skipped' @pytest.mark.trylast # force the other plugins to initialise, fixes issue with capture not being properly initialised def pytest_configure(config): config.addinivalue_line("markers", "benchmark: mark a test with custom benchmark settings.") bs = config._benchmarksession = BenchmarkSession(config) bs.handle_loading() config.pluginmanager.register(bs, "pytest-benchmark")