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
« 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
4"""In-memory fake implementation of Logger for testing."""
6from prosemark.ports.logger import Logger
9class FakeLogger(Logger):
10 """In-memory fake implementation of Logger for testing.
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.
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.
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
31 """
33 def __init__(self) -> None:
34 """Initialize empty fake logger."""
35 self._logs: list[tuple[str, object, tuple[object, ...], dict[str, object]]] = []
37 def debug(self, msg: object, *args: object, **kwargs: object) -> None:
38 """Store debug message in log buffer.
40 Args:
41 msg: Message to log
42 *args: Positional arguments for message formatting
43 **kwargs: Keyword arguments for structured logging
45 """
46 self._logs.append(('debug', msg, args, kwargs))
48 def info(self, msg: object, *args: object, **kwargs: object) -> None:
49 """Store info message in log buffer.
51 Args:
52 msg: Message to log
53 *args: Positional arguments for message formatting
54 **kwargs: Keyword arguments for structured logging
56 """
57 self._logs.append(('info', msg, args, kwargs))
59 def warning(self, msg: object, *args: object, **kwargs: object) -> None:
60 """Store warning message in log buffer.
62 Args:
63 msg: Message to log
64 *args: Positional arguments for message formatting
65 **kwargs: Keyword arguments for structured logging
67 """
68 self._logs.append(('warning', msg, args, kwargs))
70 def error(self, msg: object, *args: object, **kwargs: object) -> None:
71 """Store error message in log buffer.
73 Args:
74 msg: Message to log
75 *args: Positional arguments for message formatting
76 **kwargs: Keyword arguments for structured logging
78 """
79 self._logs.append(('error', msg, args, kwargs))
81 def exception(self, msg: object, *args: object, **kwargs: object) -> None:
82 """Store exception message in log buffer.
84 Args:
85 msg: Message to log
86 *args: Positional arguments for message formatting
87 **kwargs: Keyword arguments for structured logging
89 """
90 self._logs.append(('exception', msg, args, kwargs))
92 def get_logs(self) -> list[tuple[str, object, tuple[object, ...], dict[str, object]]]:
93 """Return list of all logged messages.
95 Returns:
96 List of tuples containing (level, message, args, kwargs) in the order they were logged.
98 """
99 return self._logs.copy()
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.
104 Args:
105 level: Log level to filter by ('debug', 'info', 'warning', 'error')
107 Returns:
108 List of tuples containing (level, message, args, kwargs) for the specified level.
110 """
111 return [log for log in self._logs if log[0] == level]
113 def has_logged(self, level: str, text: str) -> bool:
114 """Check if any log message at the given level contains the text.
116 Args:
117 level: Log level to check ('debug', 'info', 'warning', 'error')
118 text: Text to search for in log messages
120 Returns:
121 True if any message at the specified level contains the given text.
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
140 def get_logged_messages(self) -> list[str]:
141 """Get all logged messages formatted as strings.
143 Returns:
144 List of formatted log messages as strings.
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)
157 # Append kwargs if present
158 if kwargs:
159 formatted_msg += f' {kwargs!r}'
161 messages.append(formatted_msg)
162 return messages
164 def clear_logs(self) -> None:
165 """Clear all stored log messages.
167 Useful for resetting state between test cases.
169 """
170 self._logs.clear()
172 def last_log(self) -> tuple[str, object, tuple[object, ...], dict[str, object]]:
173 """Return the last logged message.
175 Returns:
176 Tuple containing (level, message, args, kwargs) for the most recent log entry.
178 Raises:
179 IndexError: If no messages have been logged.
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
187 def log_count(self) -> int:
188 """Return the total number of logged messages.
190 Returns:
191 Total count of all logged messages across all levels.
193 """
194 return len(self._logs)
196 def log_count_by_level(self, level: str) -> int:
197 """Return the count of logged messages for a specific level.
199 Args:
200 level: Log level to count ('debug', 'info', 'warning', 'error')
202 Returns:
203 Count of messages for the specified level.
205 """
206 return len(self.get_logs_by_level(level))
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')]
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')]
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')]
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')]
228 def clear(self) -> None:
229 """Alias for clear_logs for convenience."""
230 self.clear_logs()