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

1import os 

2import json 

3import platform 

4import socket 

5import sys 

6from datetime import datetime, timedelta, timezone 

7from typing import Dict, List 

8 

9import pytest 

10from _pytest.config import Config 

11from _pytest.config.argparsing import Parser 

12from _pytest.terminal import TerminalReporter 

13 

14from pytest_recap.models import RerunTestGroup, TestOutcome, TestResult, TestSession 

15from pytest_recap.storage import JSONStorage 

16 

17 

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] 

27 

28 

29def pytest_addoption(parser: Parser) -> None: 

30 """Add command line options for pytest-recap, supporting environment variable defaults. 

31 

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 ) 

53 

54 

55def pytest_configure(config: Config) -> None: 

56 config._recap_enabled = config.getoption("--recap") 

57 config._recap_destination = config.getoption("--recap-destination") 

58 

59 

60@pytest.hookimpl(hookwrapper=True) 

61def pytest_terminal_summary(terminalreporter: TerminalReporter, exitstatus: int, config: Config): 

62 yield 

63 

64 if not getattr(config, "_recap_enabled", False): 

65 return 

66 

67 # Get the destination URI if specified 

68 recap_destination = getattr(config, "_recap_destination", None) 

69 

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 

77 

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 ) 

115 

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 

124 

125 # Create/process rerun test groups 

126 rerun_test_groups = group_tests_into_rerun_test_groups(test_results) 

127 

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 

134 

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.") 

150 

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 ) 

171 

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) 

189 

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}")