Skip to content

Commit 3acbe82

Browse files
authored
feat(ci-insights): Detect newly added tests (#178)
Fixes: MRGFY-5824
1 parent 4b5ec4f commit 3acbe82

File tree

4 files changed

+108
-42
lines changed

4 files changed

+108
-42
lines changed

pytest_mergify/__init__.py

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ def _report_flaky_detection(
102102
self,
103103
terminalreporter: _pytest.terminal.TerminalReporter,
104104
) -> None:
105+
if not self.mergify_ci.is_flaky_detection_enabled():
106+
return
107+
105108
if self.mergify_ci.flaky_detection_error_message:
106109
terminalreporter.write_line(
107110
f"Unable to perform flaky detection. Error: {self.mergify_ci.flaky_detection_error_message}",
@@ -110,11 +113,20 @@ def _report_flaky_detection(
110113

111114
return
112115

113-
# NOTE(remyduthu): This is a temporary log. These names form the
114-
# baseline to later detect newly added tests.
116+
# NOTE(remyduthu):
117+
# The following logs are temporary.
118+
# We'll automatically retry newly added tests to detect flakiness.
115119
terminalreporter.write_line(
116-
f"Found {len(self.mergify_ci.existing_test_names)} existing tests",
120+
f"Fetched {len(self.mergify_ci.existing_test_names)} existing tests",
117121
)
122+
terminalreporter.write_line(
123+
f"Detected {len(self.mergify_ci.new_test_durations_by_name)} new tests"
124+
)
125+
for (
126+
test_name,
127+
test_duration_ms,
128+
) in self.mergify_ci.new_test_durations_by_name.items():
129+
terminalreporter.write_line(f" - {test_name} ({test_duration_ms}ms)")
118130

119131
@property
120132
def tracer(self) -> typing.Optional[opentelemetry.trace.Tracer]:
@@ -252,6 +264,27 @@ def pytest_exception_interact(
252264
)
253265
)
254266

267+
def _handle_flaky_detection_for_report(
268+
self,
269+
report: _pytest.reports.TestReport,
270+
) -> None:
271+
if not self.mergify_ci.is_flaky_detection_active():
272+
return
273+
274+
test_duration_ms = int(report.duration * 1000)
275+
self.mergify_ci.total_test_durations_ms += test_duration_ms
276+
277+
test_name = report.nodeid
278+
if test_name in self.mergify_ci.existing_test_names:
279+
return
280+
281+
self.mergify_ci.add_new_test_duration(test_name, test_duration_ms)
282+
283+
if self.tracer:
284+
opentelemetry.trace.get_current_span().set_attributes(
285+
{"cicd.test.new": True}
286+
)
287+
255288
def pytest_runtest_logreport(self, report: _pytest.reports.TestReport) -> None:
256289
if self.tracer is None:
257290
return
@@ -278,6 +311,8 @@ def pytest_runtest_logreport(self, report: _pytest.reports.TestReport) -> None:
278311
}
279312
)
280313

314+
self._handle_flaky_detection_for_report(report)
315+
281316

282317
def pytest_addoption(parser: _pytest.config.argparsing.Parser) -> None:
283318
group = parser.getgroup("pytest-mergify", "Mergify support for pytest")

