|
@@ -0,0 +1,190 @@
|
|
|
|
|
+using System;
|
|
|
|
|
+using System.Collections.Generic;
|
|
|
|
|
+using System.Linq;
|
|
|
|
|
+using System.Threading;
|
|
|
|
|
+using IvfTl.AutoFocus.Calib;
|
|
|
|
|
+using IvfTl.ControlHost.Debug;
|
|
|
|
|
+using Xunit;
|
|
|
|
|
+
|
|
|
|
|
+namespace IvfTl.ControlHost.Tests
|
|
|
|
|
+{
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Task3.1 标定协作器单测(TDD)。用假 calibrateOne 委托驱动纯状态机,不碰真相机。
|
|
|
|
|
+ /// 后台线程编排用 synchronous=true 选项同步执行做确定性断言。
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ public class CalibrationCoordinatorTests
|
|
|
|
|
+ {
|
|
|
|
|
+ // 合格阈值与现网一致(>1.2)。
|
|
|
|
|
+ const double Threshold = 1.2;
|
|
|
|
|
+
|
|
|
|
|
+ static WellCalib Good(int well) => new WellCalib
|
|
|
|
|
+ {
|
|
|
|
|
+ Well = well, CircleFound = true, PeakRatio = 2.0,
|
|
|
|
|
+ FocusZ = 1000 + well, Exposure = 500 + well, CenterOffsetPct = 1.5 + well, Note = "ok"
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ static WellCalib Weak(int well) => new WellCalib
|
|
|
|
|
+ {
|
|
|
|
|
+ Well = well, CircleFound = true, PeakRatio = 1.0, FocusZ = 7, Note = "弱峰"
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ CalibrationCoordinator New(Func<int, WellCalib> runner)
|
|
|
|
|
+ => new CalibrationCoordinator(runner, Threshold, log: _ => { });
|
|
|
|
|
+
|
|
|
|
|
+ // 1. 全合格 → 每孔 Qualified、completed==total、IsRunning 最终 false。
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public void Start_AllPass_AllQualified()
|
|
|
|
|
+ {
|
|
|
|
|
+ var wells = new List<int> { 1, 2, 3 };
|
|
|
|
|
+ var co = New(Good);
|
|
|
|
|
+ co.Start(wells, synchronous: true);
|
|
|
|
|
+
|
|
|
|
|
+ var p = co.GetProgress();
|
|
|
|
|
+ Assert.False(p.IsRunning);
|
|
|
|
|
+ Assert.Equal(3, p.Total);
|
|
|
|
|
+ Assert.Equal(3, p.Completed);
|
|
|
|
|
+ Assert.Null(p.CurrentWell);
|
|
|
|
|
+ Assert.All(p.Wells, w => Assert.Equal(WellCalibState.Qualified, w.State));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 某孔 PeakRatio<=阈值 → 该孔 FakePeak,其余仍 Qualified。
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public void Start_WeakPeak_MarksFakePeak()
|
|
|
|
|
+ {
|
|
|
|
|
+ var wells = new List<int> { 1, 2, 3 };
|
|
|
|
|
+ var co = New(w => w == 2 ? Weak(w) : Good(w));
|
|
|
|
|
+ co.Start(wells, synchronous: true);
|
|
|
|
|
+
|
|
|
|
|
+ var p = co.GetProgress();
|
|
|
|
|
+ Assert.Equal(WellCalibState.Qualified, p.Wells.Single(x => x.WellSn == 1).State);
|
|
|
|
|
+ Assert.Equal(WellCalibState.FakePeak, p.Wells.Single(x => x.WellSn == 2).State);
|
|
|
|
|
+ Assert.Equal(WellCalibState.Qualified, p.Wells.Single(x => x.WellSn == 3).State);
|
|
|
|
|
+ Assert.Equal(3, p.Completed);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 3. calibrateOne 抛异常的孔 → Failed,不影响后续孔继续。
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public void Start_RunnerThrows_MarksFailed_ContinuesRest()
|
|
|
|
|
+ {
|
|
|
|
|
+ var wells = new List<int> { 1, 2, 3 };
|
|
|
|
|
+ var co = New(w => w == 2 ? throw new InvalidOperationException("boom") : Good(w));
|
|
|
|
|
+ co.Start(wells, synchronous: true);
|
|
|
|
|
+
|
|
|
|
|
+ var p = co.GetProgress();
|
|
|
|
|
+ Assert.Equal(WellCalibState.Qualified, p.Wells.Single(x => x.WellSn == 1).State);
|
|
|
|
|
+ Assert.Equal(WellCalibState.Failed, p.Wells.Single(x => x.WellSn == 2).State);
|
|
|
|
|
+ Assert.Equal(WellCalibState.Qualified, p.Wells.Single(x => x.WellSn == 3).State);
|
|
|
|
|
+ Assert.Equal(3, p.Completed);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 3b. null 结果也 → Failed。
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public void Start_RunnerReturnsNull_MarksFailed()
|
|
|
|
|
+ {
|
|
|
|
|
+ var co = New(w => w == 1 ? null : Good(w));
|
|
|
|
|
+ co.Start(new List<int> { 1, 2 }, synchronous: true);
|
|
|
|
|
+ var p = co.GetProgress();
|
|
|
|
|
+ Assert.Equal(WellCalibState.Failed, p.Wells.Single(x => x.WellSn == 1).State);
|
|
|
|
|
+ Assert.Equal(WellCalibState.Qualified, p.Wells.Single(x => x.WellSn == 2).State);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 4. Stop 后当前孔跑完即停,剩余孔保持 Pending。
|
|
|
|
|
+ // 用后台线程 + 闸门:第1孔进入 runner 时发信号并阻塞,主线程见到后 Stop(),再放行。
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public void Stop_FinishesCurrentWell_LeavesRestPending()
|
|
|
|
|
+ {
|
|
|
|
|
+ var wells = new List<int> { 1, 2, 3, 4 };
|
|
|
|
|
+ var entered = new ManualResetEventSlim(false);
|
|
|
|
|
+ var release = new ManualResetEventSlim(false);
|
|
|
|
|
+ int calls = 0;
|
|
|
|
|
+
|
|
|
|
|
+ var co = New(w =>
|
|
|
|
|
+ {
|
|
|
|
|
+ Interlocked.Increment(ref calls);
|
|
|
|
|
+ entered.Set();
|
|
|
|
|
+ release.Wait(2000);
|
|
|
|
|
+ return Good(w);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ co.Start(wells); // 后台线程异步
|
|
|
|
|
+ Assert.True(entered.Wait(2000), "第1孔应进入 runner");
|
|
|
|
|
+ co.Stop();
|
|
|
|
|
+ release.Set();
|
|
|
|
|
+
|
|
|
|
|
+ // 等后台线程收尾
|
|
|
|
|
+ Assert.True(SpinUntil(() => !co.GetProgress().IsRunning, 2000), "Stop 后应停下");
|
|
|
|
|
+
|
|
|
|
|
+ var p = co.GetProgress();
|
|
|
|
|
+ Assert.Equal(1, calls); // 只跑了第1孔
|
|
|
|
|
+ Assert.Equal(WellCalibState.Qualified, p.Wells.Single(x => x.WellSn == 1).State);
|
|
|
|
|
+ Assert.All(p.Wells.Where(x => x.WellSn != 1),
|
|
|
|
|
+ w => Assert.Equal(WellCalibState.Pending, w.State));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 5. GetProgress 每孔字段来自 WellCalib(focusZ/exposure/peakRatio/centerOffsetPct/note)。
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public void GetProgress_CopiesFieldsFromWellCalib()
|
|
|
|
|
+ {
|
|
|
|
|
+ var co = New(Good);
|
|
|
|
|
+ co.Start(new List<int> { 5 }, synchronous: true);
|
|
|
|
|
+ var w = co.GetProgress().Wells.Single();
|
|
|
|
|
+ Assert.Equal(5, w.WellSn);
|
|
|
|
|
+ Assert.Equal(1005, w.FocusZ);
|
|
|
|
|
+ Assert.Equal(505, w.Exposure);
|
|
|
|
|
+ Assert.Equal(2.0, w.PeakRatio);
|
|
|
|
|
+ Assert.Equal(6.5, w.CenterOffsetPct);
|
|
|
|
|
+ Assert.Equal("ok", w.Note);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 6. Recalibrate 单孔重标:把该孔重置后再跑一次 runner。
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public void Recalibrate_RerunsSingleWell()
|
|
|
|
|
+ {
|
|
|
|
|
+ var map = new Dictionary<int, WellCalib> { [1] = Weak(1), [2] = Good(2) };
|
|
|
|
|
+ var co = New(w => map[w]);
|
|
|
|
|
+ co.Start(new List<int> { 1, 2 }, synchronous: true);
|
|
|
|
|
+ Assert.Equal(WellCalibState.FakePeak, co.GetProgress().Wells.Single(x => x.WellSn == 1).State);
|
|
|
|
|
+
|
|
|
|
|
+ // 第1孔修好了再重标
|
|
|
|
|
+ map[1] = Good(1);
|
|
|
|
|
+ co.Recalibrate(1, synchronous: true);
|
|
|
|
|
+ var p = co.GetProgress();
|
|
|
|
|
+ Assert.Equal(WellCalibState.Qualified, p.Wells.Single(x => x.WellSn == 1).State);
|
|
|
|
|
+ Assert.Equal(WellCalibState.Qualified, p.Wells.Single(x => x.WellSn == 2).State);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 7. 重入保护:已在跑时再 Start 被忽略(busy),不重复跑。
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public void Start_WhileRunning_IsIgnored()
|
|
|
|
|
+ {
|
|
|
|
|
+ var entered = new ManualResetEventSlim(false);
|
|
|
|
|
+ var release = new ManualResetEventSlim(false);
|
|
|
|
|
+ int calls = 0;
|
|
|
|
|
+ var co = New(w =>
|
|
|
|
|
+ {
|
|
|
|
|
+ Interlocked.Increment(ref calls);
|
|
|
|
|
+ entered.Set();
|
|
|
|
|
+ release.Wait(2000);
|
|
|
|
|
+ return Good(w);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ co.Start(new List<int> { 1, 2 });
|
|
|
|
|
+ Assert.True(entered.Wait(2000));
|
|
|
|
|
+ bool second = co.Start(new List<int> { 1, 2 }); // 应被拒
|
|
|
|
|
+ Assert.False(second);
|
|
|
|
|
+ release.Set();
|
|
|
|
|
+ Assert.True(SpinUntil(() => !co.GetProgress().IsRunning, 2000));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ static bool SpinUntil(Func<bool> cond, int timeoutMs)
|
|
|
|
|
+ {
|
|
|
|
|
+ var sw = System.Diagnostics.Stopwatch.StartNew();
|
|
|
|
|
+ while (sw.ElapsedMilliseconds < timeoutMs)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (cond()) return true;
|
|
|
|
|
+ Thread.Sleep(10);
|
|
|
|
|
+ }
|
|
|
|
|
+ return cond();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|