#0021 增加测试工程

This commit is contained in:
zhengxuan.zhang
2026-03-14 23:58:38 +08:00
parent 7c7d0b6a20
commit 13d140bcfe
15 changed files with 1235 additions and 19 deletions
@@ -0,0 +1,215 @@
using Moq;
using Xunit;
using System.Windows.Media.Imaging;
using XplorePlane.Services;
using XplorePlane.ViewModels;
using XplorePlane.Tests.Helpers;
using System.Threading.Tasks;
using System;
using System.Linq;
using System.Collections.Generic;
using System.Threading;
namespace XplorePlane.Tests.Pipeline
{
/// <summary>
/// 测试 PipelineExecutionService(任务 6.6
/// </summary>
public class PipelineExecutionServiceTests
{
private readonly Mock<IImageProcessingService> _mockImageSvc;
private readonly PipelineExecutionService _svc;
private readonly BitmapSource _testBitmap;
public PipelineExecutionServiceTests()
{
_mockImageSvc = TestHelpers.CreateMockImageService();
_svc = new PipelineExecutionService(_mockImageSvc.Object);
_testBitmap = TestHelpers.CreateTestBitmap();
}
private static PipelineNodeViewModel MakeNode(string key, int order = 0, bool enabled = true)
{
var node = new PipelineNodeViewModel(key, key + "算子") { Order = order, IsEnabled = enabled };
return node;
}
// ── 空流水线 ──────────────────────────────────────────────────
[Fact]
public async Task EmptyPipeline_ReturnsSourceImage()
{
var result = await _svc.ExecutePipelineAsync(
Enumerable.Empty<PipelineNodeViewModel>(), _testBitmap);
Assert.Same(_testBitmap, result);
}
[Fact]
public async Task NullNodes_ReturnsSourceImage()
{
var result = await _svc.ExecutePipelineAsync(null!, _testBitmap);
Assert.Same(_testBitmap, result);
}
[Fact]
public async Task AllDisabledNodes_ReturnsSourceImage()
{
var nodes = new[] { MakeNode("Blur", 0, enabled: false) };
var result = await _svc.ExecutePipelineAsync(nodes, _testBitmap);
Assert.Same(_testBitmap, result);
}
[Fact]
public async Task NullSource_ThrowsArgumentNullException()
{
await Assert.ThrowsAsync<ArgumentNullException>(
() => _svc.ExecutePipelineAsync(Enumerable.Empty<PipelineNodeViewModel>(), null!));
}
// ── 取消令牌 ──────────────────────────────────────────────────
[Fact]
public async Task CancelledToken_ThrowsOperationCanceledException()
{
// 让 ProcessImageAsync 在执行时检查取消令牌
_mockImageSvc.Setup(s => s.ProcessImageAsync(
It.IsAny<BitmapSource>(),
It.IsAny<string>(),
It.IsAny<IDictionary<string, object>>(),
It.IsAny<IProgress<double>>(),
It.IsAny<CancellationToken>()))
.Returns<BitmapSource, string, IDictionary<string, object>, IProgress<double>, CancellationToken>(
(src, _, _, _, ct) =>
{
ct.ThrowIfCancellationRequested();
return Task.FromResult(src);
});
using var cts = new CancellationTokenSource();
cts.Cancel();
var nodes = new[] { MakeNode("Blur", 0) };
await Assert.ThrowsAsync<OperationCanceledException>(
() => _svc.ExecutePipelineAsync(nodes, _testBitmap, cancellationToken: cts.Token));
}
[Fact]
public async Task PreCancelledToken_ThrowsBeforeExecution()
{
using var cts = new CancellationTokenSource();
cts.Cancel();
var nodes = new[] { MakeNode("Blur", 0) };
await Assert.ThrowsAsync<OperationCanceledException>(
() => _svc.ExecutePipelineAsync(nodes, _testBitmap, cancellationToken: cts.Token));
// ProcessImageAsync 不应被调用
_mockImageSvc.Verify(s => s.ProcessImageAsync(
It.IsAny<BitmapSource>(),
It.IsAny<string>(),
It.IsAny<IDictionary<string, object>>(),
It.IsAny<IProgress<double>>(),
It.IsAny<CancellationToken>()), Times.Never);
}
// ── 节点失败包装 ──────────────────────────────────────────────
[Fact]
public async Task NodeThrows_WrappedAsPipelineExecutionException()
{
_mockImageSvc.Setup(s => s.ProcessImageAsync(
It.IsAny<BitmapSource>(),
"Blur",
It.IsAny<IDictionary<string, object>>(),
It.IsAny<IProgress<double>>(),
It.IsAny<CancellationToken>()))
.ThrowsAsync(new InvalidOperationException("处理失败"));
var nodes = new[] { MakeNode("Blur", 0) };
var ex = await Assert.ThrowsAsync<PipelineExecutionException>(
() => _svc.ExecutePipelineAsync(nodes, _testBitmap));
Assert.Equal("Blur", ex.FailedOperatorKey);
Assert.Equal(0, ex.FailedNodeOrder);
}
[Fact]
public async Task NodeReturnsNull_ThrowsPipelineExecutionException()
{
_mockImageSvc.Setup(s => s.ProcessImageAsync(
It.IsAny<BitmapSource>(),
It.IsAny<string>(),
It.IsAny<IDictionary<string, object>>(),
It.IsAny<IProgress<double>>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync((BitmapSource?)null);
var nodes = new[] { MakeNode("Blur", 0) };
await Assert.ThrowsAsync<PipelineExecutionException>(
() => _svc.ExecutePipelineAsync(nodes, _testBitmap));
}
// ── 进度回调 ──────────────────────────────────────────────────
[Fact]
public async Task Progress_ReportedForEachNode()
{
var nodes = new[]
{
MakeNode("Blur", 0),
MakeNode("Sharpen", 1)
};
var reports = new List<PipelineProgress>();
var progress = new Progress<PipelineProgress>(p => reports.Add(p));
await _svc.ExecutePipelineAsync(nodes, _testBitmap, progress);
// 等待 Progress 回调(Progress<T> 是异步的)
await Task.Delay(50);
Assert.Equal(2, reports.Count);
Assert.Equal(1, reports[0].CurrentStep);
Assert.Equal(2, reports[1].CurrentStep);
}
// ── 节点按 Order 排序执行 ─────────────────────────────────────
[Fact]
public async Task Nodes_ExecutedInOrderAscending()
{
var executionOrder = new List<string>();
_mockImageSvc.Setup(s => s.ProcessImageAsync(
It.IsAny<BitmapSource>(),
It.IsAny<string>(),
It.IsAny<IDictionary<string, object>>(),
It.IsAny<IProgress<double>>(),
It.IsAny<CancellationToken>()))
.Returns<BitmapSource, string, IDictionary<string, object>, IProgress<double>, CancellationToken>(
(src, key, _, _, _) =>
{
executionOrder.Add(key);
return Task.FromResult(src);
});
// 故意乱序传入
var nodes = new[]
{
MakeNode("Threshold", 2),
MakeNode("Blur", 0),
MakeNode("Sharpen", 1)
};
await _svc.ExecutePipelineAsync(nodes, _testBitmap);
Assert.Equal(new[] { "Blur", "Sharpen", "Threshold" }, executionOrder);
}
}
}