pytest_mergify/ci_insights.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ class MergifyCIInsights:
8787
init=False,
8888
default=None,
8989
)
90+
total_test_durations_ms: int = dataclasses.field(init=False, default=0)
91+
new_test_durations_by_name: typing.Dict[str, int] = dataclasses.field(
92+
init=False, default_factory=dict
93+
)
9094
quarantined_tests: typing.Optional[pytest_mergify.quarantine.Quarantine] = (
9195
dataclasses.field(
9296
init=False,
@@ -168,9 +172,24 @@ def __post_init__(self) -> None:
168172
self.branch_name,
169173
)
170174

171-
def _load_flaky_detection(self) -> None:
175+
def add_new_test_duration(self, test_name: str, test_duration_ms: int) -> None:
176+
if test_name in self.new_test_durations_by_name:
177+
return
178+
179+
self.new_test_durations_by_name[test_name] = test_duration_ms
180+
181+
def is_flaky_detection_enabled(self) -> bool:
172182
# NOTE(remyduthu): Hide behind a feature flag for now.
173-
if not utils.is_env_truthy("_MERGIFY_TEST_NEW_FLAKY_DETECTION"):
183+
return utils.is_env_truthy("_MERGIFY_TEST_NEW_FLAKY_DETECTION")
184+
185+
def is_flaky_detection_active(self) -> bool:
186+
return (
187+
self.is_flaky_detection_enabled()
188+
and self.flaky_detection_error_message is None
189+
)
190+
191+
def _load_flaky_detection(self) -> None:
192+
if not self.is_flaky_detection_enabled():
174193
return
175194

176195
try:

tests/test_ci_insights.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
from pytest_mergify import ci_insights
77

8+
from . import conftest
9+
810

911
def _set_test_environment(monkeypatch: pytest.MonkeyPatch) -> None:
1012
monkeypatch.setenv("_MERGIFY_TEST_NEW_FLAKY_DETECTION", "true")
@@ -66,3 +68,50 @@ def test_load_flaky_detection_error(monkeypatch: pytest.MonkeyPatch) -> None:
6668
assert not client.existing_test_names
6769
assert client.flaky_detection_error_message is not None
6870
assert "500 Server Error" in client.flaky_detection_error_message
71+
72+
73+
@responses.activate
74+
def test_flaky_detection_detects_new_tests(
75+
monkeypatch: pytest.MonkeyPatch,
76+
pytester_with_spans: conftest.PytesterWithSpanT,
77+
) -> None:
78+
_set_test_environment(monkeypatch)
79+
_make_quarantine_mock()
80+
_make_test_names_mock(
81+
[
82+
"test_flaky_detection_detects_new_tests.py::test_foo",
83+
"test_flaky_detection_detects_new_tests.py::test_unknown",
84+
]
85+
)
86+
87+
result, spans = pytester_with_spans(
88+
code="""
89+
def test_foo():
90+
assert True
91+
92+
def test_bar():
93+
assert True
94+
95+
def test_baz():
96+
assert True
97+
"""
98+
)
99+
result.assert_outcomes(passed=3)
100+
101+
assert """Fetched 2 existing tests
102+
Detected 2 new tests
103+
- test_flaky_detection_detects_new_tests.py::test_bar (0ms)
104+
- test_flaky_detection_detects_new_tests.py::test_baz (0ms)""" in result.stdout.str()
105+
106+
assert spans is not None
107+
for test_name, expected in {
108+
"test_flaky_detection_detects_new_tests.py::test_foo": False,
109+
"test_flaky_detection_detects_new_tests.py::test_bar": True,
110+
"test_flaky_detection_detects_new_tests.py::test_baz": True,
111+
}.items():
112+
span = spans.get(test_name)
113+
114+
assert span is not None
115+
assert span.attributes is not None
116+
if expected:
117+
assert span.attributes.get("cicd.test.new")

tests/test_plugin.py

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import _pytest.config
22
import pytest
3-
import responses
43
from _pytest.pytester import Pytester
54

65
import pytest_mergify
@@ -202,39 +201,3 @@ def test_pass():
202201
line.startswith("::notice title=Mergify CI::MERGIFY_TEST_RUN_ID=")
203202
for line in result.stdout.lines
204203
)
205-
206-
207-
@responses.activate
208-
def test_summary_shows_flaky_test_detection(
209-
monkeypatch: pytest.MonkeyPatch,
210-
pytester: Pytester,
211-
) -> None:
212-
monkeypatch.setenv("_MERGIFY_TEST_NEW_FLAKY_DETECTION", "true")
213-
monkeypatch.setenv("_PYTEST_MERGIFY_TEST", "true")
214-
monkeypatch.setenv("CI", "true")
215-
monkeypatch.setenv("GITHUB_ACTIONS", "true")
216-
monkeypatch.setenv("GITHUB_BASE_REF", "main")
217-
monkeypatch.setenv("GITHUB_REPOSITORY", "Mergifyio/pytest-mergify")
218-
monkeypatch.setenv("MERGIFY_API_URL", "https://example.com")
219-
monkeypatch.setenv("MERGIFY_TOKEN", "my_token")
220-
221-
responses.add(
222-
method=responses.GET,
223-
url="https://example.com/v1/ci/Mergifyio/tests/names",
224-
match=[
225-
responses.matchers.query_param_matcher(
226-
{"repository": "pytest-mergify", "branch": "main"}
227-
)
228-
],
229-
json={"test_names": ["x::test_a", "x::test_b"]},
230-
status=200,
231-
)
232-
233-
pytester.makepyfile(
234-
"""
235-
def test_foo():
236-
assert True
237-
"""
238-
)
239-
result = pytester.runpytest_inprocess()
240-
assert "Found 2 existing tests" in result.stdout.str()

0 commit comments

Comments
 (0)