CNC执行逻辑的开发,点击运行,停止

This commit is contained in:
zhengxuan.zhang
2026-04-27 16:18:47 +08:00
parent e24bfef3e6
commit 2a64d48b54
14 changed files with 1450 additions and 16 deletions
@@ -0,0 +1,232 @@
// 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<ICncExecutionService> mockExecSvc = null,
CncProgram initialProgram = null)
{
var mockCncProgramSvc = new Mock<ICncProgramService>();
var mockAppState = new Mock<IAppStateService>();
var mockLogger = new Mock<ILoggerService>();
mockLogger.Setup(l => l.ForModule<CncEditorViewModel>()).Returns(mockLogger.Object);
mockExecSvc ??= new Mock<ICncExecutionService>();
// Setup CreateProgram so ExecuteNewProgram works
mockCncProgramSvc
.Setup(s => s.CreateProgram(It.IsAny<string>()))
.Returns((string name) => new CncProgram(
Guid.NewGuid(), name,
DateTime.UtcNow, DateTime.UtcNow,
new List<CncNode>
{
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<bool>();
var mockExecSvc = new Mock<ICncExecutionService>();
mockExecSvc
.Setup(s => s.ExecuteAsync(
It.IsAny<CncProgram>(),
It.IsAny<IProgress<CncNodeExecutionProgress>>(),
It.IsAny<CancellationToken>()))
.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<bool>()
select (nodeCount, cancelled);
return Prop.ForAll(gen.ToArbitrary(), tuple =>
{
var (nodeCount, cancelled) = tuple;
var program = MakeProgram(nodeCount);
var tcs = new TaskCompletionSource<bool>();
var mockExecSvc = new Mock<ICncExecutionService>();
mockExecSvc
.Setup(s => s.ExecuteAsync(
It.IsAny<CncProgram>(),
It.IsAny<IProgress<CncNodeExecutionProgress>>(),
It.IsAny<CancellationToken>()))
.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<bool>();
var mockExecSvc = new Mock<ICncExecutionService>();
mockExecSvc
.Setup(s => s.ExecuteAsync(
It.IsAny<CncProgram>(),
It.IsAny<IProgress<CncNodeExecutionProgress>>(),
It.IsAny<CancellationToken>()))
.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;
});
}
}
}