#0039 全局数据结构设计
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FsCheck;
|
||||
using XplorePlane.Models;
|
||||
|
||||
namespace XplorePlane.Tests.Generators
|
||||
{
|
||||
/// <summary>
|
||||
/// FsCheck 自定义生成器:为配方相关模型定义 Arbitrary 生成器。
|
||||
/// 使用 Arb.Register<RecipeGenerators>() 注册。
|
||||
/// </summary>
|
||||
public class RecipeGenerators
|
||||
{
|
||||
// ── PipelineModel: class with settable properties ──
|
||||
|
||||
public static Arbitrary<PipelineModel> PipelineModelArb()
|
||||
{
|
||||
var gen = from id in Arb.Default.Guid().Generator
|
||||
from name in Arb.Default.NonEmptyString().Generator
|
||||
from deviceId in Arb.Default.NonEmptyString().Generator
|
||||
from ticks in Gen.Choose(0, int.MaxValue)
|
||||
let createdAt = DateTime.MinValue.AddTicks((long)ticks * 10000)
|
||||
let updatedAt = createdAt
|
||||
select new PipelineModel
|
||||
{
|
||||
Id = id,
|
||||
Name = name.Get,
|
||||
DeviceId = deviceId.Get,
|
||||
CreatedAt = createdAt,
|
||||
UpdatedAt = updatedAt,
|
||||
Nodes = new List<PipelineNodeModel>()
|
||||
};
|
||||
return Arb.From(gen);
|
||||
}
|
||||
|
||||
// ── RecipeStep: reuses StateGenerators for hardware states ──
|
||||
|
||||
public static Arbitrary<RecipeStep> RecipeStepArb()
|
||||
{
|
||||
var gen = from stepIndex in Gen.Choose(0, 100)
|
||||
from motion in StateGenerators.MotionStateArb().Generator
|
||||
from ray in StateGenerators.RaySourceStateArb().Generator
|
||||
from detector in StateGenerators.DetectorStateArb().Generator
|
||||
from pipeline in PipelineModelArb().Generator
|
||||
select new RecipeStep(stepIndex, motion, ray, detector, pipeline);
|
||||
return Arb.From(gen);
|
||||
}
|
||||
|
||||
// ── InspectionRecipe: 0-10 steps with sequential StepIndex ──
|
||||
|
||||
public static Arbitrary<InspectionRecipe> InspectionRecipeArb()
|
||||
{
|
||||
var gen = from id in Arb.Default.Guid().Generator
|
||||
from name in Arb.Default.NonEmptyString().Generator
|
||||
from ticks in Gen.Choose(0, int.MaxValue)
|
||||
let createdAt = DateTime.MinValue.AddTicks((long)ticks * 10000)
|
||||
let updatedAt = createdAt
|
||||
from stepCount in Gen.Choose(0, 10)
|
||||
from steps in GenSequentialSteps(stepCount)
|
||||
select new InspectionRecipe(
|
||||
id,
|
||||
name.Get,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
steps.AsReadOnly());
|
||||
return Arb.From(gen);
|
||||
}
|
||||
|
||||
// ── RecipeExecutionState ──
|
||||
|
||||
public static Arbitrary<RecipeExecutionState> RecipeExecutionStateArb()
|
||||
{
|
||||
var gen = from totalSteps in Gen.Choose(0, 50)
|
||||
from currentStep in Gen.Choose(0, Math.Max(totalSteps, 1) - 1)
|
||||
from status in Gen.Elements(
|
||||
RecipeExecutionStatus.Idle,
|
||||
RecipeExecutionStatus.Running,
|
||||
RecipeExecutionStatus.Paused,
|
||||
RecipeExecutionStatus.Completed,
|
||||
RecipeExecutionStatus.Error)
|
||||
from recipeName in Arb.Default.NonEmptyString().Generator
|
||||
select new RecipeExecutionState(currentStep, totalSteps, status, recipeName.Get);
|
||||
return Arb.From(gen);
|
||||
}
|
||||
|
||||
// ── Helper: generate a list of steps with sequential StepIndex 0..n-1 ──
|
||||
|
||||
private static Gen<List<RecipeStep>> GenSequentialSteps(int count)
|
||||
{
|
||||
if (count == 0)
|
||||
return Gen.Constant(new List<RecipeStep>());
|
||||
|
||||
return Gen.Sequence(
|
||||
Enumerable.Range(0, count).Select(i =>
|
||||
from motion in StateGenerators.MotionStateArb().Generator
|
||||
from ray in StateGenerators.RaySourceStateArb().Generator
|
||||
from detector in StateGenerators.DetectorStateArb().Generator
|
||||
from pipeline in PipelineModelArb().Generator
|
||||
select new RecipeStep(i, motion, ray, detector, pipeline)
|
||||
)
|
||||
).Select(steps => steps.ToList());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
using System;
|
||||
using FsCheck;
|
||||
using XplorePlane.Models;
|
||||
|
||||
namespace XplorePlane.Tests.Generators
|
||||
{
|
||||
/// <summary>
|
||||
/// FsCheck 自定义生成器:为所有状态模型定义 Arbitrary 生成器。
|
||||
/// 使用 Arb.Register<StateGenerators>() 注册。
|
||||
/// </summary>
|
||||
public class StateGenerators
|
||||
{
|
||||
// ── 位置范围: -10000 ~ 10000, 速度范围: 0 ~ 1000 ──
|
||||
|
||||
private static Gen<double> PositionGen =>
|
||||
Gen.Choose(-10000000, 10000000).Select(i => i / 1000.0);
|
||||
|
||||
private static Gen<double> SpeedGen =>
|
||||
Gen.Choose(0, 1000000).Select(i => i / 1000.0);
|
||||
|
||||
private static Gen<double> VoltageGen =>
|
||||
Gen.Choose(0, 450000).Select(i => i / 1000.0);
|
||||
|
||||
private static Gen<double> PowerGen =>
|
||||
Gen.Choose(0, 3000000).Select(i => i / 1000.0);
|
||||
|
||||
private static Gen<double> FrameRateGen =>
|
||||
Gen.Choose(0, 120000).Select(i => i / 1000.0);
|
||||
|
||||
private static Gen<double> MatrixGen =>
|
||||
Gen.Choose(-10000000, 10000000).Select(i => i / 1000.0);
|
||||
|
||||
// ── MotionState: 6 positions + 6 speeds ──
|
||||
|
||||
public static Arbitrary<MotionState> MotionStateArb()
|
||||
{
|
||||
var gen = from xm in PositionGen
|
||||
from ym in PositionGen
|
||||
from zt in PositionGen
|
||||
from zd in PositionGen
|
||||
from tiltD in PositionGen
|
||||
from dist in PositionGen
|
||||
from xmSpd in SpeedGen
|
||||
from ymSpd in SpeedGen
|
||||
from ztSpd in SpeedGen
|
||||
from zdSpd in SpeedGen
|
||||
from tiltDSpd in SpeedGen
|
||||
from distSpd in SpeedGen
|
||||
select new MotionState(xm, ym, zt, zd, tiltD, dist,
|
||||
xmSpd, ymSpd, ztSpd, zdSpd, tiltDSpd, distSpd);
|
||||
return Arb.From(gen);
|
||||
}
|
||||
|
||||
// ── RaySourceState: bool + 2 doubles ──
|
||||
|
||||
public static Arbitrary<RaySourceState> RaySourceStateArb()
|
||||
{
|
||||
var gen = from isOn in Arb.Default.Bool().Generator
|
||||
from voltage in VoltageGen
|
||||
from power in PowerGen
|
||||
select new RaySourceState(isOn, voltage, power);
|
||||
return Arb.From(gen);
|
||||
}
|
||||
|
||||
// ── DetectorState: 2 bools + double + string ──
|
||||
|
||||
public static Arbitrary<DetectorState> DetectorStateArb()
|
||||
{
|
||||
var gen = from isConnected in Arb.Default.Bool().Generator
|
||||
from isAcquiring in Arb.Default.Bool().Generator
|
||||
from frameRate in FrameRateGen
|
||||
from res in Gen.Elements("1024x1024", "2048x2048", "2880x2880", "3072x3072", "4260x4260")
|
||||
select new DetectorState(isConnected, isAcquiring, frameRate, res);
|
||||
return Arb.From(gen);
|
||||
}
|
||||
|
||||
// ── SystemState: OperationMode enum + bool + string ──
|
||||
|
||||
public static Arbitrary<SystemState> SystemStateArb()
|
||||
{
|
||||
var gen = from mode in Gen.Elements(
|
||||
OperationMode.Idle,
|
||||
OperationMode.Scanning,
|
||||
OperationMode.CTAcquire,
|
||||
OperationMode.RecipeRun)
|
||||
from hasError in Arb.Default.Bool().Generator
|
||||
from msg in Arb.Default.NonEmptyString().Generator
|
||||
let errorMessage = hasError ? msg.Get : string.Empty
|
||||
select new SystemState(mode, hasError, errorMessage);
|
||||
return Arb.From(gen);
|
||||
}
|
||||
|
||||
// ── CameraState: 2 bools + null CurrentFrame + 2 ints + double ──
|
||||
|
||||
public static Arbitrary<CameraState> CameraStateArb()
|
||||
{
|
||||
var gen = from isConnected in Arb.Default.Bool().Generator
|
||||
from isStreaming in Arb.Default.Bool().Generator
|
||||
from width in Gen.Choose(0, 8192)
|
||||
from height in Gen.Choose(0, 8192)
|
||||
from frameRate in FrameRateGen
|
||||
select new CameraState(isConnected, isStreaming, null, width, height, frameRate);
|
||||
return Arb.From(gen);
|
||||
}
|
||||
|
||||
// ── CalibrationMatrix: 9 doubles ──
|
||||
|
||||
public static Arbitrary<CalibrationMatrix> CalibrationMatrixArb()
|
||||
{
|
||||
var gen = from m11 in MatrixGen
|
||||
from m12 in MatrixGen
|
||||
from m13 in MatrixGen
|
||||
from m21 in MatrixGen
|
||||
from m22 in MatrixGen
|
||||
from m23 in MatrixGen
|
||||
from m31 in MatrixGen
|
||||
from m32 in MatrixGen
|
||||
from m33 in MatrixGen
|
||||
select new CalibrationMatrix(m11, m12, m13, m21, m22, m23, m31, m32, m33);
|
||||
return Arb.From(gen);
|
||||
}
|
||||
|
||||
// ── PhysicalPosition: 3 doubles ──
|
||||
|
||||
public static Arbitrary<PhysicalPosition> PhysicalPositionArb()
|
||||
{
|
||||
var gen = from x in PositionGen
|
||||
from y in PositionGen
|
||||
from z in PositionGen
|
||||
select new PhysicalPosition(x, y, z);
|
||||
return Arb.From(gen);
|
||||
}
|
||||
|
||||
// ── LinkedViewState: PhysicalPosition + bool + DateTime ──
|
||||
|
||||
public static Arbitrary<LinkedViewState> LinkedViewStateArb()
|
||||
{
|
||||
var gen = from pos in PhysicalPositionArb().Generator
|
||||
from isExecuting in Arb.Default.Bool().Generator
|
||||
from ticks in Gen.Choose(0, int.MaxValue)
|
||||
let dt = DateTime.MinValue.AddTicks((long)ticks * 10000)
|
||||
select new LinkedViewState(pos, isExecuting, dt);
|
||||
return Arb.From(gen);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
using XplorePlane.Models;
|
||||
|
||||
namespace XplorePlane.Tests.Models
|
||||
{
|
||||
public class StateModelsTests
|
||||
{
|
||||
// ── Default Value Tests ───────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void MotionState_Default_AllZeros()
|
||||
{
|
||||
var state = MotionState.Default;
|
||||
|
||||
Assert.Equal(0, state.XM);
|
||||
Assert.Equal(0, state.YM);
|
||||
Assert.Equal(0, state.ZT);
|
||||
Assert.Equal(0, state.ZD);
|
||||
Assert.Equal(0, state.TiltD);
|
||||
Assert.Equal(0, state.Dist);
|
||||
Assert.Equal(0, state.XMSpeed);
|
||||
Assert.Equal(0, state.YMSpeed);
|
||||
Assert.Equal(0, state.ZTSpeed);
|
||||
Assert.Equal(0, state.ZDSpeed);
|
||||
Assert.Equal(0, state.TiltDSpeed);
|
||||
Assert.Equal(0, state.DistSpeed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RaySourceState_Default_IsOffAndZeros()
|
||||
{
|
||||
var state = RaySourceState.Default;
|
||||
|
||||
Assert.False(state.IsOn);
|
||||
Assert.Equal(0, state.Voltage);
|
||||
Assert.Equal(0, state.Power);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetectorState_Default_DisconnectedAndZeros()
|
||||
{
|
||||
var state = DetectorState.Default;
|
||||
|
||||
Assert.False(state.IsConnected);
|
||||
Assert.False(state.IsAcquiring);
|
||||
Assert.Equal(0, state.FrameRate);
|
||||
Assert.Equal(string.Empty, state.Resolution);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SystemState_Default_IdleNoError()
|
||||
{
|
||||
var state = SystemState.Default;
|
||||
|
||||
Assert.Equal(OperationMode.Idle, state.OperationMode);
|
||||
Assert.False(state.HasError);
|
||||
Assert.Equal(string.Empty, state.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CameraState_Default_DisconnectedAndZeros()
|
||||
{
|
||||
var state = CameraState.Default;
|
||||
|
||||
Assert.False(state.IsConnected);
|
||||
Assert.False(state.IsStreaming);
|
||||
Assert.Null(state.CurrentFrame);
|
||||
Assert.Equal(0, state.Width);
|
||||
Assert.Equal(0, state.Height);
|
||||
Assert.Equal(0, state.FrameRate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LinkedViewState_Default_ZeroPositionNotExecuting()
|
||||
{
|
||||
var state = LinkedViewState.Default;
|
||||
|
||||
Assert.Equal(0, state.TargetPosition.X);
|
||||
Assert.Equal(0, state.TargetPosition.Y);
|
||||
Assert.Equal(0, state.TargetPosition.Z);
|
||||
Assert.False(state.IsExecuting);
|
||||
Assert.Equal(DateTime.MinValue, state.LastRequestTime);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RecipeExecutionState_Default_IdleAndZeros()
|
||||
{
|
||||
var state = RecipeExecutionState.Default;
|
||||
|
||||
Assert.Equal(0, state.CurrentStepIndex);
|
||||
Assert.Equal(0, state.TotalSteps);
|
||||
Assert.Equal(RecipeExecutionStatus.Idle, state.Status);
|
||||
Assert.Equal(string.Empty, state.CurrentRecipeName);
|
||||
}
|
||||
|
||||
// ── Immutability Test ─────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void MotionState_WithExpression_ProducesNewInstance()
|
||||
{
|
||||
var original = MotionState.Default;
|
||||
|
||||
var modified = original with { XM = 100 };
|
||||
|
||||
// New instance is different from original
|
||||
Assert.NotSame(original, modified);
|
||||
Assert.Equal(100, modified.XM);
|
||||
|
||||
// Original is unchanged
|
||||
Assert.Equal(0, original.XM);
|
||||
}
|
||||
|
||||
// ── CalibrationMatrix Transform Tests ─────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void CalibrationMatrix_Transform_CorrectCalculation()
|
||||
{
|
||||
// Known matrix: scaling X by 2, Y by 3, with offsets
|
||||
var matrix = new CalibrationMatrix(
|
||||
M11: 2, M12: 0, M13: 10,
|
||||
M21: 0, M22: 3, M23: 20,
|
||||
M31: 0, M32: 0, M33: 0
|
||||
);
|
||||
|
||||
var (x, y, z) = matrix.Transform(pixelX: 5, pixelY: 8);
|
||||
|
||||
// x = 2*5 + 0*8 + 10 = 20
|
||||
Assert.Equal(20, x);
|
||||
// y = 0*5 + 3*8 + 20 = 44
|
||||
Assert.Equal(44, y);
|
||||
// z = 0*5 + 0*8 + 0 = 0
|
||||
Assert.Equal(0, z);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalibrationMatrix_Transform_IdentityMatrix()
|
||||
{
|
||||
// Identity-like matrix: should return (pixelX, pixelY, 0) approximately
|
||||
var identity = new CalibrationMatrix(
|
||||
M11: 1, M12: 0, M13: 0,
|
||||
M21: 0, M22: 1, M23: 0,
|
||||
M31: 0, M32: 0, M33: 0
|
||||
);
|
||||
|
||||
var (x, y, z) = identity.Transform(pixelX: 42.5, pixelY: 99.1);
|
||||
|
||||
Assert.Equal(42.5, x, precision: 10);
|
||||
Assert.Equal(99.1, y, precision: 10);
|
||||
Assert.Equal(0, z, precision: 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using Moq;
|
||||
using Serilog;
|
||||
using Xunit;
|
||||
using XP.Hardware.RaySource.Services;
|
||||
using XplorePlane.Models;
|
||||
using XplorePlane.Services.AppState;
|
||||
|
||||
namespace XplorePlane.Tests.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// AppStateService 单元测试。
|
||||
/// 验证默认状态值、Dispose 后行为、null 参数校验、CalibrationMatrix 缺失时的错误处理。
|
||||
/// </summary>
|
||||
public class AppStateServiceTests : IDisposable
|
||||
{
|
||||
private readonly AppStateService _service;
|
||||
private readonly Mock<IRaySourceService> _mockRaySource;
|
||||
private readonly Mock<ILogger> _mockLogger;
|
||||
|
||||
public AppStateServiceTests()
|
||||
{
|
||||
// Ensure WPF Application exists for Dispatcher
|
||||
if (Application.Current == null)
|
||||
{
|
||||
new Application();
|
||||
}
|
||||
|
||||
_mockRaySource = new Mock<IRaySourceService>();
|
||||
_mockLogger = new Mock<ILogger>();
|
||||
_service = new AppStateService(_mockRaySource.Object, _mockLogger.Object);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_service.Dispose();
|
||||
}
|
||||
|
||||
// ── 默认状态值验证 ──
|
||||
|
||||
[Fact]
|
||||
public void DefaultState_MotionState_IsDefault()
|
||||
{
|
||||
Assert.Same(MotionState.Default, _service.MotionState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultState_RaySourceState_IsDefault()
|
||||
{
|
||||
Assert.Same(RaySourceState.Default, _service.RaySourceState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultState_SystemState_IsDefault()
|
||||
{
|
||||
Assert.Same(SystemState.Default, _service.SystemState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultState_CalibrationMatrix_IsNull()
|
||||
{
|
||||
Assert.Null(_service.CalibrationMatrix);
|
||||
}
|
||||
|
||||
// ── null 参数抛出 ArgumentNullException ──
|
||||
|
||||
[Fact]
|
||||
public void UpdateMotionState_NullArgument_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => _service.UpdateMotionState(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateRaySourceState_NullArgument_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => _service.UpdateRaySourceState(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateDetectorState_NullArgument_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => _service.UpdateDetectorState(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateSystemState_NullArgument_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => _service.UpdateSystemState(null!));
|
||||
}
|
||||
|
||||
// ── Dispose 后 Update 被忽略 ──
|
||||
|
||||
[Fact]
|
||||
public void Dispose_ThenUpdate_IsIgnored()
|
||||
{
|
||||
var originalState = _service.MotionState;
|
||||
_service.Dispose();
|
||||
|
||||
// Should not throw, and state should remain unchanged
|
||||
var newState = new MotionState(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12);
|
||||
_service.UpdateMotionState(newState);
|
||||
|
||||
Assert.Same(originalState, _service.MotionState);
|
||||
}
|
||||
|
||||
// ── CalibrationMatrix 为 null 时 RequestLinkedView 设置错误状态 ──
|
||||
|
||||
[Fact]
|
||||
public void RequestLinkedView_NoCalibrationMatrix_SetsErrorState()
|
||||
{
|
||||
// CalibrationMatrix is null by default
|
||||
Assert.Null(_service.CalibrationMatrix);
|
||||
|
||||
_service.RequestLinkedView(100.0, 200.0);
|
||||
|
||||
Assert.True(_service.SystemState.HasError);
|
||||
Assert.NotEmpty(_service.SystemState.ErrorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Moq;
|
||||
using Serilog;
|
||||
using Xunit;
|
||||
using XplorePlane.Models;
|
||||
using XplorePlane.Services;
|
||||
using XplorePlane.Services.AppState;
|
||||
using XplorePlane.Services.Recipe;
|
||||
|
||||
namespace XplorePlane.Tests.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// RecipeService 单元测试。
|
||||
/// 验证配方创建、步骤记录、文件加载错误处理和名称校验。
|
||||
/// </summary>
|
||||
public class RecipeServiceTests
|
||||
{
|
||||
private readonly Mock<IAppStateService> _mockAppState;
|
||||
private readonly Mock<IPipelineExecutionService> _mockPipeline;
|
||||
private readonly Mock<ILogger> _mockLogger;
|
||||
private readonly RecipeService _service;
|
||||
|
||||
public RecipeServiceTests()
|
||||
{
|
||||
_mockAppState = new Mock<IAppStateService>();
|
||||
_mockPipeline = new Mock<IPipelineExecutionService>();
|
||||
_mockLogger = new Mock<ILogger>();
|
||||
|
||||
// Setup default state returns
|
||||
_mockAppState.Setup(s => s.MotionState).Returns(MotionState.Default);
|
||||
_mockAppState.Setup(s => s.RaySourceState).Returns(RaySourceState.Default);
|
||||
_mockAppState.Setup(s => s.DetectorState).Returns(DetectorState.Default);
|
||||
|
||||
_service = new RecipeService(
|
||||
_mockAppState.Object,
|
||||
_mockPipeline.Object,
|
||||
_mockLogger.Object);
|
||||
}
|
||||
|
||||
// ── CreateRecipe 验证 ──
|
||||
|
||||
[Fact]
|
||||
public void CreateRecipe_ValidName_ReturnsEmptyRecipe()
|
||||
{
|
||||
var recipe = _service.CreateRecipe("TestRecipe");
|
||||
|
||||
Assert.Equal("TestRecipe", recipe.Name);
|
||||
Assert.Empty(recipe.Steps);
|
||||
Assert.NotEqual(Guid.Empty, recipe.Id);
|
||||
Assert.True(recipe.CreatedAt <= DateTime.UtcNow);
|
||||
Assert.True(recipe.UpdatedAt <= DateTime.UtcNow);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateRecipe_EmptyName_ThrowsArgumentException()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => _service.CreateRecipe(string.Empty));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateRecipe_NullName_ThrowsArgumentException()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => _service.CreateRecipe(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateRecipe_WhitespaceName_ThrowsArgumentException()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => _service.CreateRecipe(" "));
|
||||
}
|
||||
|
||||
// ── RecordCurrentStep 验证 ──
|
||||
|
||||
[Fact]
|
||||
public void RecordCurrentStep_CapturesCurrentState()
|
||||
{
|
||||
var motionState = new MotionState(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12);
|
||||
var raySourceState = new RaySourceState(true, 120.0, 80.0);
|
||||
var detectorState = new DetectorState(true, true, 30.0, "2048x2048");
|
||||
|
||||
_mockAppState.Setup(s => s.MotionState).Returns(motionState);
|
||||
_mockAppState.Setup(s => s.RaySourceState).Returns(raySourceState);
|
||||
_mockAppState.Setup(s => s.DetectorState).Returns(detectorState);
|
||||
|
||||
var recipe = _service.CreateRecipe("TestRecipe");
|
||||
var pipeline = new PipelineModel { Name = "TestPipeline" };
|
||||
|
||||
var step = _service.RecordCurrentStep(recipe, pipeline);
|
||||
|
||||
Assert.Equal(0, step.StepIndex);
|
||||
Assert.Same(motionState, step.MotionState);
|
||||
Assert.Same(raySourceState, step.RaySourceState);
|
||||
Assert.Same(detectorState, step.DetectorState);
|
||||
Assert.Same(pipeline, step.Pipeline);
|
||||
}
|
||||
|
||||
// ── LoadAsync 错误处理 ──
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_FileNotExists_ThrowsFileNotFoundException()
|
||||
{
|
||||
var nonExistentPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".json");
|
||||
|
||||
await Assert.ThrowsAsync<FileNotFoundException>(
|
||||
() => _service.LoadAsync(nonExistentPath));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_InvalidJson_ThrowsInvalidDataException()
|
||||
{
|
||||
var tempFile = Path.GetTempFileName();
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(tempFile, "{ this is not valid json !!! }");
|
||||
|
||||
await Assert.ThrowsAsync<InvalidDataException>(
|
||||
() => _service.LoadAsync(tempFile));
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(tempFile))
|
||||
File.Delete(tempFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +1,34 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<UseWPF>true</UseWPF>
|
||||
<RootNamespace>XplorePlane.Tests</RootNamespace>
|
||||
<IsPackable>false</IsPackable>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>XplorePlane.Tests</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="xunit" Version="2.9.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="FsCheck" Version="3.1.0" />
|
||||
<PackageReference Include="FsCheck.Xunit" Version="3.1.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.*" />
|
||||
<PackageReference Include="xunit" Version="2.*" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.*" />
|
||||
<PackageReference Include="FsCheck" Version="2.*" />
|
||||
<PackageReference Include="FsCheck.Xunit" Version="2.*" />
|
||||
<PackageReference Include="Moq" Version="4.*" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Serilog" Version="4.3.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\XplorePlane\XplorePlane.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- 直接引用主项目依赖的本地 DLL -->
|
||||
<ItemGroup>
|
||||
<Reference Include="ImageProcessing.Core">
|
||||
<HintPath>..\XplorePlane\Libs\ImageProcessing\ImageProcessing.Core.dll</HintPath>
|
||||
<Reference Include="XP.Hardware.RaySource">
|
||||
<HintPath>..\XplorePlane\Libs\Hardware\XP.Hardware.RaySource.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user