test_log_viewer.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. """
  2. Tests for the Log Viewer UI component (Story 7.15).
  3. """
  4. import pytest
  5. from pathlib import Path
  6. import tempfile
  7. from datetime import datetime, timedelta
  8. from PyQt6.QtWidgets import QApplication
  9. from PyQt6.QtCore import Qt
  10. from src.ui.log_viewer import (
  11. LogLevel,
  12. LogEntry,
  13. InMemoryLogHandler,
  14. LogListModel,
  15. LogTableModel,
  16. LogViewer,
  17. get_log_handler,
  18. log_info,
  19. log_warning,
  20. log_error,
  21. )
  22. @pytest.fixture
  23. def app(qtbot):
  24. """Create QApplication for tests."""
  25. return QApplication.instance() or QApplication([])
  26. @pytest.fixture
  27. def clean_handler():
  28. """Create a fresh log handler for testing."""
  29. import src.ui.log_viewer as lv
  30. lv._global_log_handler = None
  31. handler = InMemoryLogHandler()
  32. return handler
  33. @pytest.fixture
  34. def sample_entries():
  35. """Create sample log entries."""
  36. now = datetime.now()
  37. return [
  38. LogEntry(
  39. level=LogLevel.INFO,
  40. timestamp=now - timedelta(minutes=3),
  41. message="Application started",
  42. module="main"
  43. ),
  44. LogEntry(
  45. level=LogLevel.DEBUG,
  46. timestamp=now - timedelta(minutes=2),
  47. message="Loading configuration",
  48. module="config"
  49. ),
  50. LogEntry(
  51. level=LogLevel.WARNING,
  52. timestamp=now - timedelta(minutes=1),
  53. message="Configuration file not found, using defaults",
  54. module="config"
  55. ),
  56. LogEntry(
  57. level=LogLevel.ERROR,
  58. timestamp=now - timedelta(seconds=30),
  59. message="Failed to connect to server",
  60. module="network"
  61. ),
  62. LogEntry(
  63. level=LogLevel.INFO,
  64. timestamp=now,
  65. message="Translation started",
  66. module="translator"
  67. ),
  68. ]
  69. class TestLogLevel:
  70. """Tests for LogLevel enum."""
  71. def test_level_values(self):
  72. """Test LogLevel enum values."""
  73. assert LogLevel.DEBUG.value == "DEBUG"
  74. assert LogLevel.INFO.value == "INFO"
  75. assert LogLevel.WARNING.value == "WARNING"
  76. assert LogLevel.ERROR.value == "ERROR"
  77. assert LogLevel.CRITICAL.value == "CRITICAL"
  78. def test_display_names(self):
  79. """Test Chinese display names."""
  80. assert LogLevel.DEBUG.display_name == "调试"
  81. assert LogLevel.INFO.display_name == "信息"
  82. assert LogLevel.WARNING.display_name == "警告"
  83. assert LogLevel.ERROR.display_name == "错误"
  84. assert LogLevel.CRITICAL.display_name == "严重"
  85. def test_colors(self):
  86. """Test level colors."""
  87. assert LogLevel.DEBUG.color == "#888888"
  88. assert LogLevel.INFO.color == "#3498db"
  89. assert LogLevel.WARNING.color == "#f39c12"
  90. assert LogLevel.ERROR.color == "#e74c3c"
  91. assert LogLevel.CRITICAL.color == "#8e44ad"
  92. class TestLogEntry:
  93. """Tests for LogEntry dataclass."""
  94. def test_entry_creation(self):
  95. """Test creating a log entry."""
  96. entry = LogEntry(
  97. level=LogLevel.INFO,
  98. timestamp=datetime.now(),
  99. message="Test message"
  100. )
  101. assert entry.level == LogLevel.INFO
  102. assert entry.message == "Test message"
  103. def test_str_representation(self):
  104. """Test string representation of entry."""
  105. now = datetime.now()
  106. entry = LogEntry(
  107. level=LogLevel.INFO,
  108. timestamp=now,
  109. message="Test message"
  110. )
  111. str_repr = str(entry)
  112. assert "[INFO]" in str_repr
  113. assert "Test message" in str_repr
  114. def test_timestamp_display(self):
  115. """Test timestamp display formatting."""
  116. now = datetime(2024, 3, 15, 14, 30, 45)
  117. entry = LogEntry(
  118. level=LogLevel.INFO,
  119. timestamp=now,
  120. message="Test"
  121. )
  122. assert entry.timestamp_display == "14:30:45"
  123. assert entry.date_display == "2024-03-15"
  124. def test_level_properties(self):
  125. """Test level-related properties."""
  126. entry = LogEntry(
  127. level=LogLevel.ERROR,
  128. timestamp=datetime.now(),
  129. message="Error message"
  130. )
  131. assert entry.level_display == "错误"
  132. assert entry.level_color == "#e74c3c"
  133. class TestInMemoryLogHandler:
  134. """Tests for InMemoryLogHandler."""
  135. def test_initialization(self, clean_handler):
  136. """Test handler initialization."""
  137. assert clean_handler.entry_count == 0
  138. def test_emit_entry(self, clean_handler):
  139. """Test emitting a log entry."""
  140. clean_handler.emit(LogLevel.INFO, "Test message")
  141. assert clean_handler.entry_count == 1
  142. def test_emit_with_metadata(self, clean_handler):
  143. """Test emitting with metadata."""
  144. clean_handler.emit(
  145. LogLevel.ERROR,
  146. "Error occurred",
  147. module="test_module",
  148. function="test_func",
  149. line=42
  150. )
  151. assert clean_handler.entry_count == 1
  152. def test_get_all_entries(self, clean_handler):
  153. """Test getting all entries."""
  154. clean_handler.emit(LogLevel.INFO, "Message 1")
  155. clean_handler.emit(LogLevel.WARNING, "Message 2")
  156. entries = clean_handler.get_entries()
  157. assert len(entries) == 2
  158. def test_filter_by_level(self, clean_handler):
  159. """Test filtering by log level."""
  160. clean_handler.emit(LogLevel.DEBUG, "Debug message")
  161. clean_handler.emit(LogLevel.INFO, "Info message")
  162. clean_handler.emit(LogLevel.WARNING, "Warning message")
  163. clean_handler.emit(LogLevel.ERROR, "Error message")
  164. # Get only INFO and above
  165. entries = clean_handler.get_entries(min_level=LogLevel.INFO)
  166. assert len(entries) == 3
  167. # Get only ERROR and above
  168. entries = clean_handler.get_entries(min_level=LogLevel.ERROR)
  169. assert len(entries) == 1
  170. def test_filter_by_search_text(self, clean_handler):
  171. """Test filtering by search text."""
  172. clean_handler.emit(LogLevel.INFO, "Application started")
  173. clean_handler.emit(LogLevel.INFO, "Configuration loaded")
  174. clean_handler.emit(LogLevel.ERROR, "Failed to connect")
  175. entries = clean_handler.get_entries(search_text="failed")
  176. assert len(entries) == 1
  177. entries = clean_handler.get_entries(search_text="app")
  178. assert len(entries) == 1
  179. def test_limit_entries(self, clean_handler):
  180. """Test limiting number of entries."""
  181. for i in range(10):
  182. clean_handler.emit(LogLevel.INFO, f"Message {i}")
  183. entries = clean_handler.get_entries(limit=5)
  184. assert len(entries) == 5
  185. def test_max_entries_trimming(self):
  186. """Test that handler trims old entries when over max."""
  187. handler = InMemoryLogHandler(max_entries=5)
  188. for i in range(10):
  189. handler.emit(LogLevel.INFO, f"Message {i}")
  190. # Should only keep last 5
  191. assert handler.entry_count == 5
  192. entries = handler.get_entries()
  193. assert entries[0].message == "Message 5"
  194. assert entries[-1].message == "Message 9"
  195. def test_clear(self, clean_handler):
  196. """Test clearing all entries."""
  197. clean_handler.emit(LogLevel.INFO, "Message 1")
  198. clean_handler.emit(LogLevel.INFO, "Message 2")
  199. assert clean_handler.entry_count == 2
  200. clean_handler.clear()
  201. assert clean_handler.entry_count == 0
  202. def test_listeners(self, clean_handler):
  203. """Test entry listeners."""
  204. received = []
  205. def listener(entry):
  206. received.append(entry)
  207. clean_handler.add_listener(listener)
  208. clean_handler.emit(LogLevel.INFO, "Test message")
  209. assert len(received) == 1
  210. assert received[0].message == "Test message"
  211. # Remove listener
  212. clean_handler.remove_listener(listener)
  213. clean_handler.emit(LogLevel.INFO, "Another message")
  214. assert len(received) == 1 # Should not have received the second message
  215. class TestLogTableModel:
  216. """Tests for LogTableModel."""
  217. def test_initialization(self, app, sample_entries):
  218. """Test model initialization."""
  219. model = LogTableModel(sample_entries)
  220. assert model.rowCount() == 5
  221. assert model.columnCount() == 4
  222. def test_data_retrieval(self, app, sample_entries):
  223. """Test data retrieval from model."""
  224. model = LogTableModel(sample_entries)
  225. # Check first row
  226. index = model.index(0, 1) # Level column
  227. assert model.data(index) == "信息"
  228. index = model.index(0, 2) # Message column
  229. assert "Application started" in model.data(index)
  230. def test_header_data(self, app, sample_entries):
  231. """Test header data."""
  232. model = LogTableModel(sample_entries)
  233. assert model.headerData(0, Qt.Orientation.Horizontal) == "时间"
  234. assert model.headerData(1, Qt.Orientation.Horizontal) == "级别"
  235. assert model.headerData(2, Qt.Orientation.Horizontal) == "消息"
  236. assert model.headerData(3, Qt.Orientation.Horizontal) == "模块"
  237. def test_get_entry_at_row(self, app, sample_entries):
  238. """Test getting entry at specific row."""
  239. model = LogTableModel(sample_entries)
  240. entry = model.get_entry_at_row(0)
  241. assert entry is not None
  242. assert entry.message == "Application started"
  243. entry = model.get_entry_at_row(999)
  244. assert entry is None
  245. class TestLogFunctions:
  246. """Tests for module-level logging functions."""
  247. def test_log_info(self):
  248. """Test log_info function."""
  249. import src.ui.log_viewer as lv
  250. lv._global_log_handler = None
  251. log_info("Info message")
  252. handler = get_log_handler()
  253. assert handler.entry_count == 1
  254. def test_log_warning(self):
  255. """Test log_warning function."""
  256. import src.ui.log_viewer as lv
  257. lv._global_log_handler = None
  258. log_warning("Warning message")
  259. handler = get_log_handler()
  260. assert handler.entry_count == 1
  261. def test_log_error(self):
  262. """Test log_error function."""
  263. import src.ui.log_viewer as lv
  264. lv._global_log_handler = None
  265. log_error("Error message")
  266. handler = get_log_handler()
  267. assert handler.entry_count == 1
  268. class TestLogHandlerSingleton:
  269. """Tests for the log handler singleton."""
  270. def test_get_singleton(self):
  271. """Test getting singleton instance."""
  272. import src.ui.log_viewer as lv
  273. lv._global_log_handler = None
  274. handler = get_log_handler()
  275. assert handler is not None
  276. assert isinstance(handler, InMemoryLogHandler)
  277. def test_singleton_persistence(self):
  278. """Test that singleton returns same instance."""
  279. import src.ui.log_viewer as lv
  280. lv._global_log_handler = None
  281. handler1 = get_log_handler()
  282. handler2 = get_log_handler()
  283. assert handler1 is handler2