pytest-codspeed 2.2.1__tar.gz → 3.0.0b0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (17) hide show
  1. {pytest_codspeed-2.2.1 → pytest_codspeed-3.0.0b0}/.gitignore +2 -0
  2. {pytest_codspeed-2.2.1 → pytest_codspeed-3.0.0b0}/PKG-INFO +15 -13
  3. {pytest_codspeed-2.2.1 → pytest_codspeed-3.0.0b0}/README.md +6 -6
  4. {pytest_codspeed-2.2.1 → pytest_codspeed-3.0.0b0}/pyproject.toml +18 -10
  5. {pytest_codspeed-2.2.1 → pytest_codspeed-3.0.0b0}/src/pytest_codspeed/__init__.py +1 -1
  6. pytest_codspeed-3.0.0b0/src/pytest_codspeed/instruments/__init__.py +60 -0
  7. pytest_codspeed-3.0.0b0/src/pytest_codspeed/instruments/valgrind/__init__.py +95 -0
  8. {pytest_codspeed-2.2.1/src/pytest_codspeed → pytest_codspeed-3.0.0b0/src/pytest_codspeed/instruments/valgrind}/_wrapper/.gitignore +1 -0
  9. {pytest_codspeed-2.2.1/src/pytest_codspeed → pytest_codspeed-3.0.0b0/src/pytest_codspeed/instruments/valgrind}/_wrapper/__init__.py +3 -1
  10. pytest_codspeed-3.0.0b0/src/pytest_codspeed/instruments/walltime.py +236 -0
  11. pytest_codspeed-3.0.0b0/src/pytest_codspeed/plugin.py +338 -0
  12. {pytest_codspeed-2.2.1 → pytest_codspeed-3.0.0b0}/src/pytest_codspeed/utils.py +33 -5
  13. pytest_codspeed-2.2.1/src/pytest_codspeed/plugin.py +0 -309
  14. {pytest_codspeed-2.2.1 → pytest_codspeed-3.0.0b0}/LICENSE +0 -0
  15. {pytest_codspeed-2.2.1/src/pytest_codspeed → pytest_codspeed-3.0.0b0/src/pytest_codspeed/instruments/valgrind}/_wrapper/wrapper.c +0 -0
  16. {pytest_codspeed-2.2.1/src/pytest_codspeed → pytest_codspeed-3.0.0b0/src/pytest_codspeed/instruments/valgrind}/_wrapper/wrapper.h +0 -0
  17. {pytest_codspeed-2.2.1/src/pytest_codspeed → pytest_codspeed-3.0.0b0/src/pytest_codspeed/instruments/valgrind}/_wrapper/wrapper.pyi +0 -0
@@ -162,3 +162,5 @@ cython_debug/
162
162
  .venvubuntu
163
163
  .python-version
164
164
  *.o
165
+
166
+ .codspeed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pytest-codspeed
3
- Version: 2.2.1
3
+ Version: 3.0.0b0
4
4
  Summary: Pytest plugin to create CodSpeed benchmarks
5
5
  Project-URL: Homepage, https://codspeed.io/
6
6
  Project-URL: Documentation, https://docs.codspeed.io/
@@ -35,27 +35,29 @@ Classifier: Intended Audience :: Developers
35
35
  Classifier: Intended Audience :: Information Technology
36
36
  Classifier: License :: OSI Approved :: MIT License
37
37
  Classifier: Programming Language :: Python :: 3
38
- Classifier: Programming Language :: Python :: 3.7
39
38
  Classifier: Programming Language :: Python :: 3.8
40
39
  Classifier: Programming Language :: Python :: 3.9
41
40
  Classifier: Programming Language :: Python :: 3.10
42
41
  Classifier: Programming Language :: Python :: 3.11
43
42
  Classifier: Programming Language :: Python :: 3.12
43
+ Classifier: Programming Language :: Python :: 3.13
44
44
  Classifier: Topic :: Software Development :: Testing
45
45
  Classifier: Topic :: System :: Benchmark
46
46
  Classifier: Topic :: Utilities
47
47
  Classifier: Typing :: Typed
48
- Requires-Python: >=3.7
49
- Requires-Dist: cffi>=1.15.1
48
+ Requires-Python: >=3.8
49
+ Requires-Dist: cffi>=1.17.1
50
50
  Requires-Dist: filelock>=3.12.2
51
+ Requires-Dist: importlib-metadata>=8.5.0; python_version < '3.10'
51
52
  Requires-Dist: pytest>=3.8
53
+ Requires-Dist: rich>=13.8.1
52
54
  Requires-Dist: setuptools; python_full_version >= '3.12.0'
53
55
  Provides-Extra: compat
54
56
  Requires-Dist: pytest-benchmark~=4.0.0; extra == 'compat'
55
- Requires-Dist: pytest-xdist~=2.0.0; extra == 'compat'
57
+ Requires-Dist: pytest-xdist~=3.6.1; extra == 'compat'
56
58
  Provides-Extra: lint
57
- Requires-Dist: mypy~=1.3.0; extra == 'lint'
58
- Requires-Dist: ruff~=0.3.3; extra == 'lint'
59
+ Requires-Dist: mypy~=1.11.2; extra == 'lint'
60
+ Requires-Dist: ruff~=0.6.5; extra == 'lint'
59
61
  Provides-Extra: test
60
62
  Requires-Dist: pytest-cov~=4.0.0; extra == 'test'
61
63
  Requires-Dist: pytest~=7.0; extra == 'test'
@@ -66,7 +68,7 @@ Description-Content-Type: text/markdown
66
68
 
