Skip to content

Commit 9a7fb24

Browse files
authored
chore(ci-insights): Add debug logs to the flaky detector (#348)
The goal of this change is to add some structure debug logs to the flaky detector. We often need these logs to debug some budget/reruns issues. The logs are only displayed if the `PYTEST_MERGIFY_DEBUG` flag is set. Depends-On: #347
1 parent 21311b0 commit 9a7fb24

File tree

2 files changed

+74
-3
lines changed

2 files changed

+74
-3
lines changed

pytest_mergify/flaky_detection.py

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,10 @@ class FlakyDetector:
155155
https://github.com/pytest-dev/pytest-rerunfailures/blob/master/src/pytest_rerunfailures.py#L503-L542
156156
"""
157157

158+
_debug_logs: typing.List[utils.StructuredLog] = dataclasses.field(
159+
init=False, default_factory=list
160+
)
161+
158162
def __post_init__(self) -> None:
159163
self._context = self._fetch_context()
160164

@@ -257,11 +261,24 @@ def is_rerunning_test(self, test: str) -> bool:
257261
def is_last_rerun_for_test(self, test: str) -> bool:
258262
metrics = self._test_metrics[test]
259263

260-
return (
264+
will_exceed_deadline = metrics.will_exceed_deadline()
265+
will_exceed_rerun_count = (
261266
metrics.rerun_count >= self._context.max_test_execution_count
262-
or metrics.will_exceed_deadline()
263267
)
264268

269+
self._debug_logs.append(
270+
utils.StructuredLog.make(
271+
message="Check for last rerun",
272+
test=test,
273+
deadline=metrics.deadline.isoformat() if metrics.deadline else None,
274+
rerun_count=metrics.rerun_count,
275+
will_exceed_deadline=will_exceed_deadline,
276+
will_exceed_rerun_count=will_exceed_rerun_count,
277+
)
278+
)
279+
280+
return will_exceed_deadline or will_exceed_rerun_count
281+
265282
def make_report(self) -> str:
266283
result = "🐛 Flaky detection"
267284
if self._over_length_tests:
@@ -334,16 +351,34 @@ def make_report(self) -> str:
334351
f"Reference: https://github.com/pytest-dev/pytest-timeout?tab=readme-ov-file#avoiding-timeouts-in-fixtures"
335352
)
336353

354+
if os.environ.get("PYTEST_MERGIFY_DEBUG") and self._debug_logs:
355+
result += f"{os.linesep}🔎 Debug Logs"
356+
for log in self._debug_logs:
357+
result += f"{os.linesep}{log.to_json()}"
358+
337359
return result
338360

339361
def set_test_deadline(
340362
self, test: str, timeout: typing.Optional[datetime.timedelta] = None
341363
) -> None:
342364
metrics = self._test_metrics[test]
343365

366+
remaining_budget = self._get_remaining_budget_duration()
367+
remaining_tests = self._count_remaining_tests()
368+
344369
# Distribute remaining budget equally across remaining tests.
345370
metrics.deadline = datetime.datetime.now(datetime.timezone.utc) + (
346-
self._get_remaining_budget_duration() / self._count_remaining_tests()
371+
remaining_budget / remaining_tests
372+
)
373+
self._debug_logs.append(
374+
utils.StructuredLog.make(
375+
message="Deadline set",
376+
test=test,
377+
available_budget=str(self._available_budget_duration),
378+
remaining_budget=str(remaining_budget),
379+
all_tests=len(self._tests_to_process),
380+
remaining_tests=remaining_tests,
381+
)
347382
)
348383

349384
if not timeout:
@@ -356,6 +391,15 @@ def set_test_deadline(
356391
if not metrics.deadline or timeout_deadline < metrics.deadline:
357392
metrics.deadline = timeout_deadline
358393
metrics.prevented_timeout = True
394+
self._debug_logs.append(
395+
utils.StructuredLog.make(
396+
message="Deadline updated to prevent timeout",
397+
test=test,
398+
timeout=str(timeout),
399+
safe_timeout=str(safe_timeout),
400+
deadline=metrics.deadline,
401+
)
402+
)
359403

360404
def suspend_item_finalizers(self, item: _pytest.nodes.Item) -> None:
361405
"""

pytest_mergify/utils.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import dataclasses
2+
import datetime
3+
import json
14
import os
25
import re
36
import subprocess
@@ -15,6 +18,30 @@
1518
}
1619

1720

21+
@dataclasses.dataclass
22+
class StructuredLog:
23+
message: str
24+
timestamp: datetime.datetime
25+
attributes: typing.Dict[str, typing.Any]
26+
27+
@classmethod
28+
def make(cls, message: str, **kwargs: typing.Any) -> "StructuredLog":
29+
return cls(
30+
message=message,
31+
timestamp=datetime.datetime.now(datetime.timezone.utc),
32+
attributes=kwargs,
33+
)
34+
35+
def to_json(self) -> str:
36+
return json.dumps(
37+
{
38+
"timestamp": self.timestamp.isoformat(),
39+
"message": self.message,
40+
**self.attributes,
41+
}
42+
)
43+
44+
1845
def is_in_ci() -> bool:
1946
return strtobool(os.environ.get("CI", "false")) or strtobool(
2047
os.environ.get("PYTEST_MERGIFY_ENABLE", "false")

0 commit comments

Comments
 (0)