Skip to content

Commit 6a17437

Browse files
committedAug 17, 2023
refactor(CFMTech#46): Refactor how markers are computed.
An item setup from markers is now computed by the `PyTestMonitorMarkerProcessor` which build a `PyTestMonitorItemConfig` in turned stashed at the item level.
1 parent bf383d4 commit 6a17437

File tree

4 files changed

+174
-50
lines changed

4 files changed

+174
-50
lines changed
 

‎pytest_monitor/markers.py

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import warnings
2+
from typing import Any, Dict, List
3+
4+
import pytest
5+
6+
from pytest_monitor.models import PyTestMonitorItemConfig, PyTestMonitorMarker
7+
8+
9+
class SkipTestMarker(PyTestMonitorMarker[bool]):
10+
def __init__(self) -> None:
11+
super().__init__(
12+
name="monitor_skip_test",
13+
require_item=False,
14+
attribute="skip",
15+
doc="Mark the decorated test to be executed but not monitored",
16+
default_value=False,
17+
deprecated=False,
18+
)
19+
20+
def compute_config_from_item(self, _: pytest.Item, __: pytest.Mark) -> bool:
21+
return True
22+
23+
def post_process(self, configuration: Dict[str, bool]) -> Dict[str, bool]:
24+
return configuration
25+
26+
27+
class SkipTestIfMarker(PyTestMonitorMarker[bool]):
28+
def __init__(self) -> None:
29+
super().__init__(
30+
name="monitor_skip_test_if",
31+
require_item=False,
32+
attribute="skip",
33+
doc="Mark the decorated test to be executed but not monitored",
34+
default_value=False,
35+
deprecated=False,
36+
)
37+
38+
def compute_config_from_item(self, _: pytest.Item, marker: pytest.Mark) -> bool:
39+
return bool(marker.args[0])
40+
41+
def post_process(self, configuration: Dict[str, bool]) -> Dict[str, bool]:
42+
return configuration
43+
44+
45+
class MonitorMarker(PyTestMonitorMarker[bool]):
46+
def __init__(self) -> None:
47+
super().__init__(
48+
name="monitor_test",
49+
require_item=False,
50+
attribute="force",
51+
doc="Mark the decorated test to be monitored (default behaviour)."
52+
" This can turn handy to whitelist some test when you have disabled"
53+
" monitoring on a whole module",
54+
default_value=False,
55+
deprecated=False,
56+
)
57+
58+
def compute_config_from_item(self, _: pytest.Item, __: pytest.Mark) -> bool:
59+
return True
60+
61+
def post_process(self, configuration: Dict[str, bool]) -> Dict[str, bool]:
62+
if configuration[self.attribute]:
63+
configuration["monitor_skip_test"] = True
64+
return configuration
65+
66+
67+
class MonitorIfMarker(PyTestMonitorMarker[bool]):
68+
def __init__(self) -> None:
69+
super().__init__(
70+
name="monitor_test_if",
71+
require_item=False,
72+
attribute="force",
73+
doc="Mark the decorated test to be monitored if and only if condition"
74+
" is verified. This can help you in whitelisting tests to be monitored"
75+
" depending on some external conditions.",
76+
default_value=False,
77+
deprecated=False,
78+
)
79+
80+
def compute_config_from_item(self, _: pytest.Item, marker: pytest.Mark) -> bool:
81+
return bool(marker.args[0])
82+
83+
def post_process(self, configuration: Dict[str, bool]) -> Dict[str, bool]:
84+
return configuration
85+
86+
87+
class PyTestMonitorMarkerProcessor:
88+
def __init__(self):
89+
self._markers: List[PyTestMonitorMarker] = [
90+
SkipTestMarker(),
91+
SkipTestIfMarker(),
92+
MonitorMarker(),
93+
MonitorIfMarker(),
94+
]
95+
96+
def _defaults(self) -> Dict[str, Any]:
97+
return {marker.attribute: marker.default_value for marker in self._markers}
98+
99+
def __call__(self, item: pytest.Item) -> PyTestMonitorItemConfig:
100+
item_monitor_markers: Dict[str, pytest.Mark] = {
101+
marker.name: marker for marker in item.iter_markers() if marker and marker.name.startswith("monitor_")
102+
}
103+
item_unknown_markers = {marker_name for marker_name in item_monitor_markers if marker_name not in self._markers}
104+
for marker_name in item_unknown_markers:
105+
warnings.warn(f"Nothing known about marker {marker_name}. Marker will not be considered.")
106+
config: Dict[str, bool] = self._defaults()
107+
for marker in self._markers:
108+
if marker.name in item_monitor_markers:
109+
if marker.deprecated:
110+
warnings.warn(f"Marker {marker.name} is deprecated. Consider upgrading your tests")
111+
config[marker.attribute] = marker.compute_config_from_item(item, item_monitor_markers[marker.name])
112+
113+
return PyTestMonitorItemConfig(**config)

‎pytest_monitor/models.py

+48
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,54 @@
1+
import abc
12
from dataclasses import dataclass, field
3+
from typing import Dict, Generic, TypeVar
4+
5+
import pytest
6+
7+
T = TypeVar("T")
28

39

410
@dataclass(frozen=True)
511
class PyTestMonitorConfig:
612
enabled: bool = field(default=True, init=True)
13+
14+
15+
@dataclass(frozen=True)
16+
class PyTestMonitorItemConfig:
17+
skip: bool = field(default=True, init=True)
18+
force: bool = field(default=True, init=True)
19+
20+
21+
class PyTestMonitorMarker(abc.ABC, Generic[T]):
22+
def __init__(
23+
self, name: str, require_item: bool, attribute: str, doc: str, default_value: T, deprecated: bool
24+
) -> None:
25+
self._name = name
26+
self._require_item = require_item
27+
self._attribute = attribute
28+
self._doc = doc
29+
self._deprecated = deprecated
30+
self._default_value = default_value
31+
32+
@abc.abstractmethod
33+
def compute_config_from_item(self, item: pytest.Item, marker: pytest.Mark) -> T:
34+
"""Compute the marker value to associate to the given item"""
35+
36+
@abc.abstractmethod
37+
def post_process(self, configuration: Dict[str, bool]) -> Dict[str, bool]:
38+
"""Allow to override some data after the marker has been evaluated"""
39+
40+
@property
41+
def name(self) -> str:
42+
return self._name
43+
44+
@property
45+
def deprecated(self) -> bool:
46+
return self._deprecated
47+
48+
@property
49+
def default_value(self) -> T:
50+
return self._default_value
51+
52+
@property
53+
def attribute(self) -> str:
54+
return self._attribute

‎pytest_monitor/pytest_monitor.py

+8-47
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,14 @@
66
import memory_profiler
77
import pytest
88

9-
from pytest_monitor.models import PyTestMonitorConfig
9+
from pytest_monitor.markers import PyTestMonitorMarkerProcessor
10+
from pytest_monitor.models import PyTestMonitorConfig, PyTestMonitorItemConfig
1011
from pytest_monitor.session import PyTestMonitorSession
1112

12-
# These dictionaries are used to compute members set on each items.
13-
# KEY is the marker set on a test function
14-
# value is a tuple:
15-
# expect_args: boolean
16-
# internal marker attribute name: str
17-
# callable that set member's value
18-
# default value
19-
PYTEST_MONITOR_VALID_MARKERS = {
20-
"monitor_skip_test": (False, "monitor_skip_test", lambda x: True, False),
21-
"monitor_skip_test_if": (True, "monitor_skip_test", lambda x: bool(x), False),
22-
"monitor_test": (False, "monitor_force_test", lambda x: True, False),
23-
"monitor_test_if": (True, "monitor_force_test", lambda x: bool(x), False),
24-
}
25-
PYTEST_MONITOR_DEPRECATED_MARKERS = {}
26-
2713
config_stash = pytest.StashKey[PyTestMonitorConfig]()
14+
item_config_stash = pytest.StashKey[PyTestMonitorItemConfig]()
15+
16+
marker_processor = PyTestMonitorMarkerProcessor()
2817

2918

3019
def pytest_addoption(parser):
@@ -102,7 +91,7 @@ def pytest_configure(config):
10291
config.addinivalue_line("markers", "monitor_skip_test: mark test to be executed but not monitored.")
10392
config.addinivalue_line(
10493
"markers",
105-
"monitor_skip_test_if(cond): mark test to be executed but " "not monitored if cond is verified.",
94+
"monitor_skip_test_if(cond): mark test to be executed but not monitored if cond is verified.",
10695
)
10796
config.addinivalue_line(
10897
"markers",
@@ -126,36 +115,8 @@ def pytest_runtest_setup(item: pytest.Item):
126115
"""
127116
if not item.session.stash[config_stash].enabled:
128117
return
129-
item_markers = {mark.name: mark for mark in item.iter_markers() if mark and mark.name.startswith("monitor_")}
130-
mark_to_del = []
131-
for set_marker in item_markers.keys():
132-
if set_marker not in PYTEST_MONITOR_VALID_MARKERS:
133-
warnings.warn("Nothing known about marker {}. Marker will be dropped.".format(set_marker))
134-
mark_to_del.append(set_marker)
135-
if set_marker in PYTEST_MONITOR_DEPRECATED_MARKERS:
136-
warnings.warn(f"Marker {set_marker} is deprecated. Consider upgrading your tests")
137-
138-
for marker in mark_to_del:
139-
del item_markers[marker]
140-
141-
all_valid_markers = PYTEST_MONITOR_VALID_MARKERS
142-
all_valid_markers.update(PYTEST_MONITOR_DEPRECATED_MARKERS)
143-
# Setting instantiated markers
144-
for marker, _ in item_markers.items():
145-
with_args, attr, fun_val, _ = all_valid_markers[marker]
146-
attr_val = fun_val(item_markers[marker].args[0]) if with_args else fun_val(None)
147-
setattr(item, attr, attr_val)
148-
149-
# Setting other markers to default values
150-
for marker, marker_value in all_valid_markers.items():
151-
with_args, attr, _, default = marker_value
152-
if not hasattr(item, attr):
153-
setattr(item, attr, default)
154118

155-
# Finalize marker processing by enforcing some marker's value
156-
if item.monitor_force_test:
157-
# This test has been explicitly flagged as 'to be monitored'.
158-
item.monitor_skip_test = False
119+
item.stash[item_config_stash] = marker_processor(item)
159120

160121

161122
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
@@ -294,7 +255,7 @@ def _prf_tracer(request: pytest.FixtureRequest):
294255
ptimes_a = request.session.pytest_monitor.process.cpu_times()
295256
yield
296257
ptimes_b = request.session.pytest_monitor.process.cpu_times()
297-
if not request.node.monitor_skip_test and getattr(request.node, "monitor_results", False):
258+
if not request.node.stash[item_config_stash].skip and getattr(request.node, "monitor_results", False):
298259
item_name = request.node.originalname
299260
item_loc, *_ = request.node.location
300261
request.session.pytest_monitor.add_test_info(

‎tests/test_monitor.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ def test_ok():
181181
pymon_path = pathlib.Path(str(testdir)) / ".pymon"
182182
assert pymon_path.exists()
183183

184-
# make sure that that we get a '0' exit code for the testsuite
184+
# make sure that we get a '0' exit code for the testsuite
185185
result.assert_outcomes(passed=1)
186186

187187
db = sqlite3.connect(str(pymon_path))
@@ -378,6 +378,8 @@ def test_monitor_with_doctest(testdir):
378378
# create a temporary pytest test module
379379
testdir.makepyfile(
380380
'''
381+
pytest_monitor_component = "doctest"
382+
381383
def run(a, b):
382384
"""
383385
>>> run(3, 30)
@@ -390,7 +392,7 @@ def run(a, b):
390392
# run pytest with the following cmd args
391393
result = testdir.runpytest("--doctest-modules", "-vv")
392394

393-
# make sure that that we get a '0' exit code for the testsuite
395+
# make sure that we get a '0' exit code for the testsuite
394396
result.assert_outcomes(passed=1)
395397
pymon_path = pathlib.Path(str(testdir)) / ".pymon"
396398
assert pymon_path.exists()
@@ -403,6 +405,6 @@ def run(a, b):
403405
pymon_path.unlink()
404406
result = testdir.runpytest("--doctest-modules", "--no-monitor", "-vv")
405407

406-
# make sure that that we get a '0' exit code for the testsuite
408+
# make sure that we get a '0' exit code for the testsuite
407409
result.assert_outcomes(passed=1)
408410
assert not pymon_path.exists()

0 commit comments

Comments
 (0)
Please sign in to comment.