67
69
  [![CI](https://github.com/CodSpeedHQ/pytest-codspeed/actions/workflows/ci.yml/badge.svg)](https://github.com/CodSpeedHQ/pytest-codspeed/actions/workflows/ci.yml)
68
70
  [![PyPi Version](https://img.shields.io/pypi/v/pytest-codspeed?color=%2334D058&label=pypi)](https://pypi.org/project/pytest-codspeed)
69
- ![Python Version](https://img.shields.io/badge/python-3.7%20|%203.8%20|%203.9%20|%203.10%20|%203.11%20|%203.12-informational.svg)
71
+ ![Python Version](https://img.shields.io/badge/python-3.8%20|%203.9%20|%203.10%20|%203.11%20|%203.12%20|%203.13-informational.svg)
70
72
  [![Discord](https://img.shields.io/badge/chat%20on-discord-7289da.svg)](https://discord.com/invite/MxpaCfKSqF)
71
73
  [![CodSpeed Badge](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json)](https://codspeed.io/CodSpeedHQ/pytest-codspeed)
72
74
 
@@ -76,7 +78,7 @@ Pytest plugin to create CodSpeed benchmarks
76
78
 
77
79
  ## Requirements
78
80
 
79
- **Python**: 3.7 and later
81
+ **Python**: 3.8 and later
80
82
 
81
83
  **pytest**: any recent version
82
84
 
@@ -163,14 +165,14 @@ jobs:
163
165
  benchmarks:
164
166
  runs-on: ubuntu-latest
165
167
  steps:
166
- - uses: actions/checkout@v3
167
- - uses: actions/setup-python@v3
168
+ - uses: actions/checkout@v4
169
+ - uses: actions/setup-python@v5
168
170
  with:
169
- python-version: "3.9"
171
+ python-version: "3.12"
170
172
  - name: Install dependencies
171
173
  run: pip install -r requirements.txt
172
174
  - name: Run benchmarks
173
- uses: CodSpeedHQ/action@v2
175
+ uses: CodSpeedHQ/action@v3
174
176
  with:
175
177
  token: ${{ secrets.CODSPEED_TOKEN }}
176
178
  run: pytest tests/ --codspeed
@@ -3,7 +3,7 @@
3
3
 
4
4
  [![CI](https://github.com/CodSpeedHQ/pytest-codspeed/actions/workflows/ci.yml/badge.svg)](https://github.com/CodSpeedHQ/pytest-codspeed/actions/workflows/ci.yml)
5
5
  [![PyPi Version](https://img.shields.io/pypi/v/pytest-codspeed?color=%2334D058&label=pypi)](https://pypi.org/project/pytest-codspeed)
6
- ![Python Version](https://img.shields.io/badge/python-3.7%20|%203.8%20|%203.9%20|%203.10%20|%203.11%20|%203.12-informational.svg)
6
+ ![Python Version](https://img.shields.io/badge/python-3.8%20|%203.9%20|%203.10%20|%203.11%20|%203.12%20|%203.13-informational.svg)
7
7
  [![Discord](https://img.shields.io/badge/chat%20on-discord-7289da.svg)](https://discord.com/invite/MxpaCfKSqF)
8
8
  [![CodSpeed Badge](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json)](https://codspeed.io/CodSpeedHQ/pytest-codspeed)
9
9
 
@@ -13,7 +13,7 @@ Pytest plugin to create CodSpeed benchmarks
13
13
 
14
14
  ## Requirements
15
15
 
16
- **Python**: 3.7 and later
16
+ **Python**: 3.8 and later
17
17
 
18
18
  **pytest**: any recent version
19
19
 
@@ -100,14 +100,14 @@ jobs:
100
100
  benchmarks:
101
101
  runs-on: ubuntu-latest
102
102
  steps:
103
- - uses: actions/checkout@v3
104
- - uses: actions/setup-python@v3
103
+ - uses: actions/checkout@v4
104
+ - uses: actions/setup-python@v5
105
105
  with:
106
- python-version: "3.9"
106
+ python-version: "3.12"
107
107
  - name: Install dependencies
108
108
  run: pip install -r requirements.txt
109
109
  - name: Run benchmarks
110
- uses: CodSpeedHQ/action@v2
110
+ uses: CodSpeedHQ/action@v3
111
111
  with:
112
112
  token: ${{ secrets.CODSPEED_TOKEN }}
113
113
  run: pytest tests/ --codspeed
@@ -13,7 +13,7 @@ dynamic = ["version"]
13
13
  description = "Pytest plugin to create CodSpeed benchmarks"
14
14
  readme = "README.md"
15
15
  license = { file = "LICENSE" }
16
- requires-python = ">=3.7"
16
+ requires-python = ">=3.8"
17
17
  authors = [{ name = "Arthur Pastel", email = "arthur@codspeed.io" }]
18
18
  keywords = ["codspeed", "benchmark", "performance", "pytest"]
19
19
  classifiers = [
@@ -23,31 +23,36 @@ classifiers = [
23
23
  "Intended Audience :: Information Technology",
24
24
  "License :: OSI Approved :: MIT License",
25
25
  "Programming Language :: Python :: 3",
26
- "Programming Language :: Python :: 3.7",
27
26
  "Programming Language :: Python :: 3.8",
28
27
  "Programming Language :: Python :: 3.9",
29
28
  "Programming Language :: Python :: 3.10",
30
29
  "Programming Language :: Python :: 3.11",
31
30
  "Programming Language :: Python :: 3.12",
31
+ "Programming Language :: Python :: 3.13",
32
32
  "Topic :: Software Development :: Testing",
33
33
  "Topic :: System :: Benchmark",
34
34
  "Topic :: Utilities",
35
35
  "Typing :: Typed",
36
36
  ]
37
37
  dependencies = [
38
- "cffi >= 1.15.1",
38
+ "cffi >= 1.17.1",
39
39
  # cffi doesn't automatically install setuptools with python 3.12+
40
40
  # cf https://github.com/python-cffi/cffi/releases/tag/v1.16.0
41
41
  "setuptools; python_full_version >= '3.12.0'",
42
42
  "pytest>=3.8",
43
43
  "filelock >= 3.12.2",
44
+ "rich>=13.8.1",
45
+ "importlib-metadata>=8.5.0; python_version < '3.10'",
44
46
  ]
45
47
 
46
48
  [project.optional-dependencies]
47
- lint = ["mypy ~= 1.3.0", "ruff ~= 0.3.3"]
48
- compat = ["pytest-benchmark ~= 4.0.0", "pytest-xdist ~= 2.0.0"]
49
+ lint = ["mypy ~= 1.11.2", "ruff ~= 0.6.5"]
50
+ compat = [
51
+ "pytest-benchmark ~= 4.0.0",
52
+ "pytest-xdist ~= 3.6.1",
53
+ # "pytest-speed>=0.3.5",
54
+ ]
49
55
  test = ["pytest ~= 7.0", "pytest-cov ~= 4.0.0"]
50
-
51
56
  [project.entry-points]
52
57
  pytest11 = { codspeed = "pytest_codspeed.plugin" }
53
58
 
@@ -59,7 +64,7 @@ features = ["lint", "test", "compat"]
59
64
  features = ["test"]
60
65
 
61
66
  [[tool.hatch.envs.test.matrix]]
62
- python = ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
67
+ python = ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
63
68
  features = ["compat", "test"]
64
69
 
65
70
  [tool.hatch.version]
@@ -72,9 +77,11 @@ include = ["/src"]
72
77
  python_version = "3.12"
73
78
 
74
79
  [tool.ruff]
75
- line-length = 88
80
+ target-version = "py37"
81
+
76
82
  [tool.ruff.lint]
77
- select = ["E", "F", "I", "C"]
83
+ select = ["E", "F", "I", "C", "TCH", "FA", "UP"]
84
+ flake8-type-checking = { exempt-modules = [], strict = true }
78
85
 
79
86
  [tool.isort]
80
87
  line_length = 88
@@ -85,8 +92,9 @@ force_grid_wrap = 0
85
92
  float_to_top = true
86
93
 
87
94
  [tool.pytest.ini_options]
88
- addopts = "--ignore=tests/benchmarks --ignore=tests/examples"
95
+ addopts = "--ignore=tests/benchmarks --ignore=tests/examples --ignore=tests/benchmarks/TheAlgorithms"
89
96
  filterwarnings = ["ignore::DeprecationWarning:pytest_benchmark.utils.*:"]
97
+ pythonpath = ["tests/benchmarks/TheAlgorithms"]
90
98
 
91
99
  [tool.coverage.run]
92
100
  branch = true
@@ -1,4 +1,4 @@
1
- __version__ = "2.2.1"
1
+ __version__ = "3.0.0b0"
2
2
 
3
3
  from .plugin import BenchmarkFixture
4
4
 
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABCMeta, abstractmethod
4
+ from enum import Enum
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from typing import Any, Callable, ClassVar, ParamSpec, TypeVar
9
+
10
+ import pytest
11
+
12
+ from pytest_codspeed.plugin import CodSpeedConfig
13
+
14
+ T = TypeVar("T")
15
+ P = ParamSpec("P")
16
+
17
+
18
+ class Instrument(metaclass=ABCMeta):
19
+ instrument: ClassVar[str]
20
+
21
+ @abstractmethod
22
+ def __init__(self, config: CodSpeedConfig): ...
23
+
24
+ @abstractmethod
25
+ def get_instrument_config_str_and_warns(self) -> tuple[str, list[str]]: ...
26
+
27
+ @abstractmethod
28
+ def measure(
29
+ self,
30
+ name: str,
31
+ uri: str,
32
+ fn: Callable[P, T],
33
+ *args: P.args,
34
+ **kwargs: P.kwargs,
35
+ ) -> T: ...
36
+
37
+ @abstractmethod
38
+ def report(self, session: pytest.Session) -> None: ...
39
+
40
+ @abstractmethod
41
+ def get_result_dict(
42
+ self,
43
+ ) -> dict[str, Any]: ...
44
+
45
+
46
+ class MeasurementMode(str, Enum):
47
+ Instrumentation = "instrumentation"
48
+ WallTime = "walltime"
49
+
50
+
51
+ def get_instrument_from_mode(mode: MeasurementMode) -> type[Instrument]:
52
+ from pytest_codspeed.instruments.valgrind import (
53
+ ValgrindInstrument,
54
+ )
55
+ from pytest_codspeed.instruments.walltime import WallTimeInstrument
56
+
57
+ if mode == MeasurementMode.Instrumentation:
58
+ return ValgrindInstrument
59
+ else:
60
+ return WallTimeInstrument
@@ -0,0 +1,95 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import sys
5
+ from typing import TYPE_CHECKING
6
+
7
+ from pytest_codspeed import __version__
8
+ from pytest_codspeed.instruments import Instrument
9
+ from pytest_codspeed.instruments.valgrind._wrapper import get_lib
10
+
11
+ if TYPE_CHECKING:
12
+ from typing import Any, Callable
13
+
14
+ from pytest import Session
15
+
16
+ from pytest_codspeed.instruments import P, T
17
+ from pytest_codspeed.instruments.valgrind._wrapper import LibType
18
+ from pytest_codspeed.plugin import CodSpeedConfig
19
+
20
+ SUPPORTS_PERF_TRAMPOLINE = sys.version_info >= (3, 12)
21
+
22
+
23
+ class ValgrindInstrument(Instrument):
24
+ instrument = "valgrind"
25
+ lib: LibType | None
26
+
27
+ def __init__(self, config: CodSpeedConfig) -> None:
28
+ self.benchmark_count = 0
29
+ self.should_measure = os.environ.get("CODSPEED_ENV") is not None
30
+ if self.should_measure:
31
+ self.lib = get_lib()
32
+ self.lib.dump_stats_at(
33
+ f"Metadata: pytest-codspeed {__version__}".encode("ascii")
34
+ )
35
+ if SUPPORTS_PERF_TRAMPOLINE:
36
+ sys.activate_stack_trampoline("perf") # type: ignore
37
+ else:
38
+ self.lib = None
39
+
40
+ def get_instrument_config_str_and_warns(self) -> tuple[str, list[str]]:
41
+ config = (
42
+ f"mode: instrumentation, "
43
+ f"callgraph: {'enabled' if SUPPORTS_PERF_TRAMPOLINE else 'not supported'}"
44
+ )
45
+ warnings = []
46
+ if not self.should_measure:
47
+ warnings.append(
48
+ "\033[1m"
49
+ "NOTICE: codspeed is enabled, but no performance measurement"
50
+ " will be made since it's running in an unknown environment."
51
+ "\033[0m"
52
+ )
53
+ return config, warnings
54
+
55
+ def measure(
56
+ self,
57
+ name: str,
58
+ uri: str,
59
+ fn: Callable[P, T],
60
+ *args: P.args,
61
+ **kwargs: P.kwargs,
62
+ ) -> T:
63
+ self.benchmark_count += 1
64
+ if self.lib is None: # Thus should_measure is False
65
+ return fn(*args, **kwargs)
66
+
67
+ def __codspeed_root_frame__() -> T:
68
+ return fn(*args, **kwargs)
69
+
70
+ if SUPPORTS_PERF_TRAMPOLINE:
71
+ # Warmup CPython performance map cache
72
+ __codspeed_root_frame__()
73
+
74
+ self.lib.zero_stats()
75
+ self.lib.start_instrumentation()
76
+ try:
77
+ return __codspeed_root_frame__()
78
+ finally:
79
+ # Ensure instrumentation is stopped even if the test failed
80
+ self.lib.stop_instrumentation()
81
+ self.lib.dump_stats_at(uri.encode("ascii"))
82
+
83
+ def report(self, session: Session) -> None:
84
+ reporter = session.config.pluginmanager.get_plugin("terminalreporter")
85
+ count_suffix = "benchmarked" if self.should_measure else "benchmark tested"
86
+ reporter.write_sep(
87
+ "=",
88
+ f"{self.benchmark_count} {count_suffix}",
89
+ )
90
+
91
+ def get_result_dict(self) -> dict[str, Any]:
92
+ return {
93
+ "instrument": {"type": self.instrument},
94
+ # bench results will be dumped by valgrind
95
+ }
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import os
2
4
  from typing import TYPE_CHECKING
3
5
 
@@ -22,7 +24,7 @@ def _get_ffi():
22
24
  return ffi
23
25
 
24
26
 
25
- def get_lib() -> "LibType":
27
+ def get_lib() -> LibType:
26
28
  try:
27
29
  ffi = _get_ffi()
28
30
  build_lock = FileLock(f"{_wrapper_dir}/build.lock")
@@ -0,0 +1,236 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict, dataclass
4
+ from math import ceil
5
+ from statistics import mean, median, stdev
6
+ from time import get_clock_info, perf_counter_ns
7
+ from typing import TYPE_CHECKING
8
+
9
+ from rich.console import Console
10
+ from rich.table import Table
11
+ from rich.text import Text
12
+
13
+ from pytest_codspeed.instruments import Instrument
14
+
15
+ if TYPE_CHECKING:
16
+ from typing import Any, Callable
17
+
18
+ from pytest import Session
19
+
20
+ from pytest_codspeed.instruments import P, T
21
+ from pytest_codspeed.plugin import CodSpeedConfig
22
+
23
+ DEFAULT_WARMUP_TIME_NS = 1_000_000_000
24
+ DEFAULT_MAX_TIME_NS = 3_000_000_000
25
+ TIMER_RESOLUTION_NS = get_clock_info("perf_counter").resolution * 1e9
26
+ DEFAULT_MIN_ROUND_TIME_NS = TIMER_RESOLUTION_NS * 100
27
+
28
+
29
+ @dataclass
30
+ class BenchmarkConfig:
31
+ warmup_time_ns: int
32
+ min_round_time_ns: float
33
+ max_time_ns: int
34
+ max_rounds: int | None
35
+
36
+ @classmethod
37
+ def from_codspeed_config(cls, config: CodSpeedConfig) -> BenchmarkConfig:
38
+ return cls(
39
+ warmup_time_ns=config.warmup_time_ns
40
+ if config.warmup_time_ns is not None
41
+ else DEFAULT_WARMUP_TIME_NS,
42
+ min_round_time_ns=DEFAULT_MIN_ROUND_TIME_NS,
43
+ max_time_ns=DEFAULT_MAX_TIME_NS,
44
+ max_rounds=config.max_rounds or None,
45
+ )
46
+
47
+
48
+ @dataclass
49
+ class BenchmarkStats:
50
+ min_ns: float
51
+ max_ns: float
52
+ stdev_ns: float
53
+ mean_ns: float
54
+ median_ns: float
55
+
56
+ rounds: int
57
+ total_time: float
58
+ outlier_rounds: int
59
+ iter_per_round: int
60
+ warmup_iters: int
61
+
62
+ @classmethod
63
+ def from_list(
64
+ cls,
65
+ times_ns: list[float],
66
+ *,
67
+ rounds: int,
68
+ iter_per_round: int,
69
+ warmup_iters: int,
70
+ total_time: float,
71
+ ) -> BenchmarkStats:
72
+ stdev_ns = stdev(times_ns) if len(times_ns) > 1 else 0
73
+ mean_ns = mean(times_ns)
74
+ return cls(
75
+ min_ns=min(times_ns),
76
+ max_ns=max(times_ns),
77
+ stdev_ns=stdev_ns,
78
+ mean_ns=mean_ns,
79
+ median_ns=median(times_ns),
80
+ rounds=rounds,
81
+ total_time=total_time,
82
+ outlier_rounds=sum(1 for t in times_ns if abs(t - mean_ns) > stdev_ns),
83
+ iter_per_round=iter_per_round,
84
+ warmup_iters=warmup_iters,
85
+ )
86
+
87
+
88
+ @dataclass
89
+ class Benchmark:
90
+ name: str
91
+ uri: str
92
+
93
+ config: BenchmarkConfig
94
+ stats: BenchmarkStats
95
+
96
+
97
+ def run_benchmark(
98
+ name: str, uri: str, fn: Callable[P, T], args, kwargs, config: BenchmarkConfig
99
+ ) -> tuple[Benchmark, T]:
100
+ # Compute the actual result of the function
101
+ out = fn(*args, **kwargs)
102
+
103
+ # Warmup
104
+ times_ns: list[float] = []
105
+ warmup_start = start = perf_counter_ns()
106
+ while True:
107
+ start = perf_counter_ns()
108
+ fn(*args, **kwargs)
109
+ end = perf_counter_ns()
110
+ times_ns.append(end - start)
111
+ if end - warmup_start > config.warmup_time_ns:
112
+ break
113
+
114
+ # Round sizing
115
+ warmup_mean_ns = mean(times_ns)
116
+ warmup_iters = len(times_ns)
117
+ times_ns.clear()
118
+ iter_per_round = (
119
+ int(ceil(config.min_round_time_ns / warmup_mean_ns))
120
+ if warmup_mean_ns <= config.min_round_time_ns
121
+ else 1
122
+ )
123
+ if config.max_rounds is None:
124
+ round_time_ns = warmup_mean_ns * iter_per_round
125
+ rounds = int(config.max_time_ns / round_time_ns)
126
+ else:
127
+ rounds = config.max_rounds
128
+
129
+ # Benchmark
130
+ iter_range = range(iter_per_round)
131
+ run_start = perf_counter_ns()
132
+ for _ in range(rounds):
133
+ start = perf_counter_ns()
134
+ for _ in iter_range:
135
+ fn(*args, **kwargs)
136
+ end = perf_counter_ns()
137
+ times_ns.append(end - start)
138
+
139
+ if end - run_start > config.max_time_ns:
140
+ # TODO: log something
141
+ break
142
+ benchmark_end = perf_counter_ns()
143
+ total_time = (benchmark_end - run_start) / 1e9
144
+
145
+ stats = BenchmarkStats.from_list(
146
+ times_ns,
147
+ rounds=rounds,
148
+ total_time=total_time,
149
+ iter_per_round=iter_per_round,
150
+ warmup_iters=warmup_iters,
151
+ )
152
+
153
+ return Benchmark(name=name, uri=uri, config=config, stats=stats), out
154
+
155
+
156
+ class WallTimeInstrument(Instrument):
157
+ instrument = "walltime"
158
+
159
+ def __init__(self, config: CodSpeedConfig) -> None:
160
+ self.config = config
161
+ self.benchmarks: list[Benchmark] = []
162
+
163
+ def get_instrument_config_str_and_warns(self) -> tuple[str, list[str]]:
164
+ return f"mode: walltime, timer_resolution: {TIMER_RESOLUTION_NS:.1f}ns", []
165
+
166
+ def measure(
167
+ self,
168
+ name: str,
169
+ uri: str,
170
+ fn: Callable[P, T],
171
+ *args: P.args,
172
+ **kwargs: P.kwargs,
173
+ ) -> T:
174
+ bench, out = run_benchmark(
175
+ name=name,
176
+ uri=uri,
177
+ fn=fn,
178
+ args=args,
179
+ kwargs=kwargs,
180
+ config=BenchmarkConfig.from_codspeed_config(self.config),
181
+ )
182
+ self.benchmarks.append(bench)
183
+ return out
184
+
185
+ def report(self, session: Session) -> None:
186
+ reporter = session.config.pluginmanager.get_plugin("terminalreporter")
187
+
188
+ if len(self.benchmarks) == 0:
189
+ reporter.write_sep(
190
+ "=",
191
+ f"{len(self.benchmarks)} benchmarked",
192
+ )
193
+ return
194
+ self._print_benchmark_table()
195
+ reporter.write_sep(
196
+ "=",
197
+ f"{len(self.benchmarks)} benchmarked",
198
+ )
199
+
200
+ def _print_benchmark_table(self) -> None:
201
+ table = Table(title="Benchmark Results")
202
+
203
+ table.add_column("Benchmark", justify="right", style="cyan", no_wrap=True)
204
+ table.add_column("Time (best)", justify="right", style="green bold")
205
+ table.add_column(
206
+ "Rel. StdDev",
207
+ justify="right",
208
+ )
209
+ table.add_column("Run time", justify="right")
210
+ table.add_column("Iters", justify="right")
211
+
212
+ for bench in self.benchmarks:
213
+ rsd = bench.stats.stdev_ns / bench.stats.mean_ns
214
+ rsd_text = Text(f"{rsd*100:.1f}%")
215
+ if rsd > 0.1:
216
+ rsd_text.stylize("red bold")
217
+ table.add_row(
218
+ bench.name,
219
+ f"{bench.stats.min_ns/bench.stats.iter_per_round:,.0f}ns",
220
+ rsd_text,
221
+ f"{bench.stats.total_time:,.2f}s",
222
+ f"{bench.stats.iter_per_round * bench.stats.rounds:,}",
223
+ )
224
+
225
+ console = Console()
226
+ print("\n")
227
+ console.print(table)
228
+
229
+ def get_result_dict(self) -> dict[str, Any]:
230
+ return {
231
+ "instrument": {
232
+ "type": self.instrument,
233
+ "clock_info": get_clock_info("perf_counter").__dict__,
234
+ },
235
+ "benchmarks": [asdict(bench) for bench in self.benchmarks],
236
+ }
@@ -0,0 +1,338 @@
1
+ from __future__ import annotations
2
+
3
+ import functools
4
+ import gc
5
+ import importlib.util
6
+ import json
7
+ import os
8
+ from dataclasses import dataclass, field
9
+ from pathlib import Path
10
+ from time import time
11
+ from typing import TYPE_CHECKING
12
+
13
+ import pytest
14
+ from _pytest.fixtures import FixtureManager
15
+
16
+ from pytest_codspeed.instruments import (
17
+ MeasurementMode,
18
+ get_instrument_from_mode,
19
+ )
20
+ from pytest_codspeed.utils import (
21
+ get_environment_metadata,
22
+ get_git_relative_uri_and_name,
23
+ )
24
+
25
+ from . import __version__
26
+
27
+ if TYPE_CHECKING:
28
+ from typing import Callable, ParamSpec, TypeVar
29
+
30
+ from pytest_codspeed.instruments import Instrument
31
+
32
+ T = TypeVar("T")
33
+ P = ParamSpec("P")
34
+
35
+ IS_PYTEST_BENCHMARK_INSTALLED = importlib.util.find_spec("pytest_benchmark") is not None
36
+ IS_PYTEST_SPEED_INSTALLED = importlib.util.find_spec("pytest_speed") is not None
37
+ BEFORE_PYTEST_8_1_1 = pytest.version_tuple < (8, 1, 1)
38
+
39
+
40
+ @pytest.hookimpl(trylast=True)
41
+ def pytest_addoption(parser: pytest.Parser):
42
+ group = parser.getgroup("CodSpeed benchmarking")
43
+ group.addoption(
44
+ "--codspeed",
45
+ action="store_true",
46
+ default=False,
47
+ help="Enable codspeed (not required when using the CodSpeed action)",
48
+ )
49
+ group.addoption(
50
+ "--codspeed-mode",
51
+ action="store",
52
+ choices=[mode.value for mode in MeasurementMode],
53
+ help="The measurement tool to use for measuring performance",
54
+ )
55
+ group.addoption(
56
+ "--codspeed-warmup-time",
57
+ action="store",
58
+ type=float,
59
+ help=(
60
+ "The time to warm up the benchmark for (in seconds), "
61
+ "only for walltime mode"
62
+ ),
63
+ )
64
+ group.addoption(
65
+ "--codspeed-max-rounds",
66
+ action="store",
67
+ type=int,
68
+ help=(
69
+ "The maximum number of rounds to run a benchmark for"
70
+ ", only for walltime mode"
71
+ ),
72
+ )
73
+
74
+
75
+ @dataclass(frozen=True)
76
+ class CodSpeedConfig:
77
+ warmup_time_ns: int | None = None
78
+ max_rounds: int | None = None
79
+
80
+ @classmethod
81
+ def from_pytest_config(cls, config: pytest.Config) -> CodSpeedConfig:
82
+ warmup_time = config.getoption("--codspeed-warmup-time", None)
83
+ warmup_time_ns = (
84
+ int(warmup_time * 1_000_000_000) if warmup_time is not None else None
85
+ )
86
+ return cls(
87
+ warmup_time_ns=warmup_time_ns,
88
+ max_rounds=config.getoption("--codspeed-max-rounds", None),
89
+ )
90
+
91
+
92
+ @dataclass(unsafe_hash=True)
93
+ class CodSpeedPlugin:
94
+ is_codspeed_enabled: bool
95
+ mode: MeasurementMode
96
+ instrument: Instrument
97
+ config: CodSpeedConfig
98
+ disabled_plugins: tuple[str, ...]
99
+ profile_folder: Path | None
100
+ benchmark_count: int = field(default=0, hash=False, compare=False)
101
+
102
+
103
+ PLUGIN_NAME = "codspeed_plugin"
104
+
105
+
106
+ def get_plugin(config: pytest.Config) -> CodSpeedPlugin:
107
+ return config.pluginmanager.get_plugin(PLUGIN_NAME)
108
+
109
+
110
+ @pytest.hookimpl(tryfirst=True)
111
+ def pytest_configure(config: pytest.Config):
112
+ config.addinivalue_line(
113
+ "markers", "codspeed_benchmark: mark an entire test for codspeed benchmarking"
114
+ )
115
+ config.addinivalue_line(
116
+ "markers", "benchmark: mark an entire test for codspeed benchmarking"
117
+ )
118
+ is_codspeed_enabled = (
119
+ config.getoption("--codspeed") or os.environ.get("CODSPEED_ENV") is not None
120
+ )
121
+
122
+ if os.environ.get("CODSPEED_ENV") is not None:
123
+ if os.environ.get("CODSPEED_RUNNER_MODE") == "walltime":
124
+ default_mode = MeasurementMode.WallTime.value
125
+ else:
126
+ default_mode = MeasurementMode.Instrumentation.value
127
+ else:
128
+ default_mode = MeasurementMode.WallTime.value
129
+
130
+ mode = MeasurementMode(config.getoption("--codspeed-mode", None) or default_mode)
131
+ instrument = get_instrument_from_mode(mode)
132
+ disabled_plugins: list[str] = []
133
+ if is_codspeed_enabled:
134
+ if IS_PYTEST_BENCHMARK_INSTALLED:
135
+ # Disable pytest-benchmark
136
+ object.__setattr__(config.option, "benchmark_disable", True)
137
+ config.pluginmanager.set_blocked("pytest_benchmark")
138
+ config.pluginmanager.set_blocked("pytest-benchmark")
139
+ disabled_plugins.append("pytest-benchmark")
140
+ if IS_PYTEST_SPEED_INSTALLED:
141
+ # Disable pytest-speed
142
+ config.pluginmanager.set_blocked("speed")
143
+ disabled_plugins.append("pytest-speed")
144
+
145
+ profile_folder = os.environ.get("CODSPEED_PROFILE_FOLDER")
146
+
147
+ codspeedconfig = CodSpeedConfig.from_pytest_config(config)
148
+
149
+ plugin = CodSpeedPlugin(
150
+ disabled_plugins=tuple(disabled_plugins),
151
+ is_codspeed_enabled=is_codspeed_enabled,
152
+ mode=mode,
153
+ instrument=instrument(codspeedconfig),
154
+ config=codspeedconfig,
155
+ profile_folder=Path(profile_folder) if profile_folder else None,
156
+ )
157
+ config.pluginmanager.register(plugin, PLUGIN_NAME)
158
+
159
+
160
+ @pytest.hookimpl()
161
+ def pytest_plugin_registered(plugin, manager: pytest.PytestPluginManager):
162
+ """
163
+ Patch the benchmark fixture to use the codspeed one if codspeed is enabled and an
164
+ alternative benchmark fixture is available
165
+ """
166
+ if (IS_PYTEST_BENCHMARK_INSTALLED or IS_PYTEST_SPEED_INSTALLED) and isinstance(
167
+ plugin, FixtureManager
168
+ ):
169
+ fixture_manager = plugin
170
+ codspeed_plugin: CodSpeedPlugin = manager.get_plugin(PLUGIN_NAME)
171
+ if codspeed_plugin.is_codspeed_enabled:
172
+ codspeed_benchmark_fixtures = plugin.getfixturedefs(
173
+ "codspeed_benchmark",
174
+ fixture_manager.session.nodeid
175
+ if BEFORE_PYTEST_8_1_1
176
+ else fixture_manager.session,
177
+ )
178
+ assert codspeed_benchmark_fixtures is not None
179
+ # Archive the alternative benchmark fixture
180
+ fixture_manager._arg2fixturedefs["__benchmark"] = (
181
+ fixture_manager._arg2fixturedefs["benchmark"]
182
+ )
183
+ # Replace the alternative fixture with the codspeed one
184
+ fixture_manager._arg2fixturedefs["benchmark"] = codspeed_benchmark_fixtures
185
+
186
+
187
+ @pytest.hookimpl(trylast=True)
188
+ def pytest_report_header(config: pytest.Config):
189
+ plugin = get_plugin(config)
190
+ config_str, warns = plugin.instrument.get_instrument_config_str_and_warns()
191
+ out = [
192
+ (
193
+ f"codspeed: {__version__} ("
194
+ f"{'enabled' if plugin.is_codspeed_enabled else 'disabled'}, {config_str}"
195
+ ")"
196
+ ),
197
+ *warns,
198
+ ]
199
+ if len(plugin.disabled_plugins) > 0:
200
+ out.append(
201
+ "\033[93mCodSpeed had to disable the following plugins: "
202
+ f"{', '.join(plugin.disabled_plugins)}\033[0m"
203
+ )
204
+ return "\n".join(out)
205
+
206
+
207
+ def has_benchmark_fixture(item: pytest.Item) -> bool:
208
+ item_fixtures = getattr(item, "fixturenames", [])
209
+ return "benchmark" in item_fixtures or "codspeed_benchmark" in item_fixtures
210
+
211
+
212
+ def has_benchmark_marker(item: pytest.Item) -> bool:
213
+ return (
214
+ item.get_closest_marker("codspeed_benchmark") is not None
215
+ or item.get_closest_marker("benchmark") is not None
216
+ )
217
+
218
+
219
+ def should_benchmark_item(item: pytest.Item) -> bool:
220
+ return has_benchmark_fixture(item) or has_benchmark_marker(item)
221
+
222
+
223
+ @pytest.hookimpl(trylast=True)
224
+ def pytest_collection_modifyitems(
225
+ session: pytest.Session, config: pytest.Config, items: list[pytest.Item]
226
+ ):
227
+ """Filter out items that should not be benchmarked when codspeed is enabled"""
228
+ plugin = get_plugin(config)
229
+ if plugin.is_codspeed_enabled:
230
+ deselected = []
231
+ selected = []
232
+ for item in items:
233
+ if should_benchmark_item(item):
234
+ selected.append(item)
235
+ else:
236
+ deselected.append(item)
237
+ config.hook.pytest_deselected(items=deselected)
238
+ items[:] = selected
239
+
240
+
241
+ def _measure(
242
+ plugin: CodSpeedPlugin,
243
+ nodeid: str,
244
+ config: pytest.Config,
245
+ fn: Callable[P, T],
246
+ *args: P.args,
247
+ **kwargs: P.kwargs,
248
+ ) -> T:
249
+ is_gc_enabled = gc.isenabled()
250
+ if is_gc_enabled:
251
+ gc.collect()
252
+ gc.disable()
253
+ try:
254
+ uri, name = get_git_relative_uri_and_name(nodeid, config.rootpath)
255
+ return plugin.instrument.measure(name, uri, fn, *args, **kwargs)
256
+ finally:
257
+ # Ensure GC is re-enabled even if the test failed
258
+ if is_gc_enabled:
259
+ gc.enable()
260
+
261
+
262
+ def wrap_runtest(
263
+ plugin: CodSpeedPlugin,
264
+ nodeid: str,
265
+ config: pytest.Config,
266
+ fn: Callable[P, T],
267
+ ) -> Callable[P, T]:
268
+ @functools.wraps(fn)
269
+ def wrapped(*args: P.args, **kwargs: P.kwargs) -> T:
270
+ return _measure(plugin, nodeid, config, fn, *args, **kwargs)
271
+
272
+ return wrapped
273
+
274
+
275
+ @pytest.hookimpl(tryfirst=True)
276
+ def pytest_runtest_protocol(item: pytest.Item, nextitem: pytest.Item | None):
277
+ plugin = get_plugin(item.config)
278
+ if not plugin.is_codspeed_enabled or not should_benchmark_item(item):
279
+ # Defer to the default test protocol since no benchmarking is needed
280
+ return None
281
+
282
+ if has_benchmark_fixture(item):
283
+ # Instrumentation is handled by the fixture
284
+ return None
285
+
286
+ # Wrap runtest and defer to default protocol
287
+ item.runtest = wrap_runtest(plugin, item.nodeid, item.config, item.runtest)
288
+ return None
289
+
290
+
291
+ @pytest.hookimpl()
292
+ def pytest_sessionfinish(session: pytest.Session, exitstatus):
293
+ plugin = get_plugin(session.config)
294
+ if plugin.is_codspeed_enabled:
295
+ plugin.instrument.report(session)
296
+ if plugin.profile_folder:
297
+ result_path = plugin.profile_folder / "results" / f"{os.getpid()}.json"
298
+ else:
299
+ result_path = (
300
+ session.config.rootpath / f".codspeed/results_{time() * 1000:.0f}.json"
301
+ )
302
+ data = {**get_environment_metadata(), **plugin.instrument.get_result_dict()}
303
+ result_path.parent.mkdir(parents=True, exist_ok=True)
304
+ result_path.write_text(json.dumps(data, indent=2))
305
+
306
+
307
+ class BenchmarkFixture:
308
+ """The fixture that can be used to benchmark a function."""
309
+
310
+ def __init__(self, request: pytest.FixtureRequest):
311
+ self.extra_info: dict = {}
312
+
313
+ self._request = request
314
+
315
+ def __call__(self, func: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T:
316
+ config = self._request.config
317
+ plugin = get_plugin(config)
318
+ if plugin.is_codspeed_enabled:
319
+ return _measure(
320
+ plugin, self._request.node.nodeid, config, func, *args, **kwargs
321
+ )
322
+ else:
323
+ return func(*args, **kwargs)
324
+
325
+
326
+ @pytest.fixture(scope="function")
327
+ def codspeed_benchmark(request: pytest.FixtureRequest) -> Callable:
328
+ return BenchmarkFixture(request)
329
+
330
+
331
+ if not IS_PYTEST_BENCHMARK_INSTALLED:
332
+
333
+ @pytest.fixture(scope="function")
334
+ def benchmark(codspeed_benchmark, request: pytest.FixtureRequest):
335
+ """
336
+ Compatibility with pytest-benchmark
337
+ """
338
+ return codspeed_benchmark
@@ -1,5 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import sys
5
+ import sysconfig
1
6
  from pathlib import Path
2
7
 
8
+ from pytest_codspeed import __version__
9
+
10
+ if sys.version_info < (3, 10):
11
+ import importlib_metadata as importlib_metadata
12
+ else:
13
+ import importlib.metadata as importlib_metadata
14
+
3
15
 
4
16
  def get_git_relative_path(abs_path: Path) -> Path:
5
17
  """Get the path relative to the git root directory. If the path is not
@@ -15,11 +27,11 @@ def get_git_relative_path(abs_path: Path) -> Path:
15
27
  return abs_path
16
28
 
17
29
 
18
- def get_git_relative_uri(uri: str, pytest_rootdir: Path) -> str:
19
- """Get the benchmark uri relative to the git root dir.
30
+ def get_git_relative_uri_and_name(nodeid: str, pytest_rootdir: Path) -> tuple[str, str]:
31
+ """Get the benchmark uri relative to the git root dir and the benchmark name.
20
32
 
21
33
  Args:
22
- uri (str): the benchmark uri, for example:
34
+ nodeid (str): the pytest nodeid, for example:
23
35
  testing/test_excinfo.py::TestFormattedExcinfo::test_repr_source
24
36
  pytest_rootdir (str): the pytest root dir, for example:
25
37
  /home/user/gitrepo/folder
@@ -29,7 +41,23 @@ def get_git_relative_uri(uri: str, pytest_rootdir: Path) -> str:
29
41
  folder/testing/test_excinfo.py::TestFormattedExcinfo::test_repr_source
30
42
 
31
43
  """
32
- file_path, function_path = uri.split("::", 1)
44
+ file_path, bench_name = nodeid.split("::", 1)
33
45
  absolute_file_path = pytest_rootdir / Path(file_path)
34
46
  relative_git_path = get_git_relative_path(absolute_file_path)
35
- return f"{str(relative_git_path)}::{function_path}"
47
+ return (f"{str(relative_git_path)}::{bench_name}", bench_name)
48
+
49
+
50
+ def get_environment_metadata() -> dict[str, dict]:
51
+ return {
52
+ "creator": {
53
+ "name": "pytest-codspeed",
54
+ "version": __version__,
55
+ "pid": os.getpid(),
56
+ },
57
+ "python": {
58
+ "sysconfig": sysconfig.get_config_vars(),
59
+ "dependencies": {
60
+ d.name: d.version for d in importlib_metadata.distributions()
61
+ },
62
+ },
63
+ }
@@ -1,309 +0,0 @@
1
- import gc
2
- import os
3
- import pkgutil
4
- import sys
5
- from dataclasses import dataclass, field
6
- from typing import (
7
- TYPE_CHECKING,
8
- Any,
9
- Callable,
10
- Dict,
11
- List,
12
- Optional,
13
- Tuple,
14
- TypeVar,
15
- Union,
16
- )
17
-
18
- import pytest
19
- from _pytest.fixtures import FixtureManager
20
-
21
- from pytest_codspeed.utils import get_git_relative_uri
22
-
23
- from . import __version__
24
- from ._wrapper import get_lib
25
-
26
- if TYPE_CHECKING:
27
- from ._wrapper import LibType
28
-
29
- IS_PYTEST_BENCHMARK_INSTALLED = pkgutil.find_loader("pytest_benchmark") is not None
30
- SUPPORTS_PERF_TRAMPOLINE = sys.version_info >= (3, 12)
31
- BEFORE_PYTEST_8_1_1 = pytest.version_tuple < (8, 1, 1)
32
-
33
-
34
- @pytest.hookimpl(trylast=True)
35
- def pytest_addoption(parser: "pytest.Parser"):
36
- group = parser.getgroup("CodSpeed benchmarking")
37
- group.addoption(
38
- "--codspeed",
39
- action="store_true",
40
- default=False,
41
- help="Enable codspeed (not required when using the CodSpeed action)",
42
- )
43
-
44
-
45
- @dataclass(unsafe_hash=True)
46
- class CodSpeedPlugin:
47
- is_codspeed_enabled: bool
48
- should_measure: bool
49
- lib: Optional["LibType"]
50
- disabled_plugins: Tuple[str, ...]
51
- benchmark_count: int = field(default=0, hash=False, compare=False)
52
-
53
-
54
- PLUGIN_NAME = "codspeed_plugin"
55
-
56
-
57
- def get_plugin(config: "pytest.Config") -> "CodSpeedPlugin":
58
- return config.pluginmanager.get_plugin(PLUGIN_NAME)
59
-
60
-
61
- @pytest.hookimpl(tryfirst=True)
62
- def pytest_configure(config: "pytest.Config"):
63
- config.addinivalue_line(
64
- "markers", "codspeed_benchmark: mark an entire test for codspeed benchmarking"
65
- )
66
- config.addinivalue_line(
67
- "markers", "benchmark: mark an entire test for codspeed benchmarking"
68
- )
69
- is_codspeed_enabled = (
70
- config.getoption("--codspeed") or os.environ.get("CODSPEED_ENV") is not None
71
- )
72
- should_measure = os.environ.get("CODSPEED_ENV") is not None
73
- lib = get_lib() if should_measure else None
74
- if lib is not None:
75
- lib.dump_stats_at(f"Metadata: pytest-codspeed {__version__}".encode("ascii"))
76
- disabled_plugins: List[str] = []
77
- # Disable pytest-benchmark if codspeed is enabled
78
- if is_codspeed_enabled and IS_PYTEST_BENCHMARK_INSTALLED:
79
- object.__setattr__(config.option, "benchmark_disable", True)
80
- config.pluginmanager.set_blocked("pytest-benchmark")
81
- disabled_plugins.append("pytest-benchmark")
82
-
83
- plugin = CodSpeedPlugin(
84
- is_codspeed_enabled=is_codspeed_enabled,
85
- should_measure=should_measure,
86
- lib=lib,
87
- disabled_plugins=tuple(disabled_plugins),
88
- )
89
- config.pluginmanager.register(plugin, PLUGIN_NAME)
90
-
91
-
92
- def pytest_plugin_registered(plugin, manager: "pytest.PytestPluginManager"):
93
- """Patch the benchmark fixture to use the codspeed one if codspeed is enabled"""
94
- if IS_PYTEST_BENCHMARK_INSTALLED and isinstance(plugin, FixtureManager):
95
- fixture_manager = plugin
96
- codspeed_plugin: CodSpeedPlugin = manager.get_plugin(PLUGIN_NAME)
97
- if codspeed_plugin.is_codspeed_enabled:
98
- codspeed_benchmark_fixtures = plugin.getfixturedefs(
99
- "codspeed_benchmark",
100
- fixture_manager.session.nodeid
101
- if BEFORE_PYTEST_8_1_1
102
- else fixture_manager.session,
103
- )
104
- assert codspeed_benchmark_fixtures is not None
105
- # Archive the pytest-benchmark fixture
106
- fixture_manager._arg2fixturedefs["__benchmark"] = (
107
- fixture_manager._arg2fixturedefs["benchmark"]
108
- )
109
- # Replace the pytest-benchmark fixture with the codspeed one
110
- fixture_manager._arg2fixturedefs["benchmark"] = codspeed_benchmark_fixtures
111
-
112
-
113
- @pytest.hookimpl(trylast=True)
114
- def pytest_report_header(config: "pytest.Config"):
115
- out = [
116
- f"codspeed: {__version__} "
117
- f"(callgraph: {'enabled' if SUPPORTS_PERF_TRAMPOLINE else 'not supported'})"
118
- ]
119
- plugin = get_plugin(config)
120
- if plugin.is_codspeed_enabled and not plugin.should_measure:
121
- out.append(
122
- "\033[1m"
123
- "NOTICE: codspeed is enabled, but no performance measurement"
124
- " will be made since it's running in an unknown environment."
125
- "\033[0m"
126
- )
127
- if len(plugin.disabled_plugins) > 0:
128
- out.append(
129
- "\033[93mCodSpeed had to disable the following plugins: "
130
- f"{', '.join(plugin.disabled_plugins)}\033[0m"
131
- )
132
- return "\n".join(out)
133
-
134
-
135
- def has_benchmark_fixture(item: "pytest.Item") -> bool:
136
- item_fixtures = getattr(item, "fixturenames", [])
137
- return "benchmark" in item_fixtures or "codspeed_benchmark" in item_fixtures
138
-
139
-
140
- def has_benchmark_marker(item: "pytest.Item") -> bool:
141
- return (
142
- item.get_closest_marker("codspeed_benchmark") is not None
143
- or item.get_closest_marker("benchmark") is not None
144
- )
145
-
146
-
147
- def should_benchmark_item(item: "pytest.Item") -> bool:
148
- return has_benchmark_fixture(item) or has_benchmark_marker(item)
149
-
150
-
151
- @pytest.hookimpl()
152
- def pytest_sessionstart(session: "pytest.Session"):
153
- plugin = get_plugin(session.config)
154
- if plugin.is_codspeed_enabled:
155
- plugin.benchmark_count = 0
156
- if plugin.should_measure and SUPPORTS_PERF_TRAMPOLINE:
157
- sys.activate_stack_trampoline("perf") # type: ignore
158
-
159
-
160
- @pytest.hookimpl(trylast=True)
161
- def pytest_collection_modifyitems(
162
- session: "pytest.Session", config: "pytest.Config", items: "List[pytest.Item]"
163
- ):
164
- plugin = get_plugin(config)
165
- if plugin.is_codspeed_enabled:
166
- deselected = []
167
- selected = []
168
- for item in items:
169
- if should_benchmark_item(item):
170
- selected.append(item)
171
- else:
172
- deselected.append(item)
173
- config.hook.pytest_deselected(items=deselected)
174
- items[:] = selected
175
-
176
-
177
- def _run_with_instrumentation(
178
- lib: "LibType",
179
- nodeId: str,
180
- config: "pytest.Config",
181
- fn: Callable[..., Any],
182
- *args,
183
- **kwargs,
184
- ):
185
- is_gc_enabled = gc.isenabled()
186
- if is_gc_enabled:
187
- gc.collect()
188
- gc.disable()
189
-
190
- result = None
191
-
192
- def __codspeed_root_frame__():
193
- nonlocal result
194
- result = fn(*args, **kwargs)
195
-
196
- if SUPPORTS_PERF_TRAMPOLINE:
197
- # Warmup CPython performance map cache
198
- __codspeed_root_frame__()
199
- lib.zero_stats()
200
- lib.start_instrumentation()
201
- __codspeed_root_frame__()
202
- lib.stop_instrumentation()
203
- uri = get_git_relative_uri(nodeId, config.rootpath)
204
- lib.dump_stats_at(uri.encode("ascii"))
205
- if is_gc_enabled:
206
- gc.enable()
207
-
208
- return result
209
-
210
-
211
- @pytest.hookimpl(tryfirst=True)
212
- def pytest_runtest_protocol(item: "pytest.Item", nextitem: Union["pytest.Item", None]):
213
- plugin = get_plugin(item.config)
214
- if not plugin.is_codspeed_enabled or not should_benchmark_item(item):
215
- return (
216
- None # Defer to the default test protocol since no benchmarking is needed
217
- )
218
-
219
- if has_benchmark_fixture(item):
220
- return None # Instrumentation is handled by the fixture
221
-
222
- plugin.benchmark_count += 1
223
- if not plugin.should_measure:
224
- return None # Benchmark counted but will be run in the default protocol
225
-
226
- # Setup phase
227
- reports = []
228
- ihook = item.ihook
229
- ihook.pytest_runtest_logstart(nodeid=item.nodeid, location=item.location)
230
- setup_call = pytest.CallInfo.from_call(
231
- lambda: ihook.pytest_runtest_setup(item=item, nextitem=nextitem), "setup"
232
- )
233
- setup_report = ihook.pytest_runtest_makereport(item=item, call=setup_call)
234
- ihook.pytest_runtest_logreport(report=setup_report)
235
- reports.append(setup_report)
236
- # Run phase
237
- if setup_report.passed and not item.config.getoption("setuponly"):
238
- assert plugin.lib is not None
239
- runtest_call = pytest.CallInfo.from_call(
240
- lambda: _run_with_instrumentation(
241
- plugin.lib, item.nodeid, item.config, item.runtest
242
- ),
243
- "call",
244
- )
245
- runtest_report = ihook.pytest_runtest_makereport(item=item, call=runtest_call)
246
- ihook.pytest_runtest_logreport(report=runtest_report)
247
- reports.append(runtest_report)
248
-
249
- # Teardown phase
250
- teardown_call = pytest.CallInfo.from_call(
251
- lambda: ihook.pytest_runtest_teardown(item=item, nextitem=nextitem), "teardown"
252
- )
253
- teardown_report = ihook.pytest_runtest_makereport(item=item, call=teardown_call)
254
- ihook.pytest_runtest_logreport(report=teardown_report)
255
- reports.append(teardown_report)
256
- ihook.pytest_runtest_logfinish(nodeid=item.nodeid, location=item.location)
257
-
258
- return reports # Deny further protocol hooks execution
259
-
260
-
261
- T = TypeVar("T")
262
-
263
-
264
- class BenchmarkFixture:
265
- """The fixture that can be used to benchmark a function."""
266
-
267
- def __init__(self, request: "pytest.FixtureRequest"):
268
- self.extra_info: Dict = {}
269
-
270
- self._request = request
271
-
272
- def __call__(self, func: Callable[..., T], *args: Any, **kwargs: Any) -> T:
273
- config = self._request.config
274
- plugin = get_plugin(config)
275
- plugin.benchmark_count += 1
276
- if plugin.is_codspeed_enabled and plugin.should_measure:
277
- assert plugin.lib is not None
278
- return _run_with_instrumentation(
279
- plugin.lib, self._request.node.nodeid, config, func, *args, **kwargs
280
- )
281
- else:
282
- return func(*args, **kwargs)
283
-
284
-
285
- @pytest.fixture(scope="function")
286
- def codspeed_benchmark(request: "pytest.FixtureRequest") -> Callable:
287
- return BenchmarkFixture(request)
288
-
289
-
290
- if not IS_PYTEST_BENCHMARK_INSTALLED:
291
-
292
- @pytest.fixture(scope="function")
293
- def benchmark(codspeed_benchmark, request: "pytest.FixtureRequest"):
294
- """
295
- Compatibility with pytest-benchmark
296
- """
297
- return codspeed_benchmark
298
-
299
-
300
- @pytest.hookimpl()
301
- def pytest_sessionfinish(session: "pytest.Session", exitstatus):
302
- plugin = get_plugin(session.config)
303
- if plugin.is_codspeed_enabled:
304
- reporter = session.config.pluginmanager.get_plugin("terminalreporter")
305
- count_suffix = "benchmarked" if plugin.should_measure else "benchmark tested"
306
- reporter.write_sep(
307
- "=",
308
- f"{plugin.benchmark_count} {count_suffix}",
309
- )