PhotoLayerCalculatorTests.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. using System;
  2. using IvfTl.AutoFocus.Layout;
  3. using Xunit;
  4. namespace IvfTl.AutoFocus.Tests
  5. {
  6. /// <summary>
  7. /// M2-02 PhotoLayerCalculator 单元测试(重建被删单测)。
  8. /// 覆盖 §2.4 层位置公式 + §2.5 就近优先/钳位/异常/边界。
  9. /// 所有期望值均按 PhotoLayerCalculator.cs 当前公式现算,并在注释里写出推导:
  10. /// start = focusZ - LayerDown*spacing; 第 i 层 = start + i*spacing。
  11. /// </summary>
  12. public class PhotoLayerCalculatorTests
  13. {
  14. // 小工具:构造一个已解析好的有效配置。
  15. private static FocusLayerConfig Cfg(int spacing, int count, int down) =>
  16. new FocusLayerConfig
  17. {
  18. LayerSpacingPulse = spacing,
  19. LayerCount = count,
  20. LayerDown = down,
  21. };
  22. // 小工具:构造原始就近优先入参。
  23. private static FocusLayerRawConfig Raw(
  24. int? wellSpacing = null, int? deviceSpacing = null,
  25. int? wellCount = null, int? deviceCount = null,
  26. int? wellDown = null, int? deviceDown = null,
  27. string tlSn = "TL-TEST", int houseSn = 4, int wellSn = 1) =>
  28. new FocusLayerRawConfig
  29. {
  30. TlSn = tlSn,
  31. HouseSn = houseSn,
  32. WellSn = wellSn,
  33. WellSpacingPulse = wellSpacing,
  34. DeviceSpacingPulse = deviceSpacing,
  35. WellLayerCount = wellCount,
  36. DeviceLayerCount = deviceCount,
  37. WellMoveDownLayer = wellDown,
  38. DeviceLayerDown = deviceDown,
  39. };
  40. // ===== ① 层公式:期望值如 [88178,88306,88434,88562,88690] =====
  41. // 推导(按代码公式现算):
  42. // focusZ=88434, down=2, spacing=128, count=5
  43. // start = 88434 - 2*128 = 88434 - 256 = 88178
  44. // 层: 88178, 88178+128=88306, +128=88434, +128=88562, +128=88690
  45. // (清晰层 FocusZ=88434 恰落在第 down=2 层 — 符合设计语义)
  46. [Fact]
  47. public void 层公式_计划样例_应得88178到88690()
  48. {
  49. var cfg = Cfg(spacing: 128, count: 5, down: 2);
  50. int[] pos = PhotoLayerCalculator.ComputeLayerPositions(88434, cfg, pulseMax: 0);
  51. Assert.Equal(new[] { 88178, 88306, 88434, 88562, 88690 }, pos);
  52. // 清晰层(第 down 层)必须 == focusZ
  53. Assert.Equal(88434, pos[2]);
  54. }
  55. // 通用公式校验:任意取值,逐层与 start + i*spacing 对齐。
  56. [Theory]
  57. [InlineData(50000, 200, 7, 3)] // start=50000-3*200=49400
  58. [InlineData(0, 10, 4, 0)] // down=0 → start=focusZ=0
  59. [InlineData(-100, 50, 3, 1)] // 负 focusZ(纯算术,不钳位)→ start=-150
  60. public void 层公式_逐层等于起点加i乘间距(int focusZ, int spacing, int count, int down)
  61. {
  62. var cfg = Cfg(spacing, count, down);
  63. int[] pos = PhotoLayerCalculator.ComputeLayerPositions(focusZ, cfg, pulseMax: 0);
  64. int start = focusZ - down * spacing;
  65. Assert.Equal(count, pos.Length);
  66. for (int i = 0; i < count; i++)
  67. Assert.Equal(start + i * spacing, pos[i]);
  68. // 第 down 层永远等于 focusZ(锚点语义)
  69. if (down < count) Assert.Equal(focusZ, pos[down]);
  70. }
  71. // ===== ② 层间距双空(well 级和设备级都没配)→ FocusConfigMissingException =====
  72. [Fact]
  73. public void 间距双空_应抛FocusConfigMissingException_默认消息为层间距未配置()
  74. {
  75. var raw = Raw(wellSpacing: null, deviceSpacing: null,
  76. deviceCount: 5, deviceDown: 2, tlSn: "TL-9", houseSn: 4, wellSn: 1);
  77. var ex = Assert.Throws<FocusConfigMissingException>(() => PhotoLayerCalculator.Resolve(raw));
  78. Assert.Equal("TL-9", ex.TlSn);
  79. Assert.Equal(4, ex.HouseSn);
  80. Assert.Equal(1, ex.WellSn);
  81. Assert.Contains("层间距未配置", ex.Message); // 默认消息
  82. }
  83. // 层数双空 / 下移双空 也各自抛(带定制消息),验证不会兜底魔法数。
  84. [Fact]
  85. public void 层数双空_应抛异常且消息为层数未配置()
  86. {
  87. var raw = Raw(deviceSpacing: 128, wellCount: null, deviceCount: null, deviceDown: 2);
  88. var ex = Assert.Throws<FocusConfigMissingException>(() => PhotoLayerCalculator.Resolve(raw));
  89. Assert.Contains("层数未配置", ex.Message);
  90. }
  91. [Fact]
  92. public void 下移双空_应抛异常且消息为下移层数未配置()
  93. {
  94. var raw = Raw(deviceSpacing: 128, deviceCount: 5, wellDown: null, deviceDown: null);
  95. var ex = Assert.Throws<FocusConfigMissingException>(() => PhotoLayerCalculator.Resolve(raw));
  96. Assert.Contains("下移层数未配置", ex.Message);
  97. }
  98. // ===== ③ 就近优先:well 级覆盖设备级 =====
  99. [Fact]
  100. public void 就近优先_well级间距覆盖设备级()
  101. {
  102. var raw = Raw(wellSpacing: 200, deviceSpacing: 128, deviceCount: 5, deviceDown: 2);
  103. var cfg = PhotoLayerCalculator.Resolve(raw);
  104. Assert.Equal(200, cfg.LayerSpacingPulse); // well 赢
  105. }
  106. [Fact]
  107. public void 就近优先_well级层数与下移覆盖设备级()
  108. {
  109. var raw = Raw(deviceSpacing: 128,
  110. wellCount: 9, deviceCount: 5,
  111. wellDown: 3, deviceDown: 2);
  112. var cfg = PhotoLayerCalculator.Resolve(raw);
  113. Assert.Equal(9, cfg.LayerCount); // well 赢
  114. Assert.Equal(3, cfg.LayerDown); // well 赢
  115. }
  116. [Fact]
  117. public void 就近优先_well级为空时回退设备级()
  118. {
  119. var raw = Raw(wellSpacing: null, deviceSpacing: 128,
  120. wellCount: null, deviceCount: 5,
  121. wellDown: null, deviceDown: 2);
  122. var cfg = PhotoLayerCalculator.Resolve(raw);
  123. Assert.Equal(128, cfg.LayerSpacingPulse);
  124. Assert.Equal(5, cfg.LayerCount);
  125. Assert.Equal(2, cfg.LayerDown);
  126. }
  127. // well 级下移 = 0 必须被当成"有效覆盖"(0 是合法 down,不能被当 null)。
  128. [Fact]
  129. public void 就近优先_well级下移为0应视为有效覆盖而非回退()
  130. {
  131. var raw = Raw(deviceSpacing: 128, deviceCount: 5,
  132. wellDown: 0, deviceDown: 2);
  133. var cfg = PhotoLayerCalculator.Resolve(raw);
  134. Assert.Equal(0, cfg.LayerDown); // 不应回退成设备级 2
  135. }
  136. // ===== ④ 下移层数 down 覆盖 — 改变起点位置 =====
  137. // down 越大,起点越往下(Z 越小),整组层整体下移。
  138. [Theory]
  139. [InlineData(0)] // start = focusZ
  140. [InlineData(2)] // start = focusZ - 2*spacing
  141. [InlineData(4)] // start = focusZ - 4*spacing (= 末层之外,清晰层落最末)
  142. public void 下移层数_控制起点为focusZ减down乘间距(int down)
  143. {
  144. int focusZ = 90000, spacing = 100, count = 5;
  145. var cfg = Cfg(spacing, count, down);
  146. int[] pos = PhotoLayerCalculator.ComputeLayerPositions(focusZ, cfg, 0);
  147. Assert.Equal(focusZ - down * spacing, pos[0]); // 第0层=起点
  148. }
  149. // ===== ⑤ pulseMax 钳位 =====
  150. // 起点为负 → 第0层钳到 0;末层超上限 → 钳到 pulseMax。
  151. [Fact]
  152. public void 钳位_下越界钳到0_上越界钳到pulseMax()
  153. {
  154. // focusZ=150, down=2, spacing=100 → start=150-200=-50(负,会被钳)
  155. // 原始层: -50, 50, 150, 250, 350 ; pulseMax=300
  156. // 钳后: 0, 50, 150, 250, 300
  157. var cfg = Cfg(spacing: 100, count: 5, down: 2);
  158. int[] pos = PhotoLayerCalculator.ComputeLayerPositions(150, cfg, pulseMax: 300);
  159. Assert.Equal(new[] { 0, 50, 150, 250, 300 }, pos);
  160. }
  161. [Fact]
  162. public void 钳位_pulseMax为0时不钳位允许负值()
  163. {
  164. var cfg = Cfg(spacing: 100, count: 3, down: 2);
  165. // start = 150 - 200 = -50 → 不钳位
  166. int[] pos = PhotoLayerCalculator.ComputeLayerPositions(150, cfg, pulseMax: 0);
  167. Assert.Equal(new[] { -50, 50, 150 }, pos);
  168. }
  169. [Fact]
  170. public void 钳位_全部在范围内时不改变()
  171. {
  172. var cfg = Cfg(spacing: 100, count: 3, down: 1);
  173. // start=88434-100=88334 → 88334,88434,88534 ; pulseMax=100000 不触发
  174. int[] pos = PhotoLayerCalculator.ComputeLayerPositions(88434, cfg, pulseMax: 100000);
  175. Assert.Equal(new[] { 88334, 88434, 88534 }, pos);
  176. }
  177. // ===== ⑥ 边界 / 防御性 =====
  178. [Fact]
  179. public void 边界_单层count1_只返回起点一层()
  180. {
  181. var cfg = Cfg(spacing: 128, count: 1, down: 0);
  182. int[] pos = PhotoLayerCalculator.ComputeLayerPositions(5000, cfg, 0);
  183. Assert.Single(pos);
  184. Assert.Equal(5000, pos[0]);
  185. }
  186. [Fact]
  187. public void 边界_LayerCount为0应抛ArgumentOutOfRange()
  188. {
  189. var cfg = Cfg(spacing: 128, count: 0, down: 0);
  190. Assert.Throws<ArgumentOutOfRangeException>(
  191. () => PhotoLayerCalculator.ComputeLayerPositions(5000, cfg, 0));
  192. }
  193. [Fact]
  194. public void 边界_负LayerCount应抛ArgumentOutOfRange()
  195. {
  196. var cfg = Cfg(spacing: 128, count: -3, down: 0);
  197. Assert.Throws<ArgumentOutOfRangeException>(
  198. () => PhotoLayerCalculator.ComputeLayerPositions(5000, cfg, 0));
  199. }
  200. [Fact]
  201. public void 边界_cfg为null应抛ArgumentNull()
  202. {
  203. Assert.Throws<ArgumentNullException>(
  204. () => PhotoLayerCalculator.ComputeLayerPositions(5000, null, 0));
  205. }
  206. [Fact]
  207. public void 边界_Resolve入参null应抛ArgumentNull()
  208. {
  209. Assert.Throws<ArgumentNullException>(() => PhotoLayerCalculator.Resolve(null));
  210. }
  211. // 可疑边界探针:down >= count(清晰层落在拍摄范围之外)。
  212. // 注:down<count 校验现已下沉到 Resolve(配置非法时抛 FocusConfigMissingException);
  213. // ComputeLayerPositions 作为纯公式仍不校验 down<count(此处直接喂 cfg 绕过 Resolve),
  214. // 这里断言"公式计算不抛、但第 down 层不在返回数组里",把绕过解析时的隐患钉成事实。
  215. [Fact]
  216. public void 可疑边界_down等于count时计算不抛但清晰层不在拍摄层内()
  217. {
  218. // down=5, count=5 → start=focusZ-5*spacing,层 index 0..4,
  219. // 清晰层(第5层)= focusZ 本身,却不在返回的 5 层(0..4)里。
  220. int focusZ = 90000, spacing = 100, count = 5, down = 5;
  221. var cfg = Cfg(spacing, count, down);
  222. int[] pos = PhotoLayerCalculator.ComputeLayerPositions(focusZ, cfg, 0);
  223. Assert.Equal(count, pos.Length);
  224. // 末层 = start + (count-1)*spacing = (focusZ-500)+400 = focusZ-100 < focusZ
  225. Assert.Equal(focusZ - 100, pos[count - 1]);
  226. Assert.DoesNotContain(focusZ, pos); // 清晰层落在拍摄层之外 — 设计隐患佐证
  227. }
  228. }
  229. }