202 lines
7.7 KiB
C#
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}"));
|
|
}
|
|
}
|
|
}
|