Files
XplorePlane/XplorePlane.Tests/Pipeline/PipelineExecutionServiceTests.cs
T
2026-03-14 23:58:38 +08:00

216 lines
8.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
}
}