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)); }); } } }