Files
XplorePlane/XplorePlane.Tests/Pipeline/PipelinePropertyTests.cs
T
2026-03-14 23:58:38 +08:00

279 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 6ReorderNode 后 Order == 下标 ──────────────────
/// <summary>
/// 任务 7.1:对任意合法 oldIndex/newIndexReorderNode 后所有 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 7RemoveOperator 后节点不存在且 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 3AddOperator 后末尾节点正确 ────────────────────
/// <summary>
/// 任务 7.3:对任意已注册 KeyAddOperator 后末尾节点 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:对任意未注册 KeyAddOperator 后 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 14Save/Load 往返一致性 ────────────────────────
/// <summary>
/// 任务 7.5:对任意合法 PipelineModelSave 后 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 2FilteredOperators 每项均包含搜索文本 ─────────
/// <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));
});
}
}
}