Coverage for src/prosemark/adapters/fake_logger.py: 100%

61 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-09-24 18:08 +0000

1# Copyright (c) 2024 Prosemark Contributors 

2# This software is licensed under the MIT License 

3 

4"""In-memory fake implementation of Logger for testing.""" 

5 

6from prosemark.ports.logger import Logger 

7 

8 

9class FakeLogger(Logger): 

10 """In-memory fake implementation of Logger for testing. 

11 

12 Provides complete logging functionality by collecting messages 

13 in memory instead of outputting them. Includes test helper methods 

14 for asserting logging behavior in tests. 

15 

16 This fake stores all logged messages with their level and provides methods 

17 to inspect the logs for test assertions without exposing internal 

18 implementation details. 

19 

20 Examples: 

21 >>> logger = FakeLogger() 

22 >>> logger.info('Operation completed') 

23 >>> logger.error('Failed to process %s', 'item') 

24 >>> logger.get_logs() 

25 [('info', 'Operation completed', (), {}), ('error', 'Failed to process %s', ('item',), {})] 

26 >>> logger.get_logs_by_level('info') 

27 [('info', 'Operation completed', (), {})] 

28 >>> logger.has_logged('error', 'Failed to process') 

29 True 

30 

31 """ 

32 

33 def __init__(self) -> None: 

34 """Initialize empty fake logger.""" 

35 self._logs: list[tuple[str, object, tuple[object, ...], dict[str, object]]] = [] 

36 

37 def debug(self, msg: object, *args: object, **kwargs: object) -> None: 

38 """Store debug message in log buffer. 

39 

40 Args: 

41 msg: Message to log 

42 *args: Positional arguments for message formatting 

43 **kwargs: Keyword arguments for structured logging 

44 

45 """ 

46 self._logs.append(('debug', msg, args, kwargs)) 

47 

48 def info(self, msg: object, *args: object, **kwargs: object) -> None: 

49 """Store info message in log buffer. 

50 

51 Args: 

52 msg: Message to log 

53 *args: Positional arguments for message formatting 

54 **kwargs: Keyword arguments for structured logging 

55 

56 """ 

57 self._logs.append(('info', msg, args, kwargs)) 

58 

59 def warning(self, msg: object, *args: object, **kwargs: object) -> None: 

60 """Store warning message in log buffer. 

61 

62 Args: 

63 msg: Message to log 

64 *args: Positional arguments for message formatting 

65 **kwargs: Keyword arguments for structured logging 

66 

67 """ 

68 self._logs.append(('warning', msg, args, kwargs)) 

69 

70 def error(self, msg: object, *args: object, **kwargs: object) -> None: 

71 """Store error message in log buffer. 

72 

73 Args: 

74 msg: Message to log 

75 *args: Positional arguments for message formatting 

76 **kwargs: Keyword arguments for structured logging 

77 

78 """ 

79 self._logs.append(('error', msg, args, kwargs)) 

80 

81 def exception(self, msg: object, *args: object, **kwargs: object) -> None: 

82 """Store exception message in log buffer. 

83 

84 Args: 

85 msg: Message to log 

86 *args: Positional arguments for message formatting 

87 **kwargs: Keyword arguments for structured logging 

88 

89 """ 

90 self._logs.append(('exception', msg, args, kwargs)) 

91 

92 def get_logs(self) -> list[tuple[str, object, tuple[object, ...], dict[str, object]]]: 

93 """Return list of all logged messages. 

94 

95 Returns: 

96 List of tuples containing (level, message, args, kwargs) in the order they were logged. 

97 

98 """ 

99 return self._logs.copy() 

100 

101 def get_logs_by_level(self, level: str) -> list[tuple[str, object, tuple[object, ...], dict[str, object]]]: 

102 """Return list of logged messages for a specific level. 

103 

104 Args: 

105 level: Log level to filter by ('debug', 'info', 'warning', 'error') 

106 

107 Returns: 

108 List of tuples containing (level, message, args, kwargs) for the specified level. 

109 

110 """ 

