WorkCamera/client/WorkCameraExport.Tests/LogServicePropertyTests.cs
2026-01-05 23:58:56 +08:00

202 lines
7.7 KiB
C#

using FsCheck;
using FsCheck.Xunit;
using WorkCameraExport.Services;
using Xunit;
namespace WorkCameraExport.Tests
{
/// <summary>
/// LogService 属性测试
/// Feature: work-camera-2.0.1, Property 13: 日志记录
/// Validates: Requirements 12.3
/// </summary>
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
{
// 忽略清理错误
}
}
/// <summary>
/// Property 13: 日志记录
/// For any 用户操作,应在日志文件中记录操作信息,包含时间戳和操作类型。
/// </summary>
[Property(MaxTest = 100)]
public Property LogEntry_ShouldContainTimestampAndLevel()
{
return Prop.ForAll(
Arb.From<NonEmptyString>(),
(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");
});
}
/// <summary>
/// Property 13 扩展: 日志格式化后应可正确解析
/// </summary>
[Property(MaxTest = 100)]
public Property LogFormat_RoundTrip_ShouldPreserveData()
{
var validLevels = new[] { "INFO", "WARN", "ERROR", "DEBUG" };
return Prop.ForAll(
Arb.From<NonEmptyString>(),
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");
});
}
/// <summary>
/// Property 13 扩展: 不同级别的日志应写入正确的文件
/// </summary>
[Property(MaxTest = 50)]
public Property ErrorLog_ShouldWriteToErrorFile()
{
return Prop.ForAll(
Arb.From<NonEmptyString>(),
(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");
});
}
/// <summary>
/// Property 13 扩展: 日志文件应按日期分割
/// </summary>
[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}"));
}
}
}