using FsCheck; using FsCheck.Xunit; using WorkCameraExport.Services; using Xunit; namespace WorkCameraExport.Tests { /// /// LogService 属性测试 /// Feature: work-camera-2.0.1, Property 13: 日志记录 /// Validates: Requirements 12.3 /// public class LogServicePropertyTests : IDisposable { private readonly string _testLogPath; private readonly LogService _logService; public LogServicePropertyTests() { // 为每个测试创建唯一的临时目录 _testLogPath = Path.Combine(Path.GetTempPath(), $"WorkCameraExport_LogTest_{Guid.NewGuid():N}"); Directory.CreateDirectory(_testLogPath); _logService = new LogService(_testLogPath); } public void Dispose() { // 清理测试目录 try { if (Directory.Exists(_testLogPath)) { Directory.Delete(_testLogPath, true); } } catch { // 忽略清理错误 } } /// /// Property 13: 日志记录 /// For any 用户操作,应在日志文件中记录操作信息,包含时间戳和操作类型。 /// [Property(MaxTest = 100)] public Property LogEntry_ShouldContainTimestampAndLevel() { return Prop.ForAll( Arb.From(), (message) => { // 过滤掉包含换行符的消息,避免解析问题 var safeMessage = new string(message.Get .Where(c => !char.IsControl(c) && c != '\r' && c != '\n') .ToArray()); if (string.IsNullOrWhiteSpace(safeMessage)) { return true.Label("Skipped: empty after filtering"); } // Act: 记录日志 var beforeLog = DateTime.Now; _logService.Info(safeMessage); var afterLog = DateTime.Now; // 读取日志文件 var logFiles = _logService.GetLogFiles(); if (logFiles.Count == 0) { return false.Label("No log files found"); } var logContent = File.ReadAllText(logFiles[0]); var lines = logContent.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); var lastLine = lines.LastOrDefault(); if (string.IsNullOrEmpty(lastLine)) { return false.Label("Log file is empty"); } // 解析日志条目 var (timestamp, level, logMessage) = LogService.ParseLogEntry(lastLine); // Assert: 验证日志包含时间戳和级别 return (timestamp != null) .Label("Timestamp should be present") .And(timestamp >= beforeLog.AddSeconds(-1) && timestamp <= afterLog.AddSeconds(1)) .Label("Timestamp should be within expected range") .And(level == "INFO") .Label("Level should be INFO") .And(logMessage == safeMessage) .Label("Message should match"); }); } /// /// Property 13 扩展: 日志格式化后应可正确解析 /// [Property(MaxTest = 100)] public Property LogFormat_RoundTrip_ShouldPreserveData() { var validLevels = new[] { "INFO", "WARN", "ERROR", "DEBUG" }; return Prop.ForAll( Arb.From(), Gen.Elements(validLevels).ToArbitrary(), (message, level) => { // 过滤掉包含换行符的消息 var safeMessage = new string(message.Get .Where(c => !char.IsControl(c) && c != '\r' && c != '\n') .ToArray()); if (string.IsNullOrWhiteSpace(safeMessage)) { return true.Label("Skipped: empty after filtering"); } // Arrange var timestamp = DateTime.Now; // Act: 格式化日志 var formatted = LogService.FormatLogEntry(timestamp, level, safeMessage); // 解析日志 var (parsedTimestamp, parsedLevel, parsedMessage) = LogService.ParseLogEntry(formatted); // Assert: 验证解析结果 return (parsedTimestamp != null) .Label("Parsed timestamp should not be null") .And(Math.Abs((parsedTimestamp.Value - timestamp).TotalMilliseconds) < 1) .Label("Timestamp should match within 1ms") .And(parsedLevel == level) .Label("Level should match") .And(parsedMessage == safeMessage) .Label("Message should match"); }); } /// /// Property 13 扩展: 不同级别的日志应写入正确的文件 /// [Property(MaxTest = 50)] public Property ErrorLog_ShouldWriteToErrorFile() { return Prop.ForAll( Arb.From(), (message) => { var safeMessage = new string(message.Get .Where(c => !char.IsControl(c) && c != '\r' && c != '\n') .ToArray()); if (string.IsNullOrWhiteSpace(safeMessage)) { return true.Label("Skipped: empty after filtering"); } // Act: 记录错误日志 _logService.Error(safeMessage); // 检查错误日志文件 var errorLogFile = Path.Combine(_testLogPath, $"error_{DateTime.Now:yyyy-MM-dd}.log"); var appLogFile = Path.Combine(_testLogPath, $"app_{DateTime.Now:yyyy-MM-dd}.log"); // Assert: 错误日志应同时写入 error 和 app 文件 return File.Exists(errorLogFile) .Label("Error log file should exist") .And(File.Exists(appLogFile)) .Label("App log file should exist") .And(File.ReadAllText(errorLogFile).Contains(safeMessage)) .Label("Error log should contain message") .And(File.ReadAllText(appLogFile).Contains(safeMessage)) .Label("App log should contain message"); }); } /// /// Property 13 扩展: 日志文件应按日期分割 /// [Fact] public void LogFiles_ShouldBeSplitByDate() { // Arrange & Act _logService.Info("Test message 1"); _logService.Warn("Test message 2"); _logService.Error("Test message 3"); // Assert var logFiles = _logService.GetLogFiles(); var today = DateTime.Now.ToString("yyyy-MM-dd"); // 应该有 app 和 error 两个日志文件 Assert.Contains(logFiles, f => f.Contains($"app_{today}")); Assert.Contains(logFiles, f => f.Contains($"error_{today}")); } } }