233 lines
9.5 KiB
C#
233 lines
9.5 KiB
C#
// 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;
|
|
});
|
|
}
|
|
}
|
|
}
|