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
{
///
/// 属性测试(Property-Based Tests)使用 FsCheck
/// 覆盖任务 7.1 ~ 7.7
///
public class PipelinePropertyTests
{
private static readonly string[] Keys = TestHelpers.DefaultKeys;
private PipelineEditorViewModel CreateVm()
{
var mockImageSvc = TestHelpers.CreateMockImageService();
var mockExecSvc = new Mock();
var mockPersistSvc = new Mock();
return new PipelineEditorViewModel(mockImageSvc.Object, mockExecSvc.Object, mockPersistSvc.Object);
}
///
/// 向 VM 中添加 n 个节点(循环使用 Keys)
///
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 == 下标 ──────────────────
///
/// 任务 7.1:对任意合法 oldIndex/newIndex,ReorderNode 后所有 Order == 下标
///
[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 连续 ───────
///
/// 任务 7.2:对任意节点,RemoveOperator 后节点不存在且剩余 Order 连续
///
[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 后末尾节点正确 ────────────────────
///
/// 任务 7.3:对任意已注册 Key,AddOperator 后末尾节点 OperatorKey 和 Order 正确
///
[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 不改变节点数 ──────────────────────
///
/// 任务 7.4:对任意未注册 Key,AddOperator 后 PipelineNodes 长度不变
///
[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 往返一致性 ────────────────────────
///
/// 任务 7.5:对任意合法 PipelineModel,Save 后 Load 得到字段一致的对象
///
[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 ──────
///
/// 任务 7.6:对任意流水线,取消令牌触发后抛出 OperationCanceledException
///
[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(),
It.IsAny(),
It.IsAny>(),
It.IsAny>(),
It.IsAny()))
.Returns, IProgress, 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 每项均包含搜索文本 ─────────
///
/// 任务 7.7:对任意搜索文本,FilteredOperators 中每个算子 DisplayName 均包含该文本
///
[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));
});
}
}
}