111 return [log for log in self._logs if log[0] == level] 

112 

113 def has_logged(self, level: str, text: str) -> bool: 

114 """Check if any log message at the given level contains the text. 

115 

116 Args: 

117 level: Log level to check ('debug', 'info', 'warning', 'error') 

118 text: Text to search for in log messages 

119 

120 Returns: 

121 True if any message at the specified level contains the given text. 

122 

123 """ 

124 level_logs = self.get_logs_by_level(level) 

125 for log in level_logs: 

126 # Check raw message 

127 if text in str(log[1]): 

128 return True 

129 # Check formatted message if args are present 

130 if log[2]: # args tuple is not empty 

131 try: 

132 formatted_msg = str(log[1]) % log[2] 

133 if text in formatted_msg: 

134 return True 

135 except (TypeError, ValueError): # pragma: no cover 

136 # If formatting fails, continue to next log 

137 pass 

138 return False 

139 

140 def get_logged_messages(self) -> list[str]: 

141 """Get all logged messages formatted as strings. 

142 

143 Returns: 

144 List of formatted log messages as strings. 

145 

146 """ 

147 messages = [] 

148 for _level, msg, args, kwargs in self._logs: 

149 if args: 

150 try: 

151 formatted_msg = str(msg) % args 

152 except (TypeError, ValueError): # pragma: no cover 

153 formatted_msg = f'{msg} {args}' 

154 else: 

155 formatted_msg = str(msg) 

156 

157 # Append kwargs if present 

158 if kwargs: 

159 formatted_msg += f' {kwargs!r}' 

160 

161 messages.append(formatted_msg) 

162 return messages 

163 

164 def clear_logs(self) -> None: 

165 """Clear all stored log messages. 

166 

167 Useful for resetting state between test cases. 

168 

169 """ 

170 self._logs.clear() 

171 

172 def last_log(self) -> tuple[str, object, tuple[object, ...], dict[str, object]]: 

173 """Return the last logged message. 

174 

175 Returns: 

176 Tuple containing (level, message, args, kwargs) for the most recent log entry. 

177 

178 Raises: 

179 IndexError: If no messages have been logged. 

180 

181 """ 

182 if not self._logs: # pragma: no cover 

183 msg = 'No logs have been recorded' 

184 raise IndexError(msg) # pragma: no cover 

185 return self._logs[-1] # pragma: no cover 

186 

187 def log_count(self) -> int: 

188 """Return the total number of logged messages. 

189 

190 Returns: 

191 Total count of all logged messages across all levels. 

192 

193 """ 

194 return len(self._logs) 

195 

196 def log_count_by_level(self, level: str) -> int: 

197 """Return the count of logged messages for a specific level. 

198 

199 Args: 

200 level: Log level to count ('debug', 'info', 'warning', 'error') 

201 

202 Returns: 

203 Count of messages for the specified level. 

204 

205 """ 

206 return len(self.get_logs_by_level(level)) 

207 

208 @property 

209 def info_messages(self) -> list[str]: 

210 """Get formatted info messages.""" 

211 return [str(log[1]) % log[2] if log[2] else str(log[1]) for log in self.get_logs_by_level('info')] 

212 

213 @property 

214 def error_messages(self) -> list[str]: 

215 """Get formatted error messages.""" 

216 return [str(log[1]) % log[2] if log[2] else str(log[1]) for log in self.get_logs_by_level('error')] 

217 

218 @property 

219 def debug_messages(self) -> list[str]: 

220 """Get formatted debug messages.""" 

221 return [str(log[1]) % log[2] if log[2] else str(log[1]) for log in self.get_logs_by_level('debug')] 

222 

223 @property 

224 def exception_messages(self) -> list[str]: 

225 """Get formatted exception messages.""" 

226 return [str(log[1]) % log[2] if log[2] else str(log[1]) for log in self.get_logs_by_level('exception')] 

227 

228 def clear(self) -> None: 

229 """Alias for clear_logs for convenience.""" 

230 self.clear_logs()