216 lines
8.1 KiB
C#
216 lines
8.1 KiB
C#
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);
|
||
}
|
||
}
|
||
}
|