交运行工位桩:三口契约、运行服务、六态 VM、跨线程切 UI。
采集/PLC/Inspect 均为桩,验收 1–8 可测。不部署现场。
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
|
||||
using VisionHost.Contracts;
|
||||
using VisionHost.Runtime;
|
||||
using Xunit;
|
||||
|
||||
namespace VisionHost.Runtime.Tests;
|
||||
|
||||
public sealed class RunServiceAcceptanceTests
|
||||
{
|
||||
private static (RunService run, StubFrameSource cam, ScriptedConfirm confirm) New(
|
||||
IEnumerable<bool>? start = null,
|
||||
IEnumerable<bool>? stop = null)
|
||||
{
|
||||
var cam = new StubFrameSource();
|
||||
var confirm = new ScriptedConfirm(start, stop);
|
||||
var run = new RunService(cam, cam, new StubInspectService(), new StubPlcIo(), confirm);
|
||||
return (run, cam, confirm);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Disconnected_StartDisabled_NoConfirm()
|
||||
{
|
||||
var (run, cam, confirm) = New();
|
||||
cam.SetConnection(ConnectionState.Disconnected);
|
||||
var s = run.Snapshot();
|
||||
Assert.Equal(RunState.Disconnected, s.RunState);
|
||||
Assert.Equal("断连", s.BadgeText);
|
||||
Assert.False(s.CanStart);
|
||||
Assert.Equal("未连接", s.ConnectionText);
|
||||
Assert.False(run.RequestStart());
|
||||
Assert.Equal(0, confirm.StartPrompts);
|
||||
run.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reconnecting_StartDisabled_NoConfirm()
|
||||
{
|
||||
var (run, cam, confirm) = New();
|
||||
cam.SetConnection(ConnectionState.Reconnecting);
|
||||
Assert.False(run.Snapshot().CanStart);
|
||||
Assert.False(run.RequestStart());
|
||||
Assert.Equal(0, confirm.StartPrompts);
|
||||
run.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Start_CancelKeepsIdle()
|
||||
{
|
||||
var (run, _, confirm) = New(start: new[] { false });
|
||||
Assert.True(run.Snapshot().CanStart);
|
||||
Assert.False(run.RequestStart());
|
||||
Assert.Equal(1, confirm.StartPrompts);
|
||||
Assert.False(run.Snapshot().IsRunning);
|
||||
Assert.Equal(RunState.Idle, run.Snapshot().RunState);
|
||||
run.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Start_Confirm_ClearsCounts_AndRuns()
|
||||
{
|
||||
var (run, _, _) = New(start: new[] { true }, stop: new[] { true });
|
||||
Assert.True(run.RequestStart());
|
||||
await WaitAsync(() => run.Snapshot().TotalCount >= 1);
|
||||
var mid = run.Snapshot();
|
||||
Assert.True(mid.IsRunning);
|
||||
Assert.True(mid.CanStop);
|
||||
Assert.False(mid.CanStart);
|
||||
Assert.True(run.RequestStop());
|
||||
Assert.True(mid.TotalCount >= 1);
|
||||
|
||||
var confirm2 = new ScriptedConfirm(new[] { true }, new[] { true });
|
||||
run.Dispose();
|
||||
var cam = new StubFrameSource();
|
||||
var run2 = new RunService(cam, cam, new StubInspectService(), new StubPlcIo(), confirm2);
|
||||
Assert.True(run2.RequestStart());
|
||||
Assert.Equal(0, run2.Snapshot().OkCount);
|
||||
Assert.Equal(0, run2.Snapshot().NgCount);
|
||||
run2.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Stop_CancelKeepsRunning_ConfirmKeepsCounts()
|
||||
{
|
||||
var confirm = new ScriptedConfirm(start: new[] { true }, stop: new[] { false, true });
|
||||
var cam = new StubFrameSource();
|
||||
using var run = new RunService(cam, cam, new StubInspectService(), new StubPlcIo(), confirm);
|
||||
Assert.True(run.RequestStart());
|
||||
await WaitAsync(() => run.Snapshot().TotalCount >= 2);
|
||||
var before = run.Snapshot().TotalCount;
|
||||
Assert.False(run.RequestStop());
|
||||
Assert.True(run.Snapshot().IsRunning);
|
||||
Assert.Equal(1, confirm.StopPrompts);
|
||||
Assert.True(run.RequestStop());
|
||||
var after = run.Snapshot();
|
||||
Assert.False(after.IsRunning);
|
||||
Assert.True(after.TotalCount >= before);
|
||||
Assert.True(after.CanStart);
|
||||
Assert.False(after.CanStop);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FifthShot_IsNgStub()
|
||||
{
|
||||
var (run, _, _) = New(start: new[] { true }, stop: new[] { true });
|
||||
run.RequestStart();
|
||||
await WaitAsync(() => run.Snapshot().TotalCount >= 5, 5000);
|
||||
var s = run.Snapshot();
|
||||
Assert.True(s.NgCount >= 1);
|
||||
Assert.Contains(s.LastResultText, new[] { "NG STUB", "OK" });
|
||||
await WaitAsync(() => run.Snapshot().LastResultText == "NG STUB" || run.Snapshot().NgCount >= 1);
|
||||
run.RequestStop();
|
||||
run.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LastResult_Dash_WhenNoShot()
|
||||
{
|
||||
var (run, _, _) = New();
|
||||
Assert.Equal("—", run.Snapshot().LastResultText);
|
||||
run.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LastResult_NgWithoutCode_IsNgOnly()
|
||||
{
|
||||
var cam = new StubFrameSource();
|
||||
var inspect = new FixedInspect(new InspectResult(false, null, 1, null));
|
||||
using var run = new RunService(cam, cam, inspect, new StubPlcIo(), new AlwaysConfirm());
|
||||
run.RequestStart();
|
||||
cam.GetType(); // keep
|
||||
// inject via queue by starting grab
|
||||
WaitAsync(() => run.Snapshot().TotalCount >= 1).GetAwaiter().GetResult();
|
||||
Assert.Equal("NG", run.Snapshot().LastResultText);
|
||||
run.RequestStop();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Alarm_WhileConnected_CanStopIfRunning()
|
||||
{
|
||||
var (run, _, _) = New(start: new[] { true });
|
||||
run.RequestStart();
|
||||
run.RaiseAlarm("光源异常");
|
||||
var s = run.Snapshot();
|
||||
Assert.Equal(RunState.Alarm, s.RunState);
|
||||
Assert.Equal("报警", s.BadgeText);
|
||||
Assert.Equal("光源异常", s.AlarmText);
|
||||
Assert.True(s.CanStop);
|
||||
run.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Disconnect_OverridesAlarm_StartDisabled()
|
||||
{
|
||||
var (run, cam, confirm) = New();
|
||||
run.RaiseAlarm("光源异常");
|
||||
cam.SetConnection(ConnectionState.Disconnected);
|
||||
var s = run.Snapshot();
|
||||
Assert.Equal(RunState.Disconnected, s.RunState);
|
||||
Assert.False(s.CanStart);
|
||||
Assert.False(run.RequestStart());
|
||||
Assert.Equal(0, confirm.StartPrompts);
|
||||
run.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Counts_PersistAcrossStop_ResetOnNextStart()
|
||||
{
|
||||
var confirm = new ScriptedConfirm(start: new[] { true, true }, stop: new[] { true });
|
||||
var cam = new StubFrameSource();
|
||||
using var run = new RunService(cam, cam, new StubInspectService(), new StubPlcIo(), confirm);
|
||||
run.RequestStart();
|
||||
WaitAsync(() => run.Snapshot().TotalCount >= 3).GetAwaiter().GetResult();
|
||||
run.RequestStop();
|
||||
var kept = run.Snapshot().TotalCount;
|
||||
Assert.True(kept >= 3);
|
||||
run.RequestStart();
|
||||
Assert.Equal(0, run.Snapshot().TotalCount);
|
||||
}
|
||||
|
||||
private static async Task WaitAsync(Func<bool> pred, int ms = 3000)
|
||||
{
|
||||
var until = DateTime.UtcNow.AddMilliseconds(ms);
|
||||
while (DateTime.UtcNow < until)
|
||||
{
|
||||
if (pred()) return;
|
||||
await Task.Delay(20);
|
||||
}
|
||||
throw new TimeoutException("condition not met");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Changed_FromWorker_PostsToUiDispatch()
|
||||
{
|
||||
var ui = new RecordingDispatch();
|
||||
using var vm = RunViewModel.CreateStub(new AlwaysConfirm(), ui);
|
||||
Assert.True(vm.Service.RequestStart());
|
||||
await WaitAsync(() => vm.TotalCount >= 1);
|
||||
Assert.True(ui.PostCount >= 1);
|
||||
vm.Service.RequestStop();
|
||||
}
|
||||
|
||||
private sealed class RecordingDispatch : IUiDispatch
|
||||
{
|
||||
public int PostCount;
|
||||
|
||||
public void Post(Action action)
|
||||
{
|
||||
PostCount++;
|
||||
action();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FixedInspect : IInspectService
|
||||
{
|
||||
private readonly InspectResult _r;
|
||||
public FixedInspect(InspectResult r) => _r = r;
|
||||
public Task<InspectResult> Inspect(object frameHandle, string recipeId, CancellationToken cancellationToken)
|
||||
=> Task.FromResult(_r);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user