#0040 增加测试用例

This commit is contained in:
zhengxuan.zhang
2026-03-18 20:41:05 +08:00
parent 67898edc3f
commit 180501808e
8 changed files with 121 additions and 1621 deletions
@@ -2,23 +2,21 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using FsCheck; using FsCheck;
using FsCheck.Fluent;
using XplorePlane.Models; using XplorePlane.Models;
namespace XplorePlane.Tests.Generators namespace XplorePlane.Tests.Generators
{ {
/// <summary> /// <summary>
/// FsCheck 自定义生成器:为配方相关模型定义 Arbitrary 生成器。 /// FsCheck 3.x 自定义生成器:为配方相关模型定义 Arbitrary 生成器。
/// 使用 Arb.Register&lt;RecipeGenerators&gt;() 注册。
/// </summary> /// </summary>
public class RecipeGenerators public class RecipeGenerators
{ {
// ── PipelineModel: class with settable properties ──
public static Arbitrary<PipelineModel> PipelineModelArb() public static Arbitrary<PipelineModel> PipelineModelArb()
{ {
var gen = from id in Arb.Default.Guid().Generator var gen = from id in ArbMap.Default.GeneratorFor<Guid>()
from name in Arb.Default.NonEmptyString().Generator from name in ArbMap.Default.GeneratorFor<NonEmptyString>()
from deviceId in Arb.Default.NonEmptyString().Generator from deviceId in ArbMap.Default.GeneratorFor<NonEmptyString>()
from ticks in Gen.Choose(0, int.MaxValue) from ticks in Gen.Choose(0, int.MaxValue)
let createdAt = DateTime.MinValue.AddTicks((long)ticks * 10000) let createdAt = DateTime.MinValue.AddTicks((long)ticks * 10000)
let updatedAt = createdAt let updatedAt = createdAt
@@ -31,11 +29,9 @@ namespace XplorePlane.Tests.Generators
UpdatedAt = updatedAt, UpdatedAt = updatedAt,
Nodes = new List<PipelineNodeModel>() Nodes = new List<PipelineNodeModel>()
}; };
return Arb.From(gen); return gen.ToArbitrary();
} }
// ── RecipeStep: reuses StateGenerators for hardware states ──
public static Arbitrary<RecipeStep> RecipeStepArb() public static Arbitrary<RecipeStep> RecipeStepArb()
{ {
var gen = from stepIndex in Gen.Choose(0, 100) var gen = from stepIndex in Gen.Choose(0, 100)
@@ -44,15 +40,13 @@ namespace XplorePlane.Tests.Generators
from detector in StateGenerators.DetectorStateArb().Generator from detector in StateGenerators.DetectorStateArb().Generator
from pipeline in PipelineModelArb().Generator from pipeline in PipelineModelArb().Generator
select new RecipeStep(stepIndex, motion, ray, detector, pipeline); select new RecipeStep(stepIndex, motion, ray, detector, pipeline);
return Arb.From(gen); return gen.ToArbitrary();
} }
// ── InspectionRecipe: 0-10 steps with sequential StepIndex ──
public static Arbitrary<InspectionRecipe> InspectionRecipeArb() public static Arbitrary<InspectionRecipe> InspectionRecipeArb()
{ {
var gen = from id in Arb.Default.Guid().Generator var gen = from id in ArbMap.Default.GeneratorFor<Guid>()
from name in Arb.Default.NonEmptyString().Generator from name in ArbMap.Default.GeneratorFor<NonEmptyString>()
from ticks in Gen.Choose(0, int.MaxValue) from ticks in Gen.Choose(0, int.MaxValue)
let createdAt = DateTime.MinValue.AddTicks((long)ticks * 10000) let createdAt = DateTime.MinValue.AddTicks((long)ticks * 10000)
let updatedAt = createdAt let updatedAt = createdAt
@@ -64,11 +58,9 @@ namespace XplorePlane.Tests.Generators
createdAt, createdAt,
updatedAt, updatedAt,
steps.AsReadOnly()); steps.AsReadOnly());
return Arb.From(gen); return gen.ToArbitrary();
} }
// ── RecipeExecutionState ──
public static Arbitrary<RecipeExecutionState> RecipeExecutionStateArb() public static Arbitrary<RecipeExecutionState> RecipeExecutionStateArb()
{ {
var gen = from totalSteps in Gen.Choose(0, 50) var gen = from totalSteps in Gen.Choose(0, 50)
@@ -79,27 +71,32 @@ namespace XplorePlane.Tests.Generators
RecipeExecutionStatus.Paused, RecipeExecutionStatus.Paused,
RecipeExecutionStatus.Completed, RecipeExecutionStatus.Completed,
RecipeExecutionStatus.Error) RecipeExecutionStatus.Error)
from recipeName in Arb.Default.NonEmptyString().Generator from recipeName in ArbMap.Default.GeneratorFor<NonEmptyString>()
select new RecipeExecutionState(currentStep, totalSteps, status, recipeName.Get); select new RecipeExecutionState(currentStep, totalSteps, status, recipeName.Get);
return Arb.From(gen); return gen.ToArbitrary();
} }
// ── Helper: generate a list of steps with sequential StepIndex 0..n-1 ──
private static Gen<List<RecipeStep>> GenSequentialSteps(int count) private static Gen<List<RecipeStep>> GenSequentialSteps(int count)
{ {
if (count == 0) if (count == 0)
return Gen.Constant(new List<RecipeStep>()); return Gen.Constant(new List<RecipeStep>());
return Gen.Sequence( // Build list step by step using SelectMany chain
Enumerable.Range(0, count).Select(i => Gen<List<RecipeStep>> result = Gen.Constant(new List<RecipeStep>());
from motion in StateGenerators.MotionStateArb().Generator for (int i = 0; i < count; i++)
from ray in StateGenerators.RaySourceStateArb().Generator {
from detector in StateGenerators.DetectorStateArb().Generator var idx = i;
from pipeline in PipelineModelArb().Generator result = from list in result
select new RecipeStep(i, motion, ray, detector, pipeline) from motion in StateGenerators.MotionStateArb().Generator
) from ray in StateGenerators.RaySourceStateArb().Generator
).Select(steps => steps.ToList()); from detector in StateGenerators.DetectorStateArb().Generator
from pipeline in PipelineModelArb().Generator
select new List<RecipeStep>(list)
{
new RecipeStep(idx, motion, ray, detector, pipeline)
};
}
return result;
} }
} }
} }
+18 -36
View File
@@ -1,17 +1,15 @@
using System; using System;
using FsCheck; using FsCheck;
using FsCheck.Fluent;
using XplorePlane.Models; using XplorePlane.Models;
namespace XplorePlane.Tests.Generators namespace XplorePlane.Tests.Generators
{ {
/// <summary> /// <summary>
/// FsCheck 自定义生成器:为所有状态模型定义 Arbitrary 生成器。 /// FsCheck 3.x 自定义生成器:为所有状态模型定义 Arbitrary 生成器。
/// 使用 Arb.Register&lt;StateGenerators&gt;() 注册。
/// </summary> /// </summary>
public class StateGenerators public class StateGenerators
{ {
// ── 位置范围: -10000 ~ 10000, 速度范围: 0 ~ 1000 ──
private static Gen<double> PositionGen => private static Gen<double> PositionGen =>
Gen.Choose(-10000000, 10000000).Select(i => i / 1000.0); Gen.Choose(-10000000, 10000000).Select(i => i / 1000.0);
@@ -30,8 +28,6 @@ namespace XplorePlane.Tests.Generators
private static Gen<double> MatrixGen => private static Gen<double> MatrixGen =>
Gen.Choose(-10000000, 10000000).Select(i => i / 1000.0); Gen.Choose(-10000000, 10000000).Select(i => i / 1000.0);
// ── MotionState: 6 positions + 6 speeds ──
public static Arbitrary<MotionState> MotionStateArb() public static Arbitrary<MotionState> MotionStateArb()
{ {
var gen = from xm in PositionGen var gen = from xm in PositionGen
@@ -48,34 +44,28 @@ namespace XplorePlane.Tests.Generators
from distSpd in SpeedGen from distSpd in SpeedGen
select new MotionState(xm, ym, zt, zd, tiltD, dist, select new MotionState(xm, ym, zt, zd, tiltD, dist,
xmSpd, ymSpd, ztSpd, zdSpd, tiltDSpd, distSpd); xmSpd, ymSpd, ztSpd, zdSpd, tiltDSpd, distSpd);
return Arb.From(gen); return gen.ToArbitrary();
} }
// ── RaySourceState: bool + 2 doubles ──
public static Arbitrary<RaySourceState> RaySourceStateArb() public static Arbitrary<RaySourceState> RaySourceStateArb()
{ {
var gen = from isOn in Arb.Default.Bool().Generator var gen = from isOn in ArbMap.Default.GeneratorFor<bool>()
from voltage in VoltageGen from voltage in VoltageGen
from power in PowerGen from power in PowerGen
select new RaySourceState(isOn, voltage, power); select new RaySourceState(isOn, voltage, power);
return Arb.From(gen); return gen.ToArbitrary();
} }
// ── DetectorState: 2 bools + double + string ──
public static Arbitrary<DetectorState> DetectorStateArb() public static Arbitrary<DetectorState> DetectorStateArb()
{ {
var gen = from isConnected in Arb.Default.Bool().Generator var gen = from isConnected in ArbMap.Default.GeneratorFor<bool>()
from isAcquiring in Arb.Default.Bool().Generator from isAcquiring in ArbMap.Default.GeneratorFor<bool>()
from frameRate in FrameRateGen from frameRate in FrameRateGen
from res in Gen.Elements("1024x1024", "2048x2048", "2880x2880", "3072x3072", "4260x4260") from res in Gen.Elements("1024x1024", "2048x2048", "2880x2880", "3072x3072", "4260x4260")
select new DetectorState(isConnected, isAcquiring, frameRate, res); select new DetectorState(isConnected, isAcquiring, frameRate, res);
return Arb.From(gen); return gen.ToArbitrary();
} }
// ── SystemState: OperationMode enum + bool + string ──
public static Arbitrary<SystemState> SystemStateArb() public static Arbitrary<SystemState> SystemStateArb()
{ {
var gen = from mode in Gen.Elements( var gen = from mode in Gen.Elements(
@@ -83,28 +73,24 @@ namespace XplorePlane.Tests.Generators
OperationMode.Scanning, OperationMode.Scanning,
OperationMode.CTAcquire, OperationMode.CTAcquire,
OperationMode.RecipeRun) OperationMode.RecipeRun)
from hasError in Arb.Default.Bool().Generator from hasError in ArbMap.Default.GeneratorFor<bool>()
from msg in Arb.Default.NonEmptyString().Generator from msg in ArbMap.Default.GeneratorFor<NonEmptyString>()
let errorMessage = hasError ? msg.Get : string.Empty let errorMessage = hasError ? msg.Get : string.Empty
select new SystemState(mode, hasError, errorMessage); select new SystemState(mode, hasError, errorMessage);
return Arb.From(gen); return gen.ToArbitrary();
} }
// ── CameraState: 2 bools + null CurrentFrame + 2 ints + double ──
public static Arbitrary<CameraState> CameraStateArb() public static Arbitrary<CameraState> CameraStateArb()
{ {
var gen = from isConnected in Arb.Default.Bool().Generator var gen = from isConnected in ArbMap.Default.GeneratorFor<bool>()
from isStreaming in Arb.Default.Bool().Generator from isStreaming in ArbMap.Default.GeneratorFor<bool>()
from width in Gen.Choose(0, 8192) from width in Gen.Choose(0, 8192)
from height in Gen.Choose(0, 8192) from height in Gen.Choose(0, 8192)
from frameRate in FrameRateGen from frameRate in FrameRateGen
select new CameraState(isConnected, isStreaming, null, width, height, frameRate); select new CameraState(isConnected, isStreaming, null, width, height, frameRate);
return Arb.From(gen); return gen.ToArbitrary();
} }
// ── CalibrationMatrix: 9 doubles ──
public static Arbitrary<CalibrationMatrix> CalibrationMatrixArb() public static Arbitrary<CalibrationMatrix> CalibrationMatrixArb()
{ {
var gen = from m11 in MatrixGen var gen = from m11 in MatrixGen
@@ -117,30 +103,26 @@ namespace XplorePlane.Tests.Generators
from m32 in MatrixGen from m32 in MatrixGen
from m33 in MatrixGen from m33 in MatrixGen
select new CalibrationMatrix(m11, m12, m13, m21, m22, m23, m31, m32, m33); select new CalibrationMatrix(m11, m12, m13, m21, m22, m23, m31, m32, m33);
return Arb.From(gen); return gen.ToArbitrary();
} }
// ── PhysicalPosition: 3 doubles ──
public static Arbitrary<PhysicalPosition> PhysicalPositionArb() public static Arbitrary<PhysicalPosition> PhysicalPositionArb()
{ {
var gen = from x in PositionGen var gen = from x in PositionGen
from y in PositionGen from y in PositionGen
from z in PositionGen from z in PositionGen
select new PhysicalPosition(x, y, z); select new PhysicalPosition(x, y, z);
return Arb.From(gen); return gen.ToArbitrary();
} }
// ── LinkedViewState: PhysicalPosition + bool + DateTime ──
public static Arbitrary<LinkedViewState> LinkedViewStateArb() public static Arbitrary<LinkedViewState> LinkedViewStateArb()
{ {
var gen = from pos in PhysicalPositionArb().Generator var gen = from pos in PhysicalPositionArb().Generator
from isExecuting in Arb.Default.Bool().Generator from isExecuting in ArbMap.Default.GeneratorFor<bool>()
from ticks in Gen.Choose(0, int.MaxValue) from ticks in Gen.Choose(0, int.MaxValue)
let dt = DateTime.MinValue.AddTicks((long)ticks * 10000) let dt = DateTime.MinValue.AddTicks((long)ticks * 10000)
select new LinkedViewState(pos, isExecuting, dt); select new LinkedViewState(pos, isExecuting, dt);
return Arb.From(gen); return gen.ToArbitrary();
} }
} }
} }
File diff suppressed because it is too large Load Diff
+19 -1
View File
@@ -1,17 +1,27 @@
using System; using System;
using Xunit; using Xunit;
using Xunit.Abstractions;
using XplorePlane.Models; using XplorePlane.Models;
namespace XplorePlane.Tests.Models namespace XplorePlane.Tests.Models
{ {
public class StateModelsTests public class StateModelsTests
{ {
private readonly ITestOutputHelper _output;
public StateModelsTests(ITestOutputHelper output)
{
_output = output;
}
// ── Default Value Tests ─────────────────────────────────────── // ── Default Value Tests ───────────────────────────────────────
[Fact] [Fact]
public void MotionState_Default_AllZeros() public void MotionState_Default_AllZeros()
{ {
var state = MotionState.Default; var state = MotionState.Default;
_output.WriteLine($"MotionState.Default: XM={state.XM}, YM={state.YM}, ZT={state.ZT}, ZD={state.ZD}, TiltD={state.TiltD}, Dist={state.Dist}");
_output.WriteLine($" Speeds: XM={state.XMSpeed}, YM={state.YMSpeed}, ZT={state.ZTSpeed}, ZD={state.ZDSpeed}, TiltD={state.TiltDSpeed}, Dist={state.DistSpeed}");
Assert.Equal(0, state.XM); Assert.Equal(0, state.XM);
Assert.Equal(0, state.YM); Assert.Equal(0, state.YM);
@@ -31,6 +41,7 @@ namespace XplorePlane.Tests.Models
public void RaySourceState_Default_IsOffAndZeros() public void RaySourceState_Default_IsOffAndZeros()
{ {
var state = RaySourceState.Default; var state = RaySourceState.Default;
_output.WriteLine($"RaySourceState.Default: IsOn={state.IsOn}, Voltage={state.Voltage}, Power={state.Power}");
Assert.False(state.IsOn); Assert.False(state.IsOn);
Assert.Equal(0, state.Voltage); Assert.Equal(0, state.Voltage);
@@ -41,6 +52,7 @@ namespace XplorePlane.Tests.Models
public void DetectorState_Default_DisconnectedAndZeros() public void DetectorState_Default_DisconnectedAndZeros()
{ {
var state = DetectorState.Default; var state = DetectorState.Default;
_output.WriteLine($"DetectorState.Default: IsConnected={state.IsConnected}, IsAcquiring={state.IsAcquiring}, FrameRate={state.FrameRate}, Resolution='{state.Resolution}'");
Assert.False(state.IsConnected); Assert.False(state.IsConnected);
Assert.False(state.IsAcquiring); Assert.False(state.IsAcquiring);
@@ -52,6 +64,7 @@ namespace XplorePlane.Tests.Models
public void SystemState_Default_IdleNoError() public void SystemState_Default_IdleNoError()
{ {
var state = SystemState.Default; var state = SystemState.Default;
_output.WriteLine($"SystemState.Default: OperationMode={state.OperationMode}, HasError={state.HasError}, ErrorMessage='{state.ErrorMessage}'");
Assert.Equal(OperationMode.Idle, state.OperationMode); Assert.Equal(OperationMode.Idle, state.OperationMode);
Assert.False(state.HasError); Assert.False(state.HasError);
@@ -62,6 +75,7 @@ namespace XplorePlane.Tests.Models
public void CameraState_Default_DisconnectedAndZeros() public void CameraState_Default_DisconnectedAndZeros()
{ {
var state = CameraState.Default; var state = CameraState.Default;
_output.WriteLine($"CameraState.Default: IsConnected={state.IsConnected}, IsStreaming={state.IsStreaming}, CurrentFrame={state.CurrentFrame}, Width={state.Width}, Height={state.Height}, FrameRate={state.FrameRate}");
Assert.False(state.IsConnected); Assert.False(state.IsConnected);
Assert.False(state.IsStreaming); Assert.False(state.IsStreaming);
@@ -75,6 +89,7 @@ namespace XplorePlane.Tests.Models
public void LinkedViewState_Default_ZeroPositionNotExecuting() public void LinkedViewState_Default_ZeroPositionNotExecuting()
{ {
var state = LinkedViewState.Default; var state = LinkedViewState.Default;
_output.WriteLine($"LinkedViewState.Default: TargetPosition=({state.TargetPosition.X}, {state.TargetPosition.Y}, {state.TargetPosition.Z}), IsExecuting={state.IsExecuting}, LastRequestTime={state.LastRequestTime}");
Assert.Equal(0, state.TargetPosition.X); Assert.Equal(0, state.TargetPosition.X);
Assert.Equal(0, state.TargetPosition.Y); Assert.Equal(0, state.TargetPosition.Y);
@@ -87,6 +102,7 @@ namespace XplorePlane.Tests.Models
public void RecipeExecutionState_Default_IdleAndZeros() public void RecipeExecutionState_Default_IdleAndZeros()
{ {
var state = RecipeExecutionState.Default; var state = RecipeExecutionState.Default;
_output.WriteLine($"RecipeExecutionState.Default: CurrentStepIndex={state.CurrentStepIndex}, TotalSteps={state.TotalSteps}, Status={state.Status}, CurrentRecipeName='{state.CurrentRecipeName}'");
Assert.Equal(0, state.CurrentStepIndex); Assert.Equal(0, state.CurrentStepIndex);
Assert.Equal(0, state.TotalSteps); Assert.Equal(0, state.TotalSteps);
@@ -100,8 +116,8 @@ namespace XplorePlane.Tests.Models
public void MotionState_WithExpression_ProducesNewInstance() public void MotionState_WithExpression_ProducesNewInstance()
{ {
var original = MotionState.Default; var original = MotionState.Default;
var modified = original with { XM = 100 }; var modified = original with { XM = 100 };
_output.WriteLine($"Original.XM={original.XM}, Modified.XM={modified.XM}, SameRef={ReferenceEquals(original, modified)}");
// New instance is different from original // New instance is different from original
Assert.NotSame(original, modified); Assert.NotSame(original, modified);
@@ -124,6 +140,7 @@ namespace XplorePlane.Tests.Models
); );
var (x, y, z) = matrix.Transform(pixelX: 5, pixelY: 8); var (x, y, z) = matrix.Transform(pixelX: 5, pixelY: 8);
_output.WriteLine($"Matrix Transform(5, 8): x={x}, y={y}, z={z} (expected: 20, 44, 0)");
// x = 2*5 + 0*8 + 10 = 20 // x = 2*5 + 0*8 + 10 = 20
Assert.Equal(20, x); Assert.Equal(20, x);
@@ -144,6 +161,7 @@ namespace XplorePlane.Tests.Models
); );
var (x, y, z) = identity.Transform(pixelX: 42.5, pixelY: 99.1); var (x, y, z) = identity.Transform(pixelX: 42.5, pixelY: 99.1);
_output.WriteLine($"Identity Transform(42.5, 99.1): x={x}, y={y}, z={z}");
Assert.Equal(42.5, x, precision: 10); Assert.Equal(42.5, x, precision: 10);
Assert.Equal(99.1, y, precision: 10); Assert.Equal(99.1, y, precision: 10);
@@ -3,6 +3,7 @@ using System.Windows;
using Moq; using Moq;
using Serilog; using Serilog;
using Xunit; using Xunit;
using Xunit.Abstractions;
using XP.Hardware.RaySource.Services; using XP.Hardware.RaySource.Services;
using XplorePlane.Models; using XplorePlane.Models;
using XplorePlane.Services.AppState; using XplorePlane.Services.AppState;
@@ -18,9 +19,12 @@ namespace XplorePlane.Tests.Services
private readonly AppStateService _service; private readonly AppStateService _service;
private readonly Mock<IRaySourceService> _mockRaySource; private readonly Mock<IRaySourceService> _mockRaySource;
private readonly Mock<ILogger> _mockLogger; private readonly Mock<ILogger> _mockLogger;
private readonly ITestOutputHelper _output;
public AppStateServiceTests() public AppStateServiceTests(ITestOutputHelper output)
{ {
_output = output;
// Ensure WPF Application exists for Dispatcher // Ensure WPF Application exists for Dispatcher
if (Application.Current == null) if (Application.Current == null)
{ {
@@ -42,24 +46,28 @@ namespace XplorePlane.Tests.Services
[Fact] [Fact]
public void DefaultState_MotionState_IsDefault() public void DefaultState_MotionState_IsDefault()
{ {
_output.WriteLine($"MotionState == MotionState.Default: {ReferenceEquals(MotionState.Default, _service.MotionState)}");
Assert.Same(MotionState.Default, _service.MotionState); Assert.Same(MotionState.Default, _service.MotionState);
} }
[Fact] [Fact]
public void DefaultState_RaySourceState_IsDefault() public void DefaultState_RaySourceState_IsDefault()
{ {
_output.WriteLine($"RaySourceState == RaySourceState.Default: {ReferenceEquals(RaySourceState.Default, _service.RaySourceState)}");
Assert.Same(RaySourceState.Default, _service.RaySourceState); Assert.Same(RaySourceState.Default, _service.RaySourceState);
} }
[Fact] [Fact]
public void DefaultState_SystemState_IsDefault() public void DefaultState_SystemState_IsDefault()
{ {
_output.WriteLine($"SystemState == SystemState.Default: {ReferenceEquals(SystemState.Default, _service.SystemState)}");
Assert.Same(SystemState.Default, _service.SystemState); Assert.Same(SystemState.Default, _service.SystemState);
} }
[Fact] [Fact]
public void DefaultState_CalibrationMatrix_IsNull() public void DefaultState_CalibrationMatrix_IsNull()
{ {
_output.WriteLine($"CalibrationMatrix: {_service.CalibrationMatrix?.ToString() ?? "null"}");
Assert.Null(_service.CalibrationMatrix); Assert.Null(_service.CalibrationMatrix);
} }
@@ -68,25 +76,29 @@ namespace XplorePlane.Tests.Services
[Fact] [Fact]
public void UpdateMotionState_NullArgument_ThrowsArgumentNullException() public void UpdateMotionState_NullArgument_ThrowsArgumentNullException()
{ {
Assert.Throws<ArgumentNullException>(() => _service.UpdateMotionState(null!)); var ex = Assert.Throws<ArgumentNullException>(() => _service.UpdateMotionState(null!));
_output.WriteLine($"UpdateMotionState(null) threw: {ex.GetType().Name}, Param={ex.ParamName}");
} }
[Fact] [Fact]
public void UpdateRaySourceState_NullArgument_ThrowsArgumentNullException() public void UpdateRaySourceState_NullArgument_ThrowsArgumentNullException()
{ {
Assert.Throws<ArgumentNullException>(() => _service.UpdateRaySourceState(null!)); var ex = Assert.Throws<ArgumentNullException>(() => _service.UpdateRaySourceState(null!));
_output.WriteLine($"UpdateRaySourceState(null) threw: {ex.GetType().Name}, Param={ex.ParamName}");
} }
[Fact] [Fact]
public void UpdateDetectorState_NullArgument_ThrowsArgumentNullException() public void UpdateDetectorState_NullArgument_ThrowsArgumentNullException()
{ {
Assert.Throws<ArgumentNullException>(() => _service.UpdateDetectorState(null!)); var ex = Assert.Throws<ArgumentNullException>(() => _service.UpdateDetectorState(null!));
_output.WriteLine($"UpdateDetectorState(null) threw: {ex.GetType().Name}, Param={ex.ParamName}");
} }
[Fact] [Fact]
public void UpdateSystemState_NullArgument_ThrowsArgumentNullException() public void UpdateSystemState_NullArgument_ThrowsArgumentNullException()
{ {
Assert.Throws<ArgumentNullException>(() => _service.UpdateSystemState(null!)); var ex = Assert.Throws<ArgumentNullException>(() => _service.UpdateSystemState(null!));
_output.WriteLine($"UpdateSystemState(null) threw: {ex.GetType().Name}, Param={ex.ParamName}");
} }
// ── Dispose 后 Update 被忽略 ── // ── Dispose 后 Update 被忽略 ──
@@ -96,11 +108,13 @@ namespace XplorePlane.Tests.Services
{ {
var originalState = _service.MotionState; var originalState = _service.MotionState;
_service.Dispose(); _service.Dispose();
_output.WriteLine("Service disposed, attempting UpdateMotionState...");
// Should not throw, and state should remain unchanged // Should not throw, and state should remain unchanged
var newState = new MotionState(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); var newState = new MotionState(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12);
_service.UpdateMotionState(newState); _service.UpdateMotionState(newState);
_output.WriteLine($"State unchanged after dispose: {ReferenceEquals(originalState, _service.MotionState)}");
Assert.Same(originalState, _service.MotionState); Assert.Same(originalState, _service.MotionState);
} }
@@ -114,6 +128,7 @@ namespace XplorePlane.Tests.Services
_service.RequestLinkedView(100.0, 200.0); _service.RequestLinkedView(100.0, 200.0);
_output.WriteLine($"RequestLinkedView(100, 200) without CalibrationMatrix: HasError={_service.SystemState.HasError}, ErrorMessage='{_service.SystemState.ErrorMessage}'");
Assert.True(_service.SystemState.HasError); Assert.True(_service.SystemState.HasError);
Assert.NotEmpty(_service.SystemState.ErrorMessage); Assert.NotEmpty(_service.SystemState.ErrorMessage);
} }
@@ -4,6 +4,7 @@ using System.Threading.Tasks;
using Moq; using Moq;
using Serilog; using Serilog;
using Xunit; using Xunit;
using Xunit.Abstractions;
using XplorePlane.Models; using XplorePlane.Models;
using XplorePlane.Services; using XplorePlane.Services;
using XplorePlane.Services.AppState; using XplorePlane.Services.AppState;
@@ -21,9 +22,11 @@ namespace XplorePlane.Tests.Services
private readonly Mock<IPipelineExecutionService> _mockPipeline; private readonly Mock<IPipelineExecutionService> _mockPipeline;
private readonly Mock<ILogger> _mockLogger; private readonly Mock<ILogger> _mockLogger;
private readonly RecipeService _service; private readonly RecipeService _service;
private readonly ITestOutputHelper _output;
public RecipeServiceTests() public RecipeServiceTests(ITestOutputHelper output)
{ {
_output = output;
_mockAppState = new Mock<IAppStateService>(); _mockAppState = new Mock<IAppStateService>();
_mockPipeline = new Mock<IPipelineExecutionService>(); _mockPipeline = new Mock<IPipelineExecutionService>();
_mockLogger = new Mock<ILogger>(); _mockLogger = new Mock<ILogger>();
@@ -45,6 +48,7 @@ namespace XplorePlane.Tests.Services
public void CreateRecipe_ValidName_ReturnsEmptyRecipe() public void CreateRecipe_ValidName_ReturnsEmptyRecipe()
{ {
var recipe = _service.CreateRecipe("TestRecipe"); var recipe = _service.CreateRecipe("TestRecipe");
_output.WriteLine($"CreateRecipe('TestRecipe'): Name={recipe.Name}, Id={recipe.Id}, Steps.Count={recipe.Steps.Count}, CreatedAt={recipe.CreatedAt}");
Assert.Equal("TestRecipe", recipe.Name); Assert.Equal("TestRecipe", recipe.Name);
Assert.Empty(recipe.Steps); Assert.Empty(recipe.Steps);
@@ -56,19 +60,22 @@ namespace XplorePlane.Tests.Services
[Fact] [Fact]
public void CreateRecipe_EmptyName_ThrowsArgumentException() public void CreateRecipe_EmptyName_ThrowsArgumentException()
{ {
Assert.Throws<ArgumentException>(() => _service.CreateRecipe(string.Empty)); var ex = Assert.Throws<ArgumentException>(() => _service.CreateRecipe(string.Empty));
_output.WriteLine($"CreateRecipe('') threw: {ex.GetType().Name}, Message={ex.Message}");
} }
[Fact] [Fact]
public void CreateRecipe_NullName_ThrowsArgumentException() public void CreateRecipe_NullName_ThrowsArgumentException()
{ {
Assert.Throws<ArgumentException>(() => _service.CreateRecipe(null!)); var ex = Assert.Throws<ArgumentException>(() => _service.CreateRecipe(null!));
_output.WriteLine($"CreateRecipe(null) threw: {ex.GetType().Name}, Message={ex.Message}");
} }
[Fact] [Fact]
public void CreateRecipe_WhitespaceName_ThrowsArgumentException() public void CreateRecipe_WhitespaceName_ThrowsArgumentException()
{ {
Assert.Throws<ArgumentException>(() => _service.CreateRecipe(" ")); var ex = Assert.Throws<ArgumentException>(() => _service.CreateRecipe(" "));
_output.WriteLine($"CreateRecipe(' ') threw: {ex.GetType().Name}, Message={ex.Message}");
} }
// ── RecordCurrentStep 验证 ── // ── RecordCurrentStep 验证 ──
@@ -88,6 +95,7 @@ namespace XplorePlane.Tests.Services
var pipeline = new PipelineModel { Name = "TestPipeline" }; var pipeline = new PipelineModel { Name = "TestPipeline" };
var step = _service.RecordCurrentStep(recipe, pipeline); var step = _service.RecordCurrentStep(recipe, pipeline);
_output.WriteLine($"RecordCurrentStep: StepIndex={step.StepIndex}, MotionState.XM={step.MotionState.XM}, RaySource.Voltage={step.RaySourceState.Voltage}, Detector.Resolution={step.DetectorState.Resolution}, Pipeline={step.Pipeline.Name}");
Assert.Equal(0, step.StepIndex); Assert.Equal(0, step.StepIndex);
Assert.Same(motionState, step.MotionState); Assert.Same(motionState, step.MotionState);
@@ -102,9 +110,11 @@ namespace XplorePlane.Tests.Services
public async Task LoadAsync_FileNotExists_ThrowsFileNotFoundException() public async Task LoadAsync_FileNotExists_ThrowsFileNotFoundException()
{ {
var nonExistentPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".json"); var nonExistentPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".json");
_output.WriteLine($"LoadAsync 不存在的文件: {nonExistentPath}");
await Assert.ThrowsAsync<FileNotFoundException>( var ex = await Assert.ThrowsAsync<FileNotFoundException>(
() => _service.LoadAsync(nonExistentPath)); () => _service.LoadAsync(nonExistentPath));
_output.WriteLine($"抛出异常: {ex.GetType().Name}, Message={ex.Message}");
} }
[Fact] [Fact]
@@ -114,9 +124,11 @@ namespace XplorePlane.Tests.Services
try try
{ {
await File.WriteAllTextAsync(tempFile, "{ this is not valid json !!! }"); await File.WriteAllTextAsync(tempFile, "{ this is not valid json !!! }");
_output.WriteLine($"LoadAsync 无效JSON文件: {tempFile}");
await Assert.ThrowsAsync<InvalidDataException>( var ex = await Assert.ThrowsAsync<InvalidDataException>(
() => _service.LoadAsync(tempFile)); () => _service.LoadAsync(tempFile));
_output.WriteLine($"抛出异常: {ex.GetType().Name}, Message={ex.Message}");
} }
finally finally
{ {
+14 -2
View File
@@ -3,16 +3,24 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework> <TargetFramework>net8.0-windows</TargetFramework>
<UseWPF>true</UseWPF> <UseWPF>true</UseWPF>
<OutputType>Library</OutputType>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<RootNamespace>XplorePlane.Tests</RootNamespace> <RootNamespace>XplorePlane.Tests</RootNamespace>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Compile Remove="Helpers\Define.cs" />
</ItemGroup>
<ItemGroup>
<None Remove="Helpers\Define.cs.bak" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.*" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.*" />
<PackageReference Include="xunit" Version="2.*" /> <PackageReference Include="xunit" Version="2.*" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.*" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.*" />
<PackageReference Include="FsCheck" Version="2.*" /> <PackageReference Include="FsCheck" Version="3.*" />
<PackageReference Include="FsCheck.Xunit" Version="2.*" /> <PackageReference Include="FsCheck.Xunit" Version="3.*" />
<PackageReference Include="Moq" Version="4.*" /> <PackageReference Include="Moq" Version="4.*" />
</ItemGroup> </ItemGroup>
@@ -29,6 +37,10 @@
<HintPath>..\XplorePlane\Libs\Hardware\XP.Hardware.RaySource.dll</HintPath> <HintPath>..\XplorePlane\Libs\Hardware\XP.Hardware.RaySource.dll</HintPath>
<Private>True</Private> <Private>True</Private>
</Reference> </Reference>
<Reference Include="ImageProcessing.Core">
<HintPath>..\XplorePlane\Libs\ImageProcessing\ImageProcessing.Core.dll</HintPath>
<Private>True</Private>
</Reference>
</ItemGroup> </ItemGroup>
</Project> </Project>
+2
View File
@@ -5,6 +5,7 @@
<UseWPF>true</UseWPF> <UseWPF>true</UseWPF>
<RootNamespace>XplorePlane</RootNamespace> <RootNamespace>XplorePlane</RootNamespace>
<AssemblyName>XplorePlane</AssemblyName> <AssemblyName>XplorePlane</AssemblyName>
<ApplicationIcon>GapInspect.ico</ApplicationIcon>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -186,6 +187,7 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Link>Libs\ImageProcessing\zh-CN\%(Filename)%(Extension)</Link> <Link>Libs\ImageProcessing\zh-CN\%(Filename)%(Extension)</Link>
</None> </None>
<Content Include="GapInspect.ico" />
<!-- 配置文件 --> <!-- 配置文件 -->
<None Update="App.config"> <None Update="App.config">