// Feature: cnc-run-execution // Properties 1, 2, 12: CncEditorViewModel execution control using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Threading; using System.Threading.Tasks; using FsCheck; using FsCheck.Fluent; using FsCheck.Xunit; using Moq; using Prism.Events; using XP.Common.Logging.Interfaces; using XplorePlane.Models; using XplorePlane.Services.AppState; using XplorePlane.Services.Cnc; using XplorePlane.ViewModels.Cnc; using Xunit; namespace XplorePlane.Tests.ViewModels { public class CncEditorViewModelTests { // ── Helpers ────────────────────────────────────────────────────────── private static CncEditorViewModel CreateVm( Mock mockExecSvc = null, CncProgram initialProgram = null) { var mockCncProgramSvc = new Mock(); var mockAppState = new Mock(); var mockLogger = new Mock(); mockLogger.Setup(l => l.ForModule()).Returns(mockLogger.Object); mockExecSvc ??= new Mock(); // Setup CreateProgram so ExecuteNewProgram works mockCncProgramSvc .Setup(s => s.CreateProgram(It.IsAny())) .Returns((string name) => new CncProgram( Guid.NewGuid(), name, DateTime.UtcNow, DateTime.UtcNow, new List { new ReferencePointNode(Guid.NewGuid(), 0, "参考点_0", 0, 0, 0, 0, 0, 0, false, 0, 0) }.AsReadOnly())); var vm = new CncEditorViewModel( mockCncProgramSvc.Object, mockAppState.Object, new EventAggregator(), mockLogger.Object, mockExecSvc.Object); if (initialProgram != null) { // Use reflection to set _currentProgram and refresh nodes var field = typeof(CncEditorViewModel) .GetField("_currentProgram", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); field?.SetValue(vm, initialProgram); var refresh = typeof(CncEditorViewModel) .GetMethod("RefreshNodes", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); refresh?.Invoke(vm, null); } return vm; } private static CncProgram MakeProgram(int nodeCount = 2) { var nodes = Enumerable.Range(0, nodeCount) .Select(i => (CncNode)new ReferencePointNode(Guid.NewGuid(), i, $"Node_{i}", 0, 0, 0, 0, 0, 0, false, 0, 0)) .ToList() .AsReadOnly(); return new CncProgram(Guid.NewGuid(), "TestProgram", DateTime.UtcNow, DateTime.UtcNow, nodes); } // ── Property 1: 运行/停止按钮状态互斥 ──────────────────────────────── // Feature: cnc-run-execution, Property 1: 运行/停止按钮状态互斥 // Validates: Requirements 1.1, 1.3, 1.4 [Property(MaxTest = 100)] public Property RunStop_Commands_AreMutuallyExclusive() { var gen = from nodeCount in Gen.Choose(1, 8) select MakeProgram(nodeCount); return Prop.ForAll(gen.ToArbitrary(), program => { var tcs = new TaskCompletionSource(); var mockExecSvc = new Mock(); mockExecSvc .Setup(s => s.ExecuteAsync( It.IsAny(), It.IsAny>(), It.IsAny())) .Returns(tcs.Task); var vm = CreateVm(mockExecSvc, program); // Before running: Run enabled, Stop disabled bool beforeRunOk = vm.RunCncCommand.CanExecute() && !vm.StopCncCommand.CanExecute(); // Start execution (fire-and-forget, task is blocked by tcs) _ = Task.Run(() => vm.RunCncCommand.Execute()); // Give the async method a moment to set IsRunning = true SpinWait.SpinUntil(() => vm.IsRunning, TimeSpan.FromMilliseconds(500)); bool duringRunOk = !vm.RunCncCommand.CanExecute() && vm.StopCncCommand.CanExecute(); // Complete execution tcs.SetResult(true); SpinWait.SpinUntil(() => !vm.IsRunning, TimeSpan.FromMilliseconds(500)); bool afterRunOk = vm.RunCncCommand.CanExecute() && !vm.StopCncCommand.CanExecute(); return beforeRunOk && duringRunOk && afterRunOk; }); } // ── Property 2: 执行完成后状态重置 ─────────────────────────────────── // Feature: cnc-run-execution, Property 2: 执行完成后状态重置 // Validates: Requirements 1.7, 6.5 [Property(MaxTest = 100)] public Property AfterExecution_IsRunningFalse_AllNodesIdle() { var gen = from nodeCount in Gen.Choose(1, 6) from cancelled in ArbMap.Default.GeneratorFor() select (nodeCount, cancelled); return Prop.ForAll(gen.ToArbitrary(), tuple => { var (nodeCount, cancelled) = tuple; var program = MakeProgram(nodeCount); var tcs = new TaskCompletionSource(); var mockExecSvc = new Mock(); mockExecSvc .Setup(s => s.ExecuteAsync( It.IsAny(), It.IsAny>(), It.IsAny())) .Returns(tcs.Task); var vm = CreateVm(mockExecSvc, program); // Start execution _ = Task.Run(() => vm.RunCncCommand.Execute()); SpinWait.SpinUntil(() => vm.IsRunning, TimeSpan.FromMilliseconds(500)); // Simulate some nodes going Running foreach (var node in vm.Nodes) node.ExecutionState = NodeExecutionState.Running; // Complete or cancel if (cancelled) tcs.SetCanceled(); else tcs.SetResult(true); SpinWait.SpinUntil(() => !vm.IsRunning, TimeSpan.FromMilliseconds(500)); bool isRunningFalse = !vm.IsRunning; bool allIdle = vm.Nodes.All(n => n.ExecutionState == NodeExecutionState.Idle); return isRunningFalse && allIdle; }); } // ── Property 12: 执行中编辑命令全部禁用 ────────────────────────────── // Feature: cnc-run-execution, Property 12: 执行中编辑命令全部禁用 // Validates: Requirements 6.7 [Property(MaxTest = 100)] public Property WhileRunning_AllEditCommands_AreDisabled() { var gen = from nodeCount in Gen.Choose(2, 8) select MakeProgram(nodeCount); return Prop.ForAll(gen.ToArbitrary(), program => { var tcs = new TaskCompletionSource(); var mockExecSvc = new Mock(); mockExecSvc .Setup(s => s.ExecuteAsync( It.IsAny(), It.IsAny>(), It.IsAny())) .Returns(tcs.Task); var vm = CreateVm(mockExecSvc, program); // Start execution _ = Task.Run(() => vm.RunCncCommand.Execute()); SpinWait.SpinUntil(() => vm.IsRunning, TimeSpan.FromMilliseconds(500)); // All insert/delete/move commands must be disabled var editCommands = new[] { vm.InsertReferencePointCommand.CanExecute(), vm.InsertSaveNodeWithImageCommand.CanExecute(), vm.InsertSaveNodeCommand.CanExecute(), vm.InsertSavePositionCommand.CanExecute(), vm.InsertInspectionModuleCommand.CanExecute(), vm.InsertInspectionMarkerCommand.CanExecute(), vm.InsertPauseDialogCommand.CanExecute(), vm.InsertWaitDelayCommand.CanExecute(), vm.InsertCompleteProgramCommand.CanExecute(), vm.DeleteNodeCommand.CanExecute(), }; bool allDisabled = editCommands.All(canExec => !canExec); // Cleanup tcs.SetResult(true); SpinWait.SpinUntil(() => !vm.IsRunning, TimeSpan.FromMilliseconds(500)); return allDisabled; }); } } }