Files
Axi_Omron/tests/AxiOmron.PcbCheck.Tests/WorkflowHostedServiceTests.cs
yunxiao.zhu c67509ec82 ✨ feat(file-check): 添加 PLC 应答后命中文件删除
* 新增默认关闭的删除开关,仅在收到 PLC 放行应答后执行

* 本地与 SFTP Provider 校验根目录并支持幂等删除

* 补充工作流、设置页和文件删除相关测试与文档
2026-08-01 16:42:28 +08:00

834 lines
30 KiB
C#

using AxiOmron.PcbCheck.Models;
using AxiOmron.PcbCheck.Options;
using AxiOmron.PcbCheck.Services.Implementations;
using AxiOmron.PcbCheck.Services.Interfaces;
using Xunit;
namespace AxiOmron.PcbCheck.Tests;
/// <summary>
/// 验证流程后台服务的文件校验启动探活行为。
/// </summary>
public sealed class WorkflowHostedServiceTests
{
/// <summary>
/// 启动探活失败时,不应抛出异常,且应写入运行态状态。
/// </summary>
/// <returns>异步测试任务。</returns>
[Fact]
public async Task ProbeFileCheckOnStartupAsync_ShouldUpdateSnapshot_WhenConnectionFails()
{
var stateStore = new AppStateStore();
var service = new WorkflowHostedService(
new FakePlcService(),
new FakeScannerService(),
new FakeFileCheckService
{
TestOutcome = new FileCheckTestOutcome
{
IsSuccess = false,
IsSystemError = true,
StatusMessage = "启动探活失败"
}
},
new FakeAndonService(),
stateStore,
new AppConfig(),
new FakeAppLogger<WorkflowHostedService>());
await service.ProbeFileCheckOnStartupAsync(CancellationToken.None);
Assert.Equal("异常", stateStore.GetSnapshot().SftpStatus);
}
/// <summary>
/// 启动探活 PLC 成功时,应立即将 PLC 状态更新为已连接。
/// </summary>
/// <returns>异步测试任务。</returns>
[Fact]
public async Task ProbePlcOnStartupAsync_ShouldUpdateSnapshot_WhenConnectionSucceeds()
{
var stateStore = new AppStateStore();
var plcService = new ProbingPlcService();
var service = new WorkflowHostedService(
plcService,
new FakeScannerService(),
new FakeFileCheckService(),
new FakeAndonService(),
stateStore,
new AppConfig(),
new FakeAppLogger<WorkflowHostedService>());
await service.ProbePlcOnStartupAsync(CancellationToken.None);
RuntimeSnapshot snapshot = stateStore.GetSnapshot();
Assert.Equal("已连接", snapshot.PlcStatus);
Assert.True(plcService.ForceReconnectCalled);
Assert.Contains(snapshot.PlcMonitorItems, item => item.Name == "PcbArrived" && item.CurrentValue == "True");
}
/// <summary>
/// 启动探活 PLC 失败时,应将状态更新为连接失败而不是保留默认未连接。
/// </summary>
/// <returns>异步测试任务。</returns>
[Fact]
public async Task ProbePlcOnStartupAsync_ShouldUpdateSnapshot_WhenConnectionFails()
{
var stateStore = new AppStateStore();
var service = new WorkflowHostedService(
new FailingPlcService(),
new FakeScannerService(),
new FakeFileCheckService(),
new FakeAndonService(),
stateStore,
new AppConfig(),
new FakeAppLogger<WorkflowHostedService>());
await service.ProbePlcOnStartupAsync(CancellationToken.None);
Assert.StartsWith("连接失败:", stateStore.GetSnapshot().PlcStatus);
}
/// <summary>
/// 当 PLC 仅提供到位信号时,流程仍应启动,不再依赖就绪、自动模式和工位使能位。
/// </summary>
/// <returns>异步测试任务。</returns>
[Fact]
public async Task ExecuteAsync_ShouldStartWorkflow_WhenOnlyPcbArrivedIsProvided()
{
var stateStore = new AppStateStore();
var plcService = new SequencedPlcService(new[]
{
new PlcSignalSnapshot { PcbArrived = true, PlcAckRelease = true },
new PlcSignalSnapshot { PcbArrived = false, PlcAckRelease = true }
});
var scannerService = new CountingScannerService
{
Result = new ScanOperationResult
{
IsSuccess = true,
DeviceConnected = true,
Barcode = "PCB-001"
}
};
var fileCheckService = new FakeFileCheckService
{
CheckOutcome = new FileCheckOutcome
{
Exists = true,
ConnectionSucceeded = true,
MatchedFilePath = "/pcb/PCB-001.txt"
}
};
var service = new WorkflowHostedService(
plcService,
scannerService,
fileCheckService,
new FakeAndonService(),
stateStore,
new AppConfig
{
Plc = new PlcOptions
{
PollIntervalMs = 20,
ReleaseAckTimeoutMs = 100,
ReleasePulseMs = 10
},
FileCheck = new FileCheckOptions
{
MaxRetryCount = 0
}
},
new FakeAppLogger<WorkflowHostedService>());
await service.StartAsync(CancellationToken.None);
await Task.Delay(200);
await service.StopAsync(CancellationToken.None);
Assert.True(scannerService.TriggerCount > 0);
Assert.Contains(plcService.WrittenStates, state => state.ReleasePermit);
Assert.Contains(plcService.WrittenStates, state => state.ResultCode == (ushort)WorkflowResultCode.Passed);
RuntimeSnapshot snapshot = stateStore.GetSnapshot();
Assert.Contains(snapshot.PlcMonitorItems, item => item.Name == "PcbArrived" && item.CurrentValue == "False");
Assert.Contains(snapshot.PlcMonitorItems, item => item.Name == "PcBusy" && item.CurrentValue == "False");
Assert.Contains(snapshot.PlcMonitorItems, item => item.Name == "ResultCode" && item.CurrentValue == ((ushort)WorkflowResultCode.Passed).ToString());
Assert.All(snapshot.PlcMonitorItems, item => Assert.NotEqual(default, item.LastUpdatedAt));
}
/// <summary>
/// 启用应答后删除功能时,收到 PLC 放行应答后应删除本次精确命中的产品 ID 文件。
/// </summary>
/// <returns>异步测试任务。</returns>
[Fact]
public async Task ExecuteAsync_ShouldDeleteMatchedFile_WhenReleaseAckReceivedAndDeletionEnabled()
{
const string matchedFilePath = "/pcb/PCB-DELETE-001.txt";
var fileCheckService = new FakeFileCheckService
{
CheckOutcome = new FileCheckOutcome
{
Exists = true,
ConnectionSucceeded = true,
MatchedFilePath = matchedFilePath
}
};
var service = new WorkflowHostedService(
new SequencedPlcService(new[]
{
new PlcSignalSnapshot { PcbArrived = true, PlcAckRelease = true },
new PlcSignalSnapshot { PcbArrived = false, PlcAckRelease = true }
}),
new CountingScannerService
{
Result = new ScanOperationResult
{
IsSuccess = true,
DeviceConnected = true,
Barcode = "PCB-DELETE-001"
}
},
fileCheckService,
new FakeAndonService(),
new AppStateStore(),
new AppConfig
{
Plc = new PlcOptions
{
PollIntervalMs = 20,
ReleaseAckTimeoutMs = 100,
ReleasePulseMs = 10
},
FileCheck = new FileCheckOptions
{
MaxRetryCount = 0,
DeleteMatchedFileAfterPlcAck = true
}
},
new FakeAppLogger<WorkflowHostedService>());
await service.StartAsync(CancellationToken.None);
await Task.Delay(200);
await service.StopAsync(CancellationToken.None);
Assert.Equal(matchedFilePath, Assert.Single(fileCheckService.DeletedFilePaths));
}
/// <summary>
/// 未收到 PLC 放行应答时,即使启用删除功能也不得删除产品 ID 文件。
/// </summary>
/// <returns>异步测试任务。</returns>
[Fact]
public async Task ExecuteAsync_ShouldNotDeleteMatchedFile_WhenReleaseAckTimesOut()
{
var fileCheckService = new FakeFileCheckService
{
CheckOutcome = new FileCheckOutcome
{
Exists = true,
ConnectionSucceeded = true,
MatchedFilePath = "/pcb/PCB-KEEP-001.txt"
}
};
var service = new WorkflowHostedService(
new SequencedPlcService(new[]
{
new PlcSignalSnapshot { PcbArrived = true, PlcAckRelease = false },
new PlcSignalSnapshot { PcbArrived = false, PlcAckRelease = false }
}),
new CountingScannerService
{
Result = new ScanOperationResult
{
IsSuccess = true,
DeviceConnected = true,
Barcode = "PCB-KEEP-001"
}
},
fileCheckService,
new FakeAndonService(),
new AppStateStore(),
new AppConfig
{
Plc = new PlcOptions
{
PollIntervalMs = 20,
ReleaseAckTimeoutMs = 50,
ReleasePulseMs = 10
},
FileCheck = new FileCheckOptions
{
MaxRetryCount = 0,
DeleteMatchedFileAfterPlcAck = true
}
},
new FakeAppLogger<WorkflowHostedService>());
await service.StartAsync(CancellationToken.None);
await Task.Delay(200);
await service.StopAsync(CancellationToken.None);
Assert.Empty(fileCheckService.DeletedFilePaths);
}
/// <summary>
/// 删除开关关闭时,即使收到 PLC 放行应答也不得删除产品 ID 文件。
/// </summary>
/// <returns>异步测试任务。</returns>
[Fact]
public async Task ExecuteAsync_ShouldNotDeleteMatchedFile_WhenDeletionDisabled()
{
var fileCheckService = new FakeFileCheckService
{
CheckOutcome = new FileCheckOutcome
{
Exists = true,
ConnectionSucceeded = true,
MatchedFilePath = "/pcb/PCB-KEEP-002.txt"
}
};
var service = new WorkflowHostedService(
new SequencedPlcService(new[]
{
new PlcSignalSnapshot { PcbArrived = true, PlcAckRelease = true },
new PlcSignalSnapshot { PcbArrived = false, PlcAckRelease = true }
}),
new CountingScannerService
{
Result = new ScanOperationResult
{
IsSuccess = true,
DeviceConnected = true,
Barcode = "PCB-KEEP-002"
}
},
fileCheckService,
new FakeAndonService(),
new AppStateStore(),
new AppConfig
{
Plc = new PlcOptions
{
PollIntervalMs = 20,
ReleaseAckTimeoutMs = 100,
ReleasePulseMs = 10
},
FileCheck = new FileCheckOptions
{
MaxRetryCount = 0,
DeleteMatchedFileAfterPlcAck = false
}
},
new FakeAppLogger<WorkflowHostedService>());
await service.StartAsync(CancellationToken.None);
await Task.Delay(200);
await service.StopAsync(CancellationToken.None);
Assert.Empty(fileCheckService.DeletedFilePaths);
}
/// <summary>
/// 双扫码汇总后,应使用汇总条码继续执行统一文件校验。
/// </summary>
/// <returns>异步测试任务。</returns>
[Fact]
public async Task ExecuteAsync_ShouldCheckFileWithMergedBarcode_WhenScannerServiceReturnsMergedBarcode()
{
var stateStore = new AppStateStore();
var plcService = new SequencedPlcService(new[]
{
new PlcSignalSnapshot { PcbArrived = true, PlcAckRelease = true },
new PlcSignalSnapshot { PcbArrived = false, PlcAckRelease = true }
});
var scannerService = new CountingScannerService
{
Result = new ScanOperationResult
{
IsSuccess = true,
DeviceConnected = true,
Barcode = "PCB-MERGED",
RawMessage = "使用扫码枪2"
}
};
var fileCheckService = new FakeFileCheckService
{
CheckOutcome = new FileCheckOutcome
{
Exists = true,
ConnectionSucceeded = true,
MatchedFilePath = "/pcb/PCB-MERGED.txt"
}
};
var service = new WorkflowHostedService(
plcService,
scannerService,
fileCheckService,
new FakeAndonService(),
stateStore,
new AppConfig
{
Plc = new PlcOptions
{
PollIntervalMs = 20,
ReleaseAckTimeoutMs = 100,
ReleasePulseMs = 10
},
FileCheck = new FileCheckOptions
{
MaxRetryCount = 0
}
},
new FakeAppLogger<WorkflowHostedService>());
await service.StartAsync(CancellationToken.None);
await Task.Delay(200);
await service.StopAsync(CancellationToken.None);
Assert.Equal(1, scannerService.TriggerCount);
Assert.Equal("PCB-MERGED", fileCheckService.LastBarcode);
Assert.Contains(plcService.WrittenStates, state => state.ResultCode == (ushort)WorkflowResultCode.Passed);
}
/// <summary>
/// 当到位信号保持高电平时,即使执行软件复位,也不应被视为新的上升沿重复触发流程。
/// </summary>
/// <returns>异步测试任务。</returns>
[Fact]
public async Task ResetAsync_ShouldNotRestartWorkflow_WhenPcbArrivedRemainsHigh()
{
var stateStore = new AppStateStore();
var plcService = new SequencedPlcService(new[]
{
new PlcSignalSnapshot { PcbArrived = true, PlcAckRelease = true },
new PlcSignalSnapshot { PcbArrived = true, PlcAckRelease = true },
new PlcSignalSnapshot { PcbArrived = true, PlcAckRelease = true },
new PlcSignalSnapshot { PcbArrived = true, PlcAckRelease = true },
new PlcSignalSnapshot { PcbArrived = true, PlcAckRelease = true }
});
var scannerService = new CountingScannerService
{
Result = new ScanOperationResult
{
IsSuccess = true,
DeviceConnected = true,
Barcode = "PCB-EDGE-001"
}
};
var service = new WorkflowHostedService(
plcService,
scannerService,
new FakeFileCheckService
{
CheckOutcome = new FileCheckOutcome
{
Exists = true,
ConnectionSucceeded = true,
MatchedFilePath = "/pcb/PCB-EDGE-001.txt"
}
},
new FakeAndonService(),
stateStore,
new AppConfig
{
Plc = new PlcOptions
{
PollIntervalMs = 20,
ReleaseAckTimeoutMs = 100,
ReleasePulseMs = 10
},
FileCheck = new FileCheckOptions
{
MaxRetryCount = 0
}
},
new FakeAppLogger<WorkflowHostedService>());
await service.StartAsync(CancellationToken.None);
await Task.Delay(120);
await service.ResetAsync(CancellationToken.None);
await Task.Delay(120);
await service.StopAsync(CancellationToken.None);
Assert.Equal(1, scannerService.TriggerCount);
}
/// <summary>
/// 应用启动时,应从记录仓储恢复当天最近记录与统计信息。
/// </summary>
/// <returns>异步测试任务。</returns>
[Fact]
public async Task StartAsync_ShouldRestoreTodayRecords_FromRepository()
{
var stateStore = new AppStateStore();
var recordRepository = new FakeBoardRecordRepository
{
LoadResult = new BoardRecordLoadResult
{
RecentRecords =
[
new BoardProcessRecord
{
StartedAt = new DateTimeOffset(2026, 4, 19, 8, 0, 0, TimeSpan.FromHours(8)),
CompletedAt = new DateTimeOffset(2026, 4, 19, 8, 1, 0, TimeSpan.FromHours(8)),
Barcode = "PCB-RESTORED",
ScanTryCount = 1,
SftpTryCount = 1,
ResultCode = (ushort)WorkflowResultCode.Passed,
ResultDescription = "OK",
ReleaseSent = true
}
],
Statistics = new BoardRecordStatistics
{
StatisticDate = new DateOnly(2026, 4, 19),
TotalCount = 1,
OkCount = 1,
NgCount = 0
}
}
};
var service = new WorkflowHostedService(
new FakePlcService(),
new FakeScannerService(),
new FakeFileCheckService(),
new FakeAndonService(),
stateStore,
new AppConfig
{
Plc = new PlcOptions
{
PollIntervalMs = 20
}
},
new FakeAppLogger<WorkflowHostedService>(),
recordRepository);
await service.StartAsync(CancellationToken.None);
await Task.Delay(80);
await service.StopAsync(CancellationToken.None);
RuntimeSnapshot snapshot = stateStore.GetSnapshot();
Assert.Single(snapshot.BoardRecords);
Assert.Equal("PCB-RESTORED", snapshot.BoardRecords[0].Barcode);
Assert.Equal(1, snapshot.TodayProcessCount);
Assert.Equal(1, snapshot.TodayOkCount);
Assert.Equal(0, snapshot.TodayNgCount);
}
/// <summary>
/// 当 CSV 持久化失败时,不应让主流程进入故障态。
/// </summary>
/// <returns>异步测试任务。</returns>
[Fact]
public async Task ExecuteAsync_ShouldNotEnterFault_WhenCsvAppendFails()
{
var stateStore = new AppStateStore();
var plcService = new SequencedPlcService(new[]
{
new PlcSignalSnapshot { PcbArrived = true, PlcAckRelease = true },
new PlcSignalSnapshot { PcbArrived = false, PlcAckRelease = true }
});
var scannerService = new CountingScannerService
{
Result = new ScanOperationResult
{
IsSuccess = true,
DeviceConnected = true,
Barcode = "PCB-CSV-FAIL"
}
};
var recordRepository = new FakeBoardRecordRepository
{
AppendException = new IOException("disk full")
};
var service = new WorkflowHostedService(
plcService,
scannerService,
new FakeFileCheckService
{
CheckOutcome = new FileCheckOutcome
{
Exists = true,
ConnectionSucceeded = true,
MatchedFilePath = "/pcb/PCB-CSV-FAIL.txt"
}
},
new FakeAndonService(),
stateStore,
new AppConfig
{
Plc = new PlcOptions
{
PollIntervalMs = 20,
ReleaseAckTimeoutMs = 100,
ReleasePulseMs = 10
},
FileCheck = new FileCheckOptions
{
MaxRetryCount = 0
}
},
new FakeAppLogger<WorkflowHostedService>(),
recordRepository);
await service.StartAsync(CancellationToken.None);
await Task.Delay(200);
await service.StopAsync(CancellationToken.None);
RuntimeSnapshot snapshot = stateStore.GetSnapshot();
Assert.NotEqual(WorkflowState.Faulted, snapshot.WorkflowState);
Assert.Equal((ushort)WorkflowResultCode.Passed, snapshot.ResultCode);
}
private sealed class FakePlcService : IPlcService
{
public Task ForceReconnectAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task<PlcMonitorSnapshot> ReadMonitorSnapshotAsync(CancellationToken cancellationToken) => Task.FromResult(new PlcMonitorSnapshot());
public Task<PlcSignalSnapshot> ReadSignalsAsync(CancellationToken cancellationToken) => Task.FromResult(new PlcSignalSnapshot());
public Task WriteStateAsync(PlcProcessState state, CancellationToken cancellationToken) => Task.CompletedTask;
public Task<PlcWriteResult> WriteRawValueAsync(string address, PlcWriteType type, string value, CancellationToken cancellationToken) => Task.FromResult(PlcWriteResult.Success(address, type, value));
}
private sealed class ProbingPlcService : IPlcService
{
public bool ForceReconnectCalled { get; private set; }
public Task ForceReconnectAsync(CancellationToken cancellationToken)
{
ForceReconnectCalled = true;
return Task.CompletedTask;
}
public Task<PlcMonitorSnapshot> ReadMonitorSnapshotAsync(CancellationToken cancellationToken)
{
return Task.FromResult(new PlcMonitorSnapshot
{
Inputs = new PlcSignalSnapshot
{
PcbArrived = true,
PlcReset = false,
PlcAckRelease = false,
CapturedAt = DateTimeOffset.Now
},
Outputs = new PlcProcessState
{
PcBusy = false,
ReleasePermit = false,
ResultCode = 0
},
CapturedAt = DateTimeOffset.Now
});
}
public Task<PlcSignalSnapshot> ReadSignalsAsync(CancellationToken cancellationToken)
{
return Task.FromResult(new PlcSignalSnapshot());
}
public Task WriteStateAsync(PlcProcessState state, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public Task<PlcWriteResult> WriteRawValueAsync(string address, PlcWriteType type, string value, CancellationToken cancellationToken)
{
return Task.FromResult(PlcWriteResult.Success(address, type, value));
}
}
private sealed class FailingPlcService : IPlcService
{
public Task ForceReconnectAsync(CancellationToken cancellationToken)
{
throw new InvalidOperationException("PLC unreachable");
}
public Task<PlcMonitorSnapshot> ReadMonitorSnapshotAsync(CancellationToken cancellationToken)
{
throw new InvalidOperationException("PLC unreachable");
}
public Task<PlcSignalSnapshot> ReadSignalsAsync(CancellationToken cancellationToken)
{
throw new InvalidOperationException("PLC unreachable");
}
public Task WriteStateAsync(PlcProcessState state, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public Task<PlcWriteResult> WriteRawValueAsync(string address, PlcWriteType type, string value, CancellationToken cancellationToken)
{
throw new InvalidOperationException("PLC unreachable");
}
}
private sealed class SequencedPlcService : IPlcService
{
private readonly PlcSignalSnapshot[] _signals;
private int _index;
public SequencedPlcService(PlcSignalSnapshot[] signals)
{
_signals = signals;
}
public List<PlcProcessState> WrittenStates { get; } = new();
public Task ForceReconnectAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task<PlcMonitorSnapshot> ReadMonitorSnapshotAsync(CancellationToken cancellationToken)
{
PlcSignalSnapshot current = _signals[Math.Min(Math.Max(_index - 1, 0), _signals.Length - 1)];
PlcProcessState lastWritten = WrittenStates.Count > 0 ? WrittenStates[^1] : new PlcProcessState();
return Task.FromResult(new PlcMonitorSnapshot
{
Inputs = new PlcSignalSnapshot
{
PcbArrived = current.PcbArrived,
PlcReset = current.PlcReset,
PlcAckRelease = current.PlcAckRelease,
CapturedAt = DateTimeOffset.Now
},
Outputs = lastWritten.Clone(),
CapturedAt = DateTimeOffset.Now
});
}
public Task<PlcSignalSnapshot> ReadSignalsAsync(CancellationToken cancellationToken)
{
var current = _signals[Math.Min(_index, _signals.Length - 1)];
_index++;
return Task.FromResult(current);
}
public Task WriteStateAsync(PlcProcessState state, CancellationToken cancellationToken)
{
WrittenStates.Add(state.Clone());
return Task.CompletedTask;
}
public Task<PlcWriteResult> WriteRawValueAsync(string address, PlcWriteType type, string value, CancellationToken cancellationToken)
{
return Task.FromResult(PlcWriteResult.Success(address, type, value));
}
}
private sealed class FakeScannerService : IScannerService
{
public Task ForceReconnectAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task<bool> TestConnectionAsync(CancellationToken cancellationToken) => Task.FromResult(true);
public Task<ScanOperationResult> TriggerScanAsync(CancellationToken cancellationToken) => Task.FromResult(new ScanOperationResult());
}
private sealed class CountingScannerService : IScannerService
{
public int TriggerCount { get; private set; }
public ScanOperationResult Result { get; set; } = new();
public Task ForceReconnectAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task<bool> TestConnectionAsync(CancellationToken cancellationToken) => Task.FromResult(true);
public Task<ScanOperationResult> TriggerScanAsync(CancellationToken cancellationToken)
{
TriggerCount++;
return Task.FromResult(Result);
}
}
private sealed class FakeFileCheckService : IFileCheckService
{
public FileCheckTestOutcome TestOutcome { get; set; } = new();
public FileCheckOutcome CheckOutcome { get; set; } = new();
public FileDeleteOutcome DeleteOutcome { get; set; } = new()
{
IsSuccess = true
};
public string LastBarcode { get; private set; } = string.Empty;
public List<string> DeletedFilePaths { get; } = new();
public Task<FileCheckOutcome> CheckFileAsync(string barcode, CancellationToken cancellationToken)
{
LastBarcode = barcode;
return Task.FromResult(CheckOutcome);
}
public Task<FileDeleteOutcome> DeleteMatchedFileAsync(string filePath, CancellationToken cancellationToken)
{
DeletedFilePaths.Add(filePath);
DeleteOutcome.FilePath = filePath;
return Task.FromResult(DeleteOutcome);
}
public Task<FileCheckTestOutcome> TestConnectionAsync(AppConfig? config, CancellationToken cancellationToken)
{
return Task.FromResult(TestOutcome);
}
}
private sealed class FakeAndonService : IAndonService
{
public Task<AndonOperationResult> RaiseAlarmAsync(AndonAlarmRequest request, CancellationToken cancellationToken)
=> Task.FromResult(new AndonOperationResult());
public Task<AndonOperationResult> TestAsync(CancellationToken cancellationToken)
=> Task.FromResult(new AndonOperationResult());
}
private sealed class FakeBoardRecordRepository : IBoardRecordRepository
{
public BoardRecordLoadResult LoadResult { get; set; } = new();
public BoardRecordAppendResult AppendResult { get; set; } = new()
{
IsPersisted = true,
Statistics = new BoardRecordStatistics
{
StatisticDate = DateOnly.FromDateTime(DateTime.Today)
}
};
public Exception? AppendException { get; set; }
public Task<BoardRecordLoadResult> LoadCurrentDayAsync(CancellationToken cancellationToken)
{
return Task.FromResult(LoadResult);
}
public Task<BoardRecordLoadResult?> ReloadCurrentDayIfDateChangedAsync(CancellationToken cancellationToken)
{
return Task.FromResult<BoardRecordLoadResult?>(null);
}
public Task<BoardRecordAppendResult> AppendAsync(BoardProcessRecord record, CancellationToken cancellationToken)
{
if (AppendException is not null)
{
throw AppendException;
}
return Task.FromResult(AppendResult);
}
}
private sealed class FakeAppLogger<TCategoryName> : IAppLogger<TCategoryName>
{
public void LogError(string message, bool showInUi = false, params object?[] args) { }
public void LogError(Exception exception, string message, bool showInUi = false, params object?[] args) { }
public void LogInformation(string message, bool showInUi = false, params object?[] args) { }
public void LogWarning(string message, bool showInUi = false, params object?[] args) { }
public void LogWarning(Exception exception, string message, bool showInUi = false, params object?[] args) { }
}
}