Files
Axi_Omron/tests/AxiOmron.PcbCheck.Tests/CsvBoardRecordRepositoryTests.cs
yunxiao.zhu 27e2c9d5d0 ✨ feat(*): 更新 PLC 相关功能与配置
- 在 appConfig.json 中添加 EndianFormat 配置项并更新相关输入输出地址
- 修改 ResultCode 类型为 short,确保与 PLC 通信一致
- 新增 DesignTimePlcService 以支持设计时的 PLC 测试写入功能
- 更新 SystemSettingViewModel 和 DashboardPage 以支持手动测试写入
- 调整多个视图模型和服务以适应新的 PLC 写入逻辑
- 更新相关单元测试以验证新功能的正确性
2026-06-02 17:26:24 +08:00

208 lines
7.4 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using AxiOmron.PcbCheck.Models;
using AxiOmron.PcbCheck.Options;
using AxiOmron.PcbCheck.Services.Implementations;
using Xunit;
namespace AxiOmron.PcbCheck.Tests;
/// <summary>
/// 验证 CSV 处理记录仓储的持久化、恢复与坏行容错行为。
/// </summary>
public sealed class CsvBoardRecordRepositoryTests : IDisposable
{
private readonly string _tempDirectory;
/// <summary>
/// 初始化测试临时目录。
/// </summary>
public CsvBoardRecordRepositoryTests()
{
_tempDirectory = Path.Combine(Path.GetTempPath(), "AxiOmron.PcbCheck.Tests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(_tempDirectory);
}
/// <summary>
/// 首次追加记录后,应创建按天 CSV,并能从当天文件恢复记录与统计。
/// </summary>
[Fact]
public async Task AppendAsync_ShouldCreateDailyCsv_AndLoadTodayAsync_ShouldRecoverStatistics()
{
var timeProvider = new FakeTimeProvider(new DateTimeOffset(2026, 4, 19, 8, 0, 0, TimeSpan.FromHours(8)));
var repository = new CsvBoardRecordRepository(
new RecordPersistenceOptions
{
DirectoryPath = _tempDirectory,
DayChangeCheckIntervalSeconds = 30
},
timeProvider);
await repository.AppendAsync(
CreateRecord("PCB-001", new DateTimeOffset(2026, 4, 19, 8, 0, 0, TimeSpan.FromHours(8)), (short)WorkflowResultCode.Passed),
CancellationToken.None);
await repository.AppendAsync(
CreateRecord("PCB-002", new DateTimeOffset(2026, 4, 19, 8, 5, 0, TimeSpan.FromHours(8)), (short)WorkflowResultCode.NgReleased),
CancellationToken.None);
BoardRecordLoadResult result = await repository.LoadCurrentDayAsync(CancellationToken.None);
Assert.Equal(2, result.RecentRecords.Count);
Assert.Equal(2, result.Statistics.TotalCount);
Assert.Equal(1, result.Statistics.OkCount);
Assert.Equal(1, result.Statistics.NgCount);
Assert.Equal(new DateOnly(2026, 4, 19), result.Statistics.StatisticDate);
Assert.True(File.Exists(Path.Combine(_tempDirectory, "2026-04-19.csv")));
}
/// <summary>
/// 当 CSV 中存在坏行时,应跳过坏行并继续恢复其余有效记录。
/// </summary>
[Fact]
public async Task LoadCurrentDayAsync_ShouldSkipBrokenRows_AndContinue()
{
var timeProvider = new FakeTimeProvider(new DateTimeOffset(2026, 4, 19, 8, 0, 0, TimeSpan.FromHours(8)));
string filePath = Path.Combine(_tempDirectory, "2026-04-19.csv");
await File.WriteAllTextAsync(
filePath,
"""
StartedAt,CompletedAt,Barcode,ScanTryCount,SftpTryCount,ResultCode,ResultDescription,ReleaseSent,AlarmRaised,ExceptionSummary
broken,row
2026-04-19 08:00:00.000,2026-04-19 08:01:00.000,PCB-001,1,1,10,OK,True,False,
""");
var repository = new CsvBoardRecordRepository(
new RecordPersistenceOptions
{
DirectoryPath = _tempDirectory,
DayChangeCheckIntervalSeconds = 30
},
timeProvider);
BoardRecordLoadResult result = await repository.LoadCurrentDayAsync(CancellationToken.None);
Assert.Single(result.RecentRecords);
Assert.Equal("PCB-001", result.RecentRecords[0].Barcode);
Assert.Equal(1, result.Statistics.TotalCount);
}
/// <summary>
/// 当系统时间跨过 0 点时,应自动切换到新一天数据视图。
/// </summary>
[Fact]
public async Task ReloadCurrentDayIfDateChangedAsync_ShouldSwitchToNewDay_WhenClockMovesPastMidnight()
{
var timeProvider = new FakeTimeProvider(new DateTimeOffset(2026, 4, 19, 23, 59, 50, TimeSpan.FromHours(8)));
var repository = new CsvBoardRecordRepository(
new RecordPersistenceOptions
{
DirectoryPath = _tempDirectory,
DayChangeCheckIntervalSeconds = 1
},
timeProvider);
await repository.AppendAsync(
CreateRecord("PCB-OLD", new DateTimeOffset(2026, 4, 19, 23, 59, 55, TimeSpan.FromHours(8)), (short)WorkflowResultCode.Passed),
CancellationToken.None);
timeProvider.SetLocalNow(new DateTimeOffset(2026, 4, 20, 0, 0, 10, TimeSpan.FromHours(8)));
BoardRecordLoadResult? result = await repository.ReloadCurrentDayIfDateChangedAsync(CancellationToken.None);
Assert.NotNull(result);
Assert.Empty(result.RecentRecords);
Assert.Equal(new DateOnly(2026, 4, 20), result.Statistics.StatisticDate);
Assert.Equal(0, result.Statistics.TotalCount);
}
/// <summary>
/// 释放测试产生的临时目录。
/// </summary>
public void Dispose()
{
if (Directory.Exists(_tempDirectory))
{
Directory.Delete(_tempDirectory, true);
}
}
/// <summary>
/// 创建测试用处理记录。
/// </summary>
/// <param name="barcode">条码。</param>
/// <param name="completedAt">完成时间。</param>
/// <param name="resultCode">结果代码。</param>
/// <returns>测试记录。</returns>
private static BoardProcessRecord CreateRecord(string barcode, DateTimeOffset completedAt, short resultCode)
{
return new BoardProcessRecord
{
StartedAt = completedAt.AddSeconds(-5),
CompletedAt = completedAt,
Barcode = barcode,
ScanTryCount = 1,
SftpTryCount = 1,
ResultCode = resultCode,
ResultDescription = resultCode == (short)WorkflowResultCode.Passed ? "OK" : "NG",
ReleaseSent = true,
AlarmRaised = false,
ExceptionSummary = string.Empty
};
}
/// <summary>
/// 提供可控本地时间的测试时钟。
/// </summary>
private sealed class FakeTimeProvider : TimeProvider
{
private DateTimeOffset _localNow;
private TimeZoneInfo _localTimeZone;
/// <summary>
/// 初始化测试时钟。
/// </summary>
/// <param name="localNow">当前本地时间。</param>
public FakeTimeProvider(DateTimeOffset localNow)
{
_localNow = localNow;
_localTimeZone = TimeZoneInfo.CreateCustomTimeZone(
$"Fake-{localNow.Offset}",
localNow.Offset,
"Fake Local",
"Fake Local");
}
/// <summary>
/// 获取当前 UTC 时间。
/// </summary>
/// <returns>当前 UTC 时间。</returns>
public override DateTimeOffset GetUtcNow()
{
return _localNow.ToUniversalTime();
}
/// <summary>
/// 获取当前本地时区。
/// </summary>
public override TimeZoneInfo LocalTimeZone
{
get
{
return _localTimeZone;
}
}
/// <summary>
/// 设置当前本地时间。
/// </summary>
/// <param name="localNow">新的本地时间。</param>
public void SetLocalNow(DateTimeOffset localNow)
{
_localNow = localNow;
_localTimeZone = TimeZoneInfo.CreateCustomTimeZone(
$"Fake-{localNow.Offset}",
localNow.Offset,
"Fake Local",
"Fake Local");
}
}
}