Coverage for pytest_recap/plugin.py: 0%
101 statements
« prev ^ index » next coverage.py v7.4.1, created at 2025-05-05 06:24 -0600
« prev ^ index » next coverage.py v7.4.1, created at 2025-05-05 06:24 -0600
1import os
2import json
3import platform
4import socket
5import sys
6from datetime import datetime, timedelta, timezone
7from typing import Dict, List
9import pytest
10from _pytest.config import Config
11from _pytest.config.argparsing import Parser
12from _pytest.terminal import TerminalReporter
14from pytest_recap.models import RerunTestGroup, TestOutcome, TestResult, TestSession
15from pytest_recap.storage import JSONStorage
18def group_tests_into_rerun_test_groups(
19 test_results: List[TestResult],
20) -> List[RerunTestGroup]:
21 rerun_test_groups: Dict[str, RerunTestGroup] = {}
22 for test_result in test_results:
23 if test_result.nodeid not in rerun_test_groups:
24 rerun_test_groups[test_result.nodeid] = RerunTestGroup(nodeid=test_result.nodeid)
25 rerun_test_groups[test_result.nodeid].add_test(test_result)
26 return [group for group in rerun_test_groups.values() if len(group.tests) > 1]
29def pytest_addoption(parser: Parser) -> None:
30 """Add command line options for pytest-recap, supporting environment variable defaults.
32 Args:
33 parser (Parser): The pytest parser object.
34 """
35 group = parser.getgroup("Pytest Recap")
36 # Support env var for enabling recap
37 recap_env = os.environ.get("RECAP_ENABLE", "0").lower()
38 recap_default = recap_env in ("1", "true", "yes")
39 # Support env var for destination path
40 recap_dest_default = os.environ.get("RECAP_DESTINATION")
41 group.addoption(
42 "--recap",
43 action="store_true",
44 default=recap_default,
45 help="Enable pytest recap plugin. (or set RECAP_ENABLE=1)",
46 )
47 group.addoption(
48 "--recap-destination",
49 action="store",
50 default=recap_dest_default,
51 help="Specify the storage destination (filepath) for pytest-recap to use (or set RECAP_DESTINATION)",
52 )
55def pytest_configure(config: Config) -> None:
56 config._recap_enabled = config.getoption("--recap")
57 config._recap_destination = config.getoption("--recap-destination")
60@pytest.hookimpl(hookwrapper=True)
61def pytest_terminal_summary(terminalreporter: TerminalReporter, exitstatus: int, config: Config):
62 yield
64 if not getattr(config, "_recap_enabled", False):
65 return
67 # Get the destination URI if specified
68 recap_destination = getattr(config, "_recap_destination", None)
70 # Gather SUT and system info
71 sut_name = os.environ.get("SBP_QA_NAME") or "pytest-recap"
72 hostname = socket.gethostname()
73 testing_system_name = hostname
74 now = datetime.now(timezone.utc)
75 session_start = None
76 session_end = None
78 test_results = []
79 stats = terminalreporter.stats
80 for outcome, reports in stats.items():
81 if not outcome or outcome == "warnings":
82 continue
83 for report in reports:
84 # Only handle TestReport instances with nodeid
85 if not hasattr(report, "nodeid") or not hasattr(report, "when"):
86 continue
87 if report.when == "call" or (
88 report.when in ("setup", "teardown") and getattr(report, "outcome", None) in ("failed", "error")
89 ):
90 # Use report.start if available, else fallback to now
91 report_time = (
92 datetime.fromtimestamp(getattr(report, "start", now.timestamp()), tz=timezone.utc)
93 if hasattr(report, "start")
94 else now
95 )
96 if session_start is None or report_time < session_start:
97 session_start = report_time
98 report_end = report_time + timedelta(seconds=getattr(report, "duration", 0) or 0)
99 if session_end is None or report_end > session_end:
100 session_end = report_end
101 test_results.append(
102 TestResult(
103 nodeid=report.nodeid,
104 outcome=(TestOutcome.from_str(outcome) if outcome else TestOutcome.SKIPPED),
105 start_time=report_time,
106 stop_time=report_end,
107 duration=getattr(report, "duration", None),
108 caplog=getattr(report, "caplog", ""),
109 capstderr=getattr(report, "capstderr", ""),
110 capstdout=getattr(report, "capstdout", ""),
111 longreprtext=str(getattr(report, "longrepr", "")),
112 has_warning=bool(getattr(report, "warning_messages", [])),
113 )
114 )
116 # Handle warnings
117 if "warnings" in stats:
118 for report in stats["warnings"]:
119 if hasattr(report, "nodeid"):
120 for test_result in test_results:
121 if test_result.nodeid == report.nodeid:
122 test_result.has_warning = True
123 break
125 # Create/process rerun test groups
126 rerun_test_groups = group_tests_into_rerun_test_groups(test_results)
128 session_timestamp = now.strftime("%Y%m%d-%H%M%S")
129 session_id = f"{sut_name}-{session_timestamp}"
130 if session_start and session_end:
131 session_duration = (session_end - session_start).total_seconds()
132 else:
133 session_duration = 0.0
135 tags_env = os.environ.get("RECAP_SESSION_TAGS")
136 session_tags = {
137 "tag_1": "value_1",
138 "tag_2": "value_2",
139 "tag_3": "value_3",
140 }
141 if tags_env:
142 try:
143 loaded_tags = json.loads(tags_env)
144 if isinstance(loaded_tags, dict):
145 session_tags = loaded_tags
146 else:
147 terminalreporter.write_line("WARNING: RECAP_SESSION_TAGS must be a JSON object. Using default tags.")
148 except Exception as e:
149 terminalreporter.write_line(f"WARNING: Invalid RECAP_SESSION_TAGS: {e}. Using default tags.")
151 session = TestSession(
152 sut_name=sut_name,
153 testing_system={
154 "hostname": hostname,
155 "name": testing_system_name,
156 "type": "local",
157 "sys_platform": sys.platform,
158 "platform_platform": platform.platform(),
159 "python_version": platform.python_version(),
160 "pytest_version": getattr(config, "version", "unknown"),
161 "environment": os.environ.get("RECAP_ENV", "test"),
162 },
163 session_id=session_id,
164 session_start_time=session_start,
165 session_stop_time=session_end,
166 session_duration=session_duration,
167 session_tags=session_tags,
168 rerun_test_groups=rerun_test_groups,
169 test_results=test_results,
170 )
172 # Determine the output file path
173 if recap_destination:
174 if os.path.isdir(recap_destination) or recap_destination.endswith("/"):
175 os.makedirs(recap_destination, exist_ok=True)
176 filename = f"{session_timestamp}_{sut_name}.json"
177 filepath = os.path.join(recap_destination, filename)
178 else:
179 filepath = recap_destination
180 parent_dir = os.path.dirname(filepath)
181 if parent_dir:
182 os.makedirs(parent_dir, exist_ok=True)
183 else:
184 base_dir = os.environ.get("SESSION_WRITE_BASE_DIR", "/tmp/pytest_recap_sessions")
185 date_dir = os.path.join(base_dir, now.strftime("%Y/%m"))
186 os.makedirs(date_dir, exist_ok=True)
187 filename = f"{session_timestamp}_{sut_name}.json"
188 filepath = os.path.join(date_dir, filename)
190 # Write the session to file
191 print(f"DEBUG: Writing recap to {filepath}")
192 storage = JSONStorage(filepath)
193 storage.save_single_session(session.to_dict())
194 terminalreporter.write_sep("-")
195 terminalreporter.write_line(f"Pytest Recap session written to: {filepath}")