#0021 增加测试工程
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
using System.Windows;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using ImageProcessing.Core;
|
||||
using Moq;
|
||||
using XplorePlane.Services;
|
||||
using System;
|
||||
|
||||
namespace XplorePlane.Tests.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// 测试辅助工具:创建 Mock 和测试用图像
|
||||
/// </summary>
|
||||
internal static class TestHelpers
|
||||
{
|
||||
public static readonly string[] DefaultKeys = { "Blur", "Sharpen", "Threshold" };
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个 1x1 像素的 BitmapSource,用于测试
|
||||
/// </summary>
|
||||
public static BitmapSource CreateTestBitmap(int width = 4, int height = 4)
|
||||
{
|
||||
var pixels = new byte[width * height * 4];
|
||||
var bitmap = BitmapSource.Create(
|
||||
width, height, 96, 96,
|
||||
PixelFormats.Bgra32, null,
|
||||
pixels, width * 4);
|
||||
bitmap.Freeze();
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个超大图像(用于测试预览缩放)
|
||||
/// </summary>
|
||||
public static BitmapSource CreateLargeBitmap(int width = 4096, int height = 4096)
|
||||
=> CreateTestBitmap(width, height);
|
||||
|
||||
/// <summary>
|
||||
/// 创建标准 Mock IImageProcessingService,注册 DefaultKeys 中的算子
|
||||
/// </summary>
|
||||
public static Mock<IImageProcessingService> CreateMockImageService(
|
||||
string[]? keys = null,
|
||||
ProcessorParameter[]? parameters = null)
|
||||
{
|
||||
keys ??= DefaultKeys;
|
||||
parameters ??= Array.Empty<ProcessorParameter>();
|
||||
|
||||
var mock = new Mock<IImageProcessingService>();
|
||||
mock.Setup(s => s.GetAvailableProcessors()).Returns(keys);
|
||||
mock.Setup(s => s.GetProcessorDisplayName(It.IsAny<string>()))
|
||||
.Returns<string>(k => k + "算子");
|
||||
mock.Setup(s => s.GetProcessorParameters(It.IsAny<string>()))
|
||||
.Returns(parameters);
|
||||
mock.Setup(s => s.ProcessImageAsync(
|
||||
It.IsAny<BitmapSource>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<IDictionary<string, object>>(),
|
||||
It.IsAny<IProgress<double>>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((BitmapSource src, string _, IDictionary<string, object> _, IProgress<double> _, CancellationToken ct) =>
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
return src;
|
||||
});
|
||||
return mock;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建带数值参数的 ProcessorParameter
|
||||
/// </summary>
|
||||
public static ProcessorParameter MakeIntParam(string name, int value = 5, int min = 0, int max = 10)
|
||||
=> new ProcessorParameter(name, name, typeof(int), value, min, max);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
using Xunit;
|
||||
using XplorePlane.ViewModels;
|
||||
using XplorePlane.Tests.Helpers;
|
||||
using System;
|
||||
|
||||
namespace XplorePlane.Tests.Pipeline
|
||||
{
|
||||
/// <summary>
|
||||
/// 测试 OperatorToolboxViewModel 搜索过滤逻辑(任务 6.7)
|
||||
/// </summary>
|
||||
public class OperatorToolboxViewModelTests
|
||||
{
|
||||
private OperatorToolboxViewModel CreateVm(string[]? keys = null)
|
||||
{
|
||||
var mock = TestHelpers.CreateMockImageService(keys);
|
||||
return new OperatorToolboxViewModel(mock.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmptySearch_ReturnsAllOperators()
|
||||
{
|
||||
var vm = CreateVm(new[] { "Blur", "Sharpen", "Threshold" });
|
||||
vm.SearchText = string.Empty;
|
||||
|
||||
Assert.Equal(3, vm.FilteredOperators.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NullSearch_ReturnsAllOperators()
|
||||
{
|
||||
var vm = CreateVm(new[] { "Blur", "Sharpen", "Threshold" });
|
||||
vm.SearchText = null!;
|
||||
|
||||
Assert.Equal(3, vm.FilteredOperators.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WhitespaceSearch_ReturnsAllOperators()
|
||||
{
|
||||
var vm = CreateVm(new[] { "Blur", "Sharpen", "Threshold" });
|
||||
vm.SearchText = " ";
|
||||
|
||||
Assert.Equal(3, vm.FilteredOperators.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SearchText_FiltersMatchingOperators()
|
||||
{
|
||||
// DisplayName = Key + "算子",所以 "Blur算子" 包含 "Blur"
|
||||
var vm = CreateVm(new[] { "Blur", "Sharpen", "Threshold" });
|
||||
vm.SearchText = "Blur";
|
||||
|
||||
Assert.Single(vm.FilteredOperators);
|
||||
Assert.Equal("Blur", vm.FilteredOperators[0].Key);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SearchText_CaseInsensitive()
|
||||
{
|
||||
var vm = CreateVm(new[] { "Blur", "Sharpen", "Threshold" });
|
||||
vm.SearchText = "blur";
|
||||
|
||||
Assert.Single(vm.FilteredOperators);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SearchText_NoMatch_ReturnsEmpty()
|
||||
{
|
||||
var vm = CreateVm(new[] { "Blur", "Sharpen", "Threshold" });
|
||||
vm.SearchText = "XYZ_NOT_EXIST";
|
||||
|
||||
Assert.Empty(vm.FilteredOperators);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SearchText_PartialMatch_ReturnsMatching()
|
||||
{
|
||||
var vm = CreateVm(new[] { "Blur", "Sharpen", "Threshold" });
|
||||
vm.SearchText = "算子"; // 所有 DisplayName 都包含"算子"
|
||||
|
||||
Assert.Equal(3, vm.FilteredOperators.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FilteredOperators_EachContainsSearchText()
|
||||
{
|
||||
var vm = CreateVm(new[] { "Blur", "Sharpen", "Threshold" });
|
||||
vm.SearchText = "Sh";
|
||||
|
||||
foreach (var op in vm.FilteredOperators)
|
||||
Assert.Contains("Sh", op.DisplayName, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NoOperators_EmptySearch_ReturnsEmpty()
|
||||
{
|
||||
var vm = CreateVm(Array.Empty<string>());
|
||||
vm.SearchText = string.Empty;
|
||||
|
||||
Assert.Empty(vm.FilteredOperators);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
using Moq;
|
||||
using Xunit;
|
||||
using XplorePlane.Models;
|
||||
using XplorePlane.Services;
|
||||
using XplorePlane.ViewModels;
|
||||
using XplorePlane.Tests.Helpers;
|
||||
|
||||
namespace XplorePlane.Tests.Pipeline
|
||||
{
|
||||
/// <summary>
|
||||
/// 测试 PipelineEditorViewModel 的命令逻辑
|
||||
/// 覆盖任务 6.1 / 6.2 / 6.3 / 6.4 / 6.8
|
||||
/// </summary>
|
||||
public class PipelineEditorViewModelTests
|
||||
{
|
||||
private readonly Mock<IImageProcessingService> _mockImageSvc;
|
||||
private readonly Mock<IPipelineExecutionService> _mockExecSvc;
|
||||
private readonly Mock<IPipelinePersistenceService> _mockPersistSvc;
|
||||
|
||||
public PipelineEditorViewModelTests()
|
||||
{
|
||||
_mockImageSvc = TestHelpers.CreateMockImageService();
|
||||
_mockExecSvc = new Mock<IPipelineExecutionService>();
|
||||
_mockPersistSvc = new Mock<IPipelinePersistenceService>();
|
||||
}
|
||||
|
||||
private PipelineEditorViewModel CreateVm() =>
|
||||
new PipelineEditorViewModel(_mockImageSvc.Object, _mockExecSvc.Object, _mockPersistSvc.Object);
|
||||
|
||||
// ── 6.1 AddOperatorCommand ────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void AddOperator_ValidKey_AppendsNode()
|
||||
{
|
||||
var vm = CreateVm();
|
||||
vm.AddOperatorCommand.Execute("Blur");
|
||||
|
||||
Assert.Single(vm.PipelineNodes);
|
||||
Assert.Equal("Blur", vm.PipelineNodes[0].OperatorKey);
|
||||
Assert.Equal(0, vm.PipelineNodes[0].Order);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddOperator_InvalidKey_NodeNotAdded()
|
||||
{
|
||||
var vm = CreateVm();
|
||||
vm.AddOperatorCommand.Execute("NonExistentKey");
|
||||
|
||||
Assert.Empty(vm.PipelineNodes);
|
||||
Assert.Contains("未注册", vm.StatusMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddOperator_NullKey_NodeNotAdded()
|
||||
{
|
||||
var vm = CreateVm();
|
||||
vm.AddOperatorCommand.Execute(null);
|
||||
|
||||
Assert.Empty(vm.PipelineNodes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddOperator_ExceedsMaxLength_NodeNotAdded()
|
||||
{
|
||||
// 主项目中 MaxPipelineLength = 20,通过反射读取或直接添加 20 个
|
||||
var vm = CreateVm();
|
||||
for (int i = 0; i < 20; i++)
|
||||
vm.AddOperatorCommand.Execute("Blur");
|
||||
|
||||
Assert.Equal(20, vm.PipelineNodes.Count);
|
||||
|
||||
vm.AddOperatorCommand.Execute("Blur");
|
||||
|
||||
Assert.Equal(20, vm.PipelineNodes.Count);
|
||||
Assert.Contains("上限", vm.StatusMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddOperator_MultipleNodes_OrderIsSequential()
|
||||
{
|
||||
var vm = CreateVm();
|
||||
vm.AddOperatorCommand.Execute("Blur");
|
||||
vm.AddOperatorCommand.Execute("Sharpen");
|
||||
vm.AddOperatorCommand.Execute("Threshold");
|
||||
|
||||
Assert.Equal(3, vm.PipelineNodes.Count);
|
||||
for (int i = 0; i < 3; i++)
|
||||
Assert.Equal(i, vm.PipelineNodes[i].Order);
|
||||
}
|
||||
|
||||
// ── 6.2 RemoveOperatorCommand ─────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void RemoveOperator_ExistingNode_NodeRemoved()
|
||||
{
|
||||
var vm = CreateVm();
|
||||
vm.AddOperatorCommand.Execute("Blur");
|
||||
var node = vm.PipelineNodes[0];
|
||||
|
||||
vm.RemoveOperatorCommand.Execute(node);
|
||||
|
||||
Assert.Empty(vm.PipelineNodes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveOperator_MiddleNode_OrderRenumbered()
|
||||
{
|
||||
var vm = CreateVm();
|
||||
vm.AddOperatorCommand.Execute("Blur");
|
||||
vm.AddOperatorCommand.Execute("Sharpen");
|
||||
vm.AddOperatorCommand.Execute("Threshold");
|
||||
var middle = vm.PipelineNodes[1];
|
||||
|
||||
vm.RemoveOperatorCommand.Execute(middle);
|
||||
|
||||
Assert.Equal(2, vm.PipelineNodes.Count);
|
||||
Assert.Equal(0, vm.PipelineNodes[0].Order);
|
||||
Assert.Equal(1, vm.PipelineNodes[1].Order);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveOperator_SelectedNode_SelectedNodeCleared()
|
||||
{
|
||||
var vm = CreateVm();
|
||||
vm.AddOperatorCommand.Execute("Blur");
|
||||
var node = vm.PipelineNodes[0];
|
||||
vm.SelectedNode = node;
|
||||
|
||||
vm.RemoveOperatorCommand.Execute(node);
|
||||
|
||||
Assert.Null(vm.SelectedNode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveOperator_NullNode_NoException()
|
||||
{
|
||||
var vm = CreateVm();
|
||||
var ex = Record.Exception(() => vm.RemoveOperatorCommand.Execute(null));
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
// ── 6.3 ReorderOperatorCommand ────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ReorderOperator_MoveFirstToLast_OrderConsistent()
|
||||
{
|
||||
var vm = CreateVm();
|
||||
vm.AddOperatorCommand.Execute("Blur");
|
||||
vm.AddOperatorCommand.Execute("Sharpen");
|
||||
vm.AddOperatorCommand.Execute("Threshold");
|
||||
|
||||
vm.ReorderOperatorCommand.Execute(new PipelineReorderArgs { OldIndex = 0, NewIndex = 2 });
|
||||
|
||||
for (int i = 0; i < vm.PipelineNodes.Count; i++)
|
||||
Assert.Equal(i, vm.PipelineNodes[i].Order);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReorderOperator_MoveLastToFirst_OrderConsistent()
|
||||
{
|
||||
var vm = CreateVm();
|
||||
vm.AddOperatorCommand.Execute("Blur");
|
||||
vm.AddOperatorCommand.Execute("Sharpen");
|
||||
vm.AddOperatorCommand.Execute("Threshold");
|
||||
var lastName = vm.PipelineNodes[2].OperatorKey;
|
||||
|
||||
vm.ReorderOperatorCommand.Execute(new PipelineReorderArgs { OldIndex = 2, NewIndex = 0 });
|
||||
|
||||
Assert.Equal(lastName, vm.PipelineNodes[0].OperatorKey);
|
||||
for (int i = 0; i < vm.PipelineNodes.Count; i++)
|
||||
Assert.Equal(i, vm.PipelineNodes[i].Order);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReorderOperator_SameIndex_NoChange()
|
||||
{
|
||||
var vm = CreateVm();
|
||||
vm.AddOperatorCommand.Execute("Blur");
|
||||
vm.AddOperatorCommand.Execute("Sharpen");
|
||||
var originalFirst = vm.PipelineNodes[0].OperatorKey;
|
||||
|
||||
vm.ReorderOperatorCommand.Execute(new PipelineReorderArgs { OldIndex = 0, NewIndex = 0 });
|
||||
|
||||
Assert.Equal(originalFirst, vm.PipelineNodes[0].OperatorKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReorderOperator_OutOfBoundsIndex_NoException()
|
||||
{
|
||||
var vm = CreateVm();
|
||||
vm.AddOperatorCommand.Execute("Blur");
|
||||
|
||||
var ex = Record.Exception(() =>
|
||||
vm.ReorderOperatorCommand.Execute(new PipelineReorderArgs { OldIndex = 0, NewIndex = 99 }));
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
// ── 6.4 LoadNodeParameters ────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void LoadNodeParameters_ModifiedValuePreserved()
|
||||
{
|
||||
var param = TestHelpers.MakeIntParam("Radius", value: 5, min: 1, max: 20);
|
||||
_mockImageSvc.Setup(s => s.GetProcessorParameters("Blur"))
|
||||
.Returns(new[] { param });
|
||||
|
||||
var vm = CreateVm();
|
||||
vm.AddOperatorCommand.Execute("Blur");
|
||||
var node = vm.PipelineNodes[0];
|
||||
|
||||
// 修改参数值
|
||||
node.Parameters[0].Value = 10;
|
||||
|
||||
// 重新选中节点触发 LoadNodeParameters
|
||||
vm.SelectedNode = null;
|
||||
vm.SelectedNode = node;
|
||||
|
||||
Assert.Equal(10, node.Parameters[0].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadNodeParameters_UnmodifiedUsesDefault()
|
||||
{
|
||||
var param = TestHelpers.MakeIntParam("Radius", value: 5, min: 1, max: 20);
|
||||
_mockImageSvc.Setup(s => s.GetProcessorParameters("Blur"))
|
||||
.Returns(new[] { param });
|
||||
|
||||
var vm = CreateVm();
|
||||
vm.AddOperatorCommand.Execute("Blur");
|
||||
var node = vm.PipelineNodes[0];
|
||||
|
||||
Assert.Equal(5, node.Parameters[0].Value);
|
||||
}
|
||||
|
||||
// ── 6.8 参数范围校验 ──────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ParameterValidation_ValueWithinRange_IsValid()
|
||||
{
|
||||
var param = TestHelpers.MakeIntParam("Radius", value: 5, min: 1, max: 10);
|
||||
var vm = new ProcessorParameterVM(param) { Value = 7 };
|
||||
|
||||
Assert.True(vm.IsValueValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParameterValidation_ValueBelowMin_IsInvalid()
|
||||
{
|
||||
var param = TestHelpers.MakeIntParam("Radius", value: 5, min: 1, max: 10);
|
||||
var vm = new ProcessorParameterVM(param) { Value = 0 };
|
||||
|
||||
Assert.False(vm.IsValueValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParameterValidation_ValueAboveMax_IsInvalid()
|
||||
{
|
||||
var param = TestHelpers.MakeIntParam("Radius", value: 5, min: 1, max: 10);
|
||||
var vm = new ProcessorParameterVM(param) { Value = 11 };
|
||||
|
||||
Assert.False(vm.IsValueValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParameterValidation_BoundaryValues_AreValid()
|
||||
{
|
||||
var param = TestHelpers.MakeIntParam("Radius", value: 5, min: 1, max: 10);
|
||||
var vmMin = new ProcessorParameterVM(param) { Value = 1 };
|
||||
var vmMax = new ProcessorParameterVM(param) { Value = 10 };
|
||||
|
||||
Assert.True(vmMin.IsValueValid);
|
||||
Assert.True(vmMax.IsValueValid);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
using Moq;
|
||||
using Xunit;
|
||||
using System.Windows.Media.Imaging;
|
||||
using XplorePlane.Services;
|
||||
using XplorePlane.ViewModels;
|
||||
using XplorePlane.Tests.Helpers;
|
||||
using System.Threading.Tasks;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
||||
namespace XplorePlane.Tests.Pipeline
|
||||
{
|
||||
/// <summary>
|
||||
/// 测试 PipelineExecutionService(任务 6.6)
|
||||
/// </summary>
|
||||
public class PipelineExecutionServiceTests
|
||||
{
|
||||
private readonly Mock<IImageProcessingService> _mockImageSvc;
|
||||
private readonly PipelineExecutionService _svc;
|
||||
private readonly BitmapSource _testBitmap;
|
||||
|
||||
public PipelineExecutionServiceTests()
|
||||
{
|
||||
_mockImageSvc = TestHelpers.CreateMockImageService();
|
||||
_svc = new PipelineExecutionService(_mockImageSvc.Object);
|
||||
_testBitmap = TestHelpers.CreateTestBitmap();
|
||||
}
|
||||
|
||||
private static PipelineNodeViewModel MakeNode(string key, int order = 0, bool enabled = true)
|
||||
{
|
||||
var node = new PipelineNodeViewModel(key, key + "算子") { Order = order, IsEnabled = enabled };
|
||||
return node;
|
||||
}
|
||||
|
||||
// ── 空流水线 ──────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task EmptyPipeline_ReturnsSourceImage()
|
||||
{
|
||||
var result = await _svc.ExecutePipelineAsync(
|
||||
Enumerable.Empty<PipelineNodeViewModel>(), _testBitmap);
|
||||
|
||||
Assert.Same(_testBitmap, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NullNodes_ReturnsSourceImage()
|
||||
{
|
||||
var result = await _svc.ExecutePipelineAsync(null!, _testBitmap);
|
||||
|
||||
Assert.Same(_testBitmap, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AllDisabledNodes_ReturnsSourceImage()
|
||||
{
|
||||
var nodes = new[] { MakeNode("Blur", 0, enabled: false) };
|
||||
var result = await _svc.ExecutePipelineAsync(nodes, _testBitmap);
|
||||
|
||||
Assert.Same(_testBitmap, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NullSource_ThrowsArgumentNullException()
|
||||
{
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(
|
||||
() => _svc.ExecutePipelineAsync(Enumerable.Empty<PipelineNodeViewModel>(), null!));
|
||||
}
|
||||
|
||||
// ── 取消令牌 ──────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task CancelledToken_ThrowsOperationCanceledException()
|
||||
{
|
||||
// 让 ProcessImageAsync 在执行时检查取消令牌
|
||||
_mockImageSvc.Setup(s => s.ProcessImageAsync(
|
||||
It.IsAny<BitmapSource>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<IDictionary<string, object>>(),
|
||||
It.IsAny<IProgress<double>>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Returns<BitmapSource, string, IDictionary<string, object>, IProgress<double>, CancellationToken>(
|
||||
(src, _, _, _, ct) =>
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
return Task.FromResult(src);
|
||||
});
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
cts.Cancel();
|
||||
|
||||
var nodes = new[] { MakeNode("Blur", 0) };
|
||||
|
||||
await Assert.ThrowsAsync<OperationCanceledException>(
|
||||
() => _svc.ExecutePipelineAsync(nodes, _testBitmap, cancellationToken: cts.Token));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PreCancelledToken_ThrowsBeforeExecution()
|
||||
{
|
||||
using var cts = new CancellationTokenSource();
|
||||
cts.Cancel();
|
||||
|
||||
var nodes = new[] { MakeNode("Blur", 0) };
|
||||
|
||||
await Assert.ThrowsAsync<OperationCanceledException>(
|
||||
() => _svc.ExecutePipelineAsync(nodes, _testBitmap, cancellationToken: cts.Token));
|
||||
|
||||
// ProcessImageAsync 不应被调用
|
||||
_mockImageSvc.Verify(s => s.ProcessImageAsync(
|
||||
It.IsAny<BitmapSource>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<IDictionary<string, object>>(),
|
||||
It.IsAny<IProgress<double>>(),
|
||||
It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
// ── 节点失败包装 ──────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task NodeThrows_WrappedAsPipelineExecutionException()
|
||||
{
|
||||
_mockImageSvc.Setup(s => s.ProcessImageAsync(
|
||||
It.IsAny<BitmapSource>(),
|
||||
"Blur",
|
||||
It.IsAny<IDictionary<string, object>>(),
|
||||
It.IsAny<IProgress<double>>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ThrowsAsync(new InvalidOperationException("处理失败"));
|
||||
|
||||
var nodes = new[] { MakeNode("Blur", 0) };
|
||||
|
||||
var ex = await Assert.ThrowsAsync<PipelineExecutionException>(
|
||||
() => _svc.ExecutePipelineAsync(nodes, _testBitmap));
|
||||
|
||||
Assert.Equal("Blur", ex.FailedOperatorKey);
|
||||
Assert.Equal(0, ex.FailedNodeOrder);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NodeReturnsNull_ThrowsPipelineExecutionException()
|
||||
{
|
||||
_mockImageSvc.Setup(s => s.ProcessImageAsync(
|
||||
It.IsAny<BitmapSource>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<IDictionary<string, object>>(),
|
||||
It.IsAny<IProgress<double>>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((BitmapSource?)null);
|
||||
|
||||
var nodes = new[] { MakeNode("Blur", 0) };
|
||||
|
||||
await Assert.ThrowsAsync<PipelineExecutionException>(
|
||||
() => _svc.ExecutePipelineAsync(nodes, _testBitmap));
|
||||
}
|
||||
|
||||
// ── 进度回调 ──────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task Progress_ReportedForEachNode()
|
||||
{
|
||||
var nodes = new[]
|
||||
{
|
||||
MakeNode("Blur", 0),
|
||||
MakeNode("Sharpen", 1)
|
||||
};
|
||||
|
||||
var reports = new List<PipelineProgress>();
|
||||
var progress = new Progress<PipelineProgress>(p => reports.Add(p));
|
||||
|
||||
await _svc.ExecutePipelineAsync(nodes, _testBitmap, progress);
|
||||
|
||||
// 等待 Progress 回调(Progress<T> 是异步的)
|
||||
await Task.Delay(50);
|
||||
|
||||
Assert.Equal(2, reports.Count);
|
||||
Assert.Equal(1, reports[0].CurrentStep);
|
||||
Assert.Equal(2, reports[1].CurrentStep);
|
||||
}
|
||||
|
||||
// ── 节点按 Order 排序执行 ─────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task Nodes_ExecutedInOrderAscending()
|
||||
{
|
||||
var executionOrder = new List<string>();
|
||||
_mockImageSvc.Setup(s => s.ProcessImageAsync(
|
||||
It.IsAny<BitmapSource>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<IDictionary<string, object>>(),
|
||||
It.IsAny<IProgress<double>>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Returns<BitmapSource, string, IDictionary<string, object>, IProgress<double>, CancellationToken>(
|
||||
(src, key, _, _, _) =>
|
||||
{
|
||||
executionOrder.Add(key);
|
||||
return Task.FromResult(src);
|
||||
});
|
||||
|
||||
// 故意乱序传入
|
||||
var nodes = new[]
|
||||
{
|
||||
MakeNode("Threshold", 2),
|
||||
MakeNode("Blur", 0),
|
||||
MakeNode("Sharpen", 1)
|
||||
};
|
||||
|
||||
await _svc.ExecutePipelineAsync(nodes, _testBitmap);
|
||||
|
||||
Assert.Equal(new[] { "Blur", "Sharpen", "Threshold" }, executionOrder);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
using Xunit;
|
||||
using XplorePlane.Models;
|
||||
using XplorePlane.Services;
|
||||
using XplorePlane.Tests.Helpers;
|
||||
using System.Threading.Tasks;
|
||||
using System.IO;
|
||||
using System;
|
||||
|
||||
namespace XplorePlane.Tests.Pipeline
|
||||
{
|
||||
/// <summary>
|
||||
/// 测试 PipelinePersistenceService(任务 6.5)
|
||||
/// </summary>
|
||||
public class PipelinePersistenceServiceTests : IDisposable
|
||||
{
|
||||
private readonly string _tempDir;
|
||||
private readonly PipelinePersistenceService _svc;
|
||||
|
||||
public PipelinePersistenceServiceTests()
|
||||
{
|
||||
_tempDir = Path.Combine(Path.GetTempPath(), $"XplorePlaneTests_{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(_tempDir);
|
||||
|
||||
var mockImageSvc = TestHelpers.CreateMockImageService(new[] { "Blur", "Sharpen" });
|
||||
_svc = new PipelinePersistenceService(mockImageSvc.Object, _tempDir);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Directory.Exists(_tempDir))
|
||||
Directory.Delete(_tempDir, recursive: true);
|
||||
}
|
||||
|
||||
private static PipelineModel BuildModel(string name = "TestPipeline", params string[] keys)
|
||||
{
|
||||
var model = new PipelineModel { Name = name };
|
||||
for (int i = 0; i < keys.Length; i++)
|
||||
model.Nodes.Add(new PipelineNodeModel { OperatorKey = keys[i], Order = i });
|
||||
return model;
|
||||
}
|
||||
|
||||
// ── 往返一致性 ────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task SaveAndLoad_RoundTrip_NamePreserved()
|
||||
{
|
||||
var model = BuildModel("MyPipeline", "Blur", "Sharpen");
|
||||
var path = Path.Combine(_tempDir, "test.pipeline.json");
|
||||
|
||||
await _svc.SaveAsync(model, path);
|
||||
var loaded = await _svc.LoadAsync(path);
|
||||
|
||||
Assert.Equal("MyPipeline", loaded.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SaveAndLoad_RoundTrip_NodeCountPreserved()
|
||||
{
|
||||
var model = BuildModel("P", "Blur", "Sharpen");
|
||||
var path = Path.Combine(_tempDir, "test.pipeline.json");
|
||||
|
||||
await _svc.SaveAsync(model, path);
|
||||
var loaded = await _svc.LoadAsync(path);
|
||||
|
||||
Assert.Equal(2, loaded.Nodes.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SaveAndLoad_RoundTrip_OperatorKeysPreserved()
|
||||
{
|
||||
var model = BuildModel("P", "Blur", "Sharpen");
|
||||
var path = Path.Combine(_tempDir, "test.pipeline.json");
|
||||
|
||||
await _svc.SaveAsync(model, path);
|
||||
var loaded = await _svc.LoadAsync(path);
|
||||
|
||||
Assert.Equal("Blur", loaded.Nodes[0].OperatorKey);
|
||||
Assert.Equal("Sharpen", loaded.Nodes[1].OperatorKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SaveAndLoad_RoundTrip_OrderIsSequential()
|
||||
{
|
||||
var model = BuildModel("P", "Blur", "Sharpen");
|
||||
var path = Path.Combine(_tempDir, "test.pipeline.json");
|
||||
|
||||
await _svc.SaveAsync(model, path);
|
||||
var loaded = await _svc.LoadAsync(path);
|
||||
|
||||
for (int i = 0; i < loaded.Nodes.Count; i++)
|
||||
Assert.Equal(i, loaded.Nodes[i].Order);
|
||||
}
|
||||
|
||||
// ── 非法路径被拒绝 ────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task SaveAsync_PathTraversal_ThrowsUnauthorized()
|
||||
{
|
||||
var model = BuildModel("P");
|
||||
await Assert.ThrowsAsync<UnauthorizedAccessException>(
|
||||
() => _svc.SaveAsync(model, "../../../evil.json"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_PathTraversal_ThrowsUnauthorized()
|
||||
{
|
||||
await Assert.ThrowsAsync<UnauthorizedAccessException>(
|
||||
() => _svc.LoadAsync("../../etc/passwd"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_EmptyPath_ThrowsArgumentException()
|
||||
{
|
||||
await Assert.ThrowsAsync<ArgumentException>(
|
||||
() => _svc.LoadAsync(string.Empty));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_NullPath_ThrowsArgumentException()
|
||||
{
|
||||
await Assert.ThrowsAsync<ArgumentException>(
|
||||
() => _svc.LoadAsync(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_FileNotFound_ThrowsFileNotFoundException()
|
||||
{
|
||||
var path = Path.Combine(_tempDir, "nonexistent.pipeline.json");
|
||||
await Assert.ThrowsAsync<FileNotFoundException>(
|
||||
() => _svc.LoadAsync(path));
|
||||
}
|
||||
|
||||
// ── 非法 OperatorKey 被跳过 ───────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task Load_UnregisteredOperatorKey_NodeSkipped()
|
||||
{
|
||||
// 手动写入包含未注册 Key 的 JSON
|
||||
var json = """
|
||||
{
|
||||
"Id": "00000000-0000-0000-0000-000000000001",
|
||||
"Name": "Test",
|
||||
"DeviceId": "",
|
||||
"Nodes": [
|
||||
{ "Id": "00000000-0000-0000-0000-000000000002", "OperatorKey": "Blur", "Order": 0, "IsEnabled": true, "Parameters": {} },
|
||||
{ "Id": "00000000-0000-0000-0000-000000000003", "OperatorKey": "UNKNOWN_KEY", "Order": 1, "IsEnabled": true, "Parameters": {} }
|
||||
]
|
||||
}
|
||||
""";
|
||||
var path = Path.Combine(_tempDir, "mixed.pipeline.json");
|
||||
await File.WriteAllTextAsync(path, json);
|
||||
|
||||
var loaded = await _svc.LoadAsync(path);
|
||||
|
||||
Assert.Single(loaded.Nodes);
|
||||
Assert.Equal("Blur", loaded.Nodes[0].OperatorKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Load_AllUnregisteredKeys_ReturnsEmptyNodes()
|
||||
{
|
||||
var json = """
|
||||
{
|
||||
"Id": "00000000-0000-0000-0000-000000000001",
|
||||
"Name": "Test",
|
||||
"DeviceId": "",
|
||||
"Nodes": [
|
||||
{ "Id": "00000000-0000-0000-0000-000000000002", "OperatorKey": "FAKE1", "Order": 0, "IsEnabled": true, "Parameters": {} },
|
||||
{ "Id": "00000000-0000-0000-0000-000000000003", "OperatorKey": "FAKE2", "Order": 1, "IsEnabled": true, "Parameters": {} }
|
||||
]
|
||||
}
|
||||
""";
|
||||
var path = Path.Combine(_tempDir, "all_fake.pipeline.json");
|
||||
await File.WriteAllTextAsync(path, json);
|
||||
|
||||
var loaded = await _svc.LoadAsync(path);
|
||||
|
||||
Assert.Empty(loaded.Nodes);
|
||||
}
|
||||
|
||||
// ── LoadAllAsync ──────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAllAsync_EmptyDirectory_ReturnsEmpty()
|
||||
{
|
||||
var result = await _svc.LoadAllAsync(_tempDir);
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAllAsync_MultipleFiles_ReturnsAll()
|
||||
{
|
||||
var m1 = BuildModel("P1", "Blur");
|
||||
var m2 = BuildModel("P2", "Sharpen");
|
||||
await _svc.SaveAsync(m1, Path.Combine(_tempDir, "p1.pipeline.json"));
|
||||
await _svc.SaveAsync(m2, Path.Combine(_tempDir, "p2.pipeline.json"));
|
||||
|
||||
var result = await _svc.LoadAllAsync(_tempDir);
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
using FsCheck;
|
||||
using FsCheck.Fluent;
|
||||
using FsCheck.Xunit;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using XplorePlane.Models;
|
||||
using XplorePlane.Services;
|
||||
using XplorePlane.ViewModels;
|
||||
using XplorePlane.Tests.Helpers;
|
||||
|
||||
namespace XplorePlane.Tests.Pipeline
|
||||
{
|
||||
/// <summary>
|
||||
/// 属性测试(Property-Based Tests)使用 FsCheck
|
||||
/// 覆盖任务 7.1 ~ 7.7
|
||||
/// </summary>
|
||||
public class PipelinePropertyTests
|
||||
{
|
||||
private static readonly string[] Keys = TestHelpers.DefaultKeys;
|
||||
|
||||
private PipelineEditorViewModel CreateVm()
|
||||
{
|
||||
var mockImageSvc = TestHelpers.CreateMockImageService();
|
||||
var mockExecSvc = new Mock<IPipelineExecutionService>();
|
||||
var mockPersistSvc = new Mock<IPipelinePersistenceService>();
|
||||
return new PipelineEditorViewModel(mockImageSvc.Object, mockExecSvc.Object, mockPersistSvc.Object);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 向 VM 中添加 n 个节点(循环使用 Keys)
|
||||
/// </summary>
|
||||
private static void PopulateNodes(PipelineEditorViewModel vm, int count)
|
||||
{
|
||||
for (int i = 0; i < count; i++)
|
||||
vm.AddOperatorCommand.Execute(Keys[i % Keys.Length]);
|
||||
}
|
||||
|
||||
// ── Property 6:ReorderNode 后 Order == 下标 ──────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 任务 7.1:对任意合法 oldIndex/newIndex,ReorderNode 后所有 Order == 下标
|
||||
/// </summary>
|
||||
[Property(MaxTest = 200)]
|
||||
public Property ReorderNode_OrderEqualsIndex()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
Arb.From(Gen.Choose(2, 8)), // nodeCount: 2..8
|
||||
Arb.From(Gen.Choose(0, 7)), // oldIndex (clamped below)
|
||||
Arb.From(Gen.Choose(0, 7)), // newIndex (clamped below)
|
||||
(nodeCount, rawOld, rawNew) =>
|
||||
{
|
||||
var vm = CreateVm();
|
||||
PopulateNodes(vm, nodeCount);
|
||||
|
||||
int oldIdx = rawOld % nodeCount;
|
||||
int newIdx = rawNew % nodeCount;
|
||||
|
||||
vm.ReorderOperatorCommand.Execute(new PipelineReorderArgs
|
||||
{
|
||||
OldIndex = oldIdx,
|
||||
NewIndex = newIdx
|
||||
});
|
||||
|
||||
return vm.PipelineNodes
|
||||
.Select((n, i) => n.Order == i)
|
||||
.All(x => x);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Property 7:RemoveOperator 后节点不存在且 Order 连续 ───────
|
||||
|
||||
/// <summary>
|
||||
/// 任务 7.2:对任意节点,RemoveOperator 后节点不存在且剩余 Order 连续
|
||||
/// </summary>
|
||||
[Property(MaxTest = 200)]
|
||||
public Property RemoveOperator_NodeAbsentAndOrderContinuous()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
Arb.From(Gen.Choose(1, 8)),
|
||||
Arb.From(Gen.Choose(0, 7)),
|
||||
(nodeCount, rawIdx) =>
|
||||
{
|
||||
var vm = CreateVm();
|
||||
PopulateNodes(vm, nodeCount);
|
||||
|
||||
int idx = rawIdx % nodeCount;
|
||||
var target = vm.PipelineNodes[idx];
|
||||
|
||||
vm.RemoveOperatorCommand.Execute(target);
|
||||
|
||||
bool notPresent = !vm.PipelineNodes.Contains(target);
|
||||
bool orderContinuous = vm.PipelineNodes
|
||||
.Select((n, i) => n.Order == i)
|
||||
.All(x => x);
|
||||
|
||||
return notPresent && orderContinuous;
|
||||
});
|
||||
}
|
||||
|
||||
// ── Property 3:AddOperator 后末尾节点正确 ────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 任务 7.3:对任意已注册 Key,AddOperator 后末尾节点 OperatorKey 和 Order 正确
|
||||
/// </summary>
|
||||
[Property(MaxTest = 200)]
|
||||
public Property AddOperator_LastNodeCorrect()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
Arb.From(Gen.Elements(Keys)), // 随机选一个合法 Key
|
||||
Arb.From(Gen.Choose(0, 5)), // 已有节点数
|
||||
(key, existingCount) =>
|
||||
{
|
||||
var vm = CreateVm();
|
||||
PopulateNodes(vm, existingCount);
|
||||
|
||||
int expectedOrder = vm.PipelineNodes.Count;
|
||||
vm.AddOperatorCommand.Execute(key);
|
||||
|
||||
var last = vm.PipelineNodes.LastOrDefault();
|
||||
return last != null
|
||||
&& last.OperatorKey == key
|
||||
&& last.Order == expectedOrder;
|
||||
});
|
||||
}
|
||||
|
||||
// ── Property 5:未注册 Key 不改变节点数 ──────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 任务 7.4:对任意未注册 Key,AddOperator 后 PipelineNodes 长度不变
|
||||
/// </summary>
|
||||
[Property(MaxTest = 200)]
|
||||
public Property AddOperator_UnregisteredKey_LengthUnchanged()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
Arb.From(Gen.Choose(0, 5)),
|
||||
(existingCount) =>
|
||||
{
|
||||
var vm = CreateVm();
|
||||
PopulateNodes(vm, existingCount);
|
||||
|
||||
int before = vm.PipelineNodes.Count;
|
||||
vm.AddOperatorCommand.Execute("UNREGISTERED_KEY_XYZ");
|
||||
|
||||
return vm.PipelineNodes.Count == before;
|
||||
});
|
||||
}
|
||||
|
||||
// ── Property 14:Save/Load 往返一致性 ────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 任务 7.5:对任意合法 PipelineModel,Save 后 Load 得到字段一致的对象
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property SaveLoad_RoundTrip_FieldsConsistent()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
Arb.From(Gen.Choose(0, 5)),
|
||||
(nodeCount) =>
|
||||
{
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), $"PBT_{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(tempDir);
|
||||
|
||||
try
|
||||
{
|
||||
var mockImageSvc = TestHelpers.CreateMockImageService(new[] { "Blur", "Sharpen" });
|
||||
var svc = new PipelinePersistenceService(mockImageSvc.Object, tempDir);
|
||||
|
||||
var model = new PipelineModel { Name = $"Pipeline_{nodeCount}" };
|
||||
for (int i = 0; i < nodeCount; i++)
|
||||
model.Nodes.Add(new PipelineNodeModel
|
||||
{
|
||||
OperatorKey = i % 2 == 0 ? "Blur" : "Sharpen",
|
||||
Order = i
|
||||
});
|
||||
|
||||
var path = Path.Combine(tempDir, "test.pipeline.json");
|
||||
svc.SaveAsync(model, path).GetAwaiter().GetResult();
|
||||
var loaded = svc.LoadAsync(path).GetAwaiter().GetResult();
|
||||
|
||||
return loaded.Name == model.Name
|
||||
&& loaded.Nodes.Count == model.Nodes.Count
|
||||
&& loaded.Nodes.Select((n, i) => n.Order == i).All(x => x);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Directory.Exists(tempDir))
|
||||
Directory.Delete(tempDir, recursive: true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Property 12:取消令牌触发 OperationCanceledException ──────
|
||||
|
||||
/// <summary>
|
||||
/// 任务 7.6:对任意流水线,取消令牌触发后抛出 OperationCanceledException
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property CancelToken_AlwaysThrowsOperationCanceled()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
Arb.From(Gen.Choose(1, 5)),
|
||||
(nodeCount) =>
|
||||
{
|
||||
var mockImageSvc = TestHelpers.CreateMockImageService();
|
||||
mockImageSvc.Setup(s => s.ProcessImageAsync(
|
||||
It.IsAny<System.Windows.Media.Imaging.BitmapSource>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<IDictionary<string, object>>(),
|
||||
It.IsAny<IProgress<double>>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Returns<System.Windows.Media.Imaging.BitmapSource, string,
|
||||
IDictionary<string, object>, IProgress<double>, CancellationToken>(
|
||||
(src, _, _, _, ct) =>
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
return Task.FromResult(src);
|
||||
});
|
||||
|
||||
var svc = new PipelineExecutionService(mockImageSvc.Object);
|
||||
var bitmap = TestHelpers.CreateTestBitmap();
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
cts.Cancel();
|
||||
|
||||
var nodes = Enumerable.Range(0, nodeCount)
|
||||
.Select(i => new PipelineNodeViewModel(Keys[i % Keys.Length], "N") { Order = i })
|
||||
.ToList();
|
||||
|
||||
try
|
||||
{
|
||||
svc.ExecutePipelineAsync(nodes, bitmap, cancellationToken: cts.Token)
|
||||
.GetAwaiter().GetResult();
|
||||
return false; // 应该抛出异常
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Property 2:FilteredOperators 每项均包含搜索文本 ─────────
|
||||
|
||||
/// <summary>
|
||||
/// 任务 7.7:对任意搜索文本,FilteredOperators 中每个算子 DisplayName 均包含该文本
|
||||
/// </summary>
|
||||
[Property(MaxTest = 200)]
|
||||
public Property FilteredOperators_AllContainSearchText()
|
||||
{
|
||||
// 生成非空、非空白的搜索字符串(限制长度避免无意义测试)
|
||||
var searchGen = Gen.Elements("Blur", "Sharpen", "Threshold", "Denoise", "Contrast",
|
||||
"bl", "sh", "th", "de", "co", "a", "e", "r");
|
||||
|
||||
return Prop.ForAll(
|
||||
Arb.From(searchGen),
|
||||
(searchText) =>
|
||||
{
|
||||
var mockImageSvc = TestHelpers.CreateMockImageService(
|
||||
new[] { "Blur", "Sharpen", "Threshold", "Denoise", "Contrast" });
|
||||
var vm = new OperatorToolboxViewModel(mockImageSvc.Object);
|
||||
vm.SearchText = searchText;
|
||||
|
||||
return vm.FilteredOperators.All(op =>
|
||||
op.DisplayName.Contains(searchText, StringComparison.OrdinalIgnoreCase));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<UseWPF>true</UseWPF>
|
||||
<RootNamespace>XplorePlane.Tests</RootNamespace>
|
||||
<IsPackable>false</IsPackable>
|
||||
<Nullable>enable</Nullable>
|
||||
</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" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\XplorePlane\XplorePlane.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- 直接引用主项目依赖的本地 DLL -->
|
||||
<ItemGroup>
|
||||
<Reference Include="ImageProcessing.Core">
|
||||
<HintPath>..\XplorePlane\Libs\ImageProcessing\ImageProcessing.Core.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user