157 lines
5.4 KiB
C#
157 lines
5.4 KiB
C#
using ClosedXML.Excel;
|
|
using FluentAssertions;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Options;
|
|
using Workflow.Application.BuildFlow.Common.Services;
|
|
using Workflow.Application.BuildFlow.Exports.Handlers;
|
|
using Workflow.Application.BuildFlow.Exports.Queries;
|
|
using Workflow.Application.Reliability;
|
|
using Workflow.Domain.Entities;
|
|
using Workflow.Domain.Enums;
|
|
using Xunit;
|
|
|
|
namespace BuildFlow.Workflow.Tests.Performance;
|
|
|
|
/// <summary>Excel 导出的 keyset 分页、顺序写入和临时文件清理边界测试。</summary>
|
|
[Collection(Reliability.PostgresWorkflowCollection.Name)]
|
|
public class OpenXmlExportBoundaryTests(Reliability.PostgresWorkflowFixture fixture)
|
|
{
|
|
[Fact]
|
|
public async Task TwelveHundredOneRowsUseThreePagesAndKeepStableOrder()
|
|
{
|
|
await ResetAsync();
|
|
var formId = await SeedFormAsync();
|
|
await SeedRowsAsync(formId, 1_201);
|
|
var interceptor = new CountingExportPageInterceptor();
|
|
await using var db = fixture.CreateDbContext(interceptor);
|
|
var handler = CreateHandler(db, new FormSchemaExcelService(), exportPageSize: 500);
|
|
string? filePath = null;
|
|
|
|
try
|
|
{
|
|
var result = await handler.Handle(
|
|
new ExportFormDataQuery(formId),
|
|
CancellationToken.None);
|
|
filePath = result.FilePath;
|
|
|
|
interceptor.PageSelectCount.Should().Be(3);
|
|
File.Exists(filePath).Should().BeTrue();
|
|
typeof(ExportFormDataResult).GetProperties()
|
|
.Should().NotContain(property => property.PropertyType == typeof(byte[]));
|
|
using var workbook = new XLWorkbook(filePath);
|
|
var worksheet = workbook.Worksheets.First();
|
|
worksheet.LastRowUsed()!.RowNumber().Should().Be(1_202);
|
|
for (var index = 0; index < 1_201; index++)
|
|
{
|
|
worksheet.Cell(index + 2, 1).GetString()
|
|
.Should().Be($"row-{index:D4}");
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (filePath is not null)
|
|
{
|
|
File.Delete(filePath);
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CancellationDeletesTemporaryExportFile()
|
|
{
|
|
await ResetAsync();
|
|
var formId = await SeedFormAsync();
|
|
using var cts = new CancellationTokenSource();
|
|
var excel = new CancellingExportExcelService(cts);
|
|
await using var db = fixture.CreateDbContext();
|
|
var handler = CreateHandler(db, excel, exportPageSize: 500);
|
|
|
|
var act = () => handler.Handle(new ExportFormDataQuery(formId), cts.Token);
|
|
|
|
await act.Should().ThrowAsync<OperationCanceledException>();
|
|
excel.OutputPath.Should().NotBeNull();
|
|
File.Exists(excel.OutputPath).Should().BeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task WriterFailureDeletesTemporaryExportFile()
|
|
{
|
|
await ResetAsync();
|
|
var formId = await SeedFormAsync();
|
|
var excel = new CancellingExportExcelService(
|
|
exception: new InvalidOperationException("writer failed"));
|
|
await using var db = fixture.CreateDbContext();
|
|
var handler = CreateHandler(db, excel, exportPageSize: 500);
|
|
|
|
var act = () => handler.Handle(new ExportFormDataQuery(formId), CancellationToken.None);
|
|
|
|
await act.Should().ThrowAsync<InvalidOperationException>();
|
|
excel.OutputPath.Should().NotBeNull();
|
|
File.Exists(excel.OutputPath).Should().BeFalse();
|
|
}
|
|
|
|
private static ExportFormDataQueryHandler CreateHandler(
|
|
WorkflowDbContext db,
|
|
IFormSchemaExcelService excelService,
|
|
int exportPageSize)
|
|
=> new(
|
|
db,
|
|
excelService,
|
|
Options.Create(new WorkflowJobOptions
|
|
{
|
|
Import = new WorkflowJobOptions.ImportJobSection
|
|
{
|
|
ExportPageSize = exportPageSize,
|
|
},
|
|
}));
|
|
|
|
private async Task ResetAsync()
|
|
{
|
|
await using var db = fixture.CreateDbContext();
|
|
await db.FormData.ExecuteDeleteAsync();
|
|
await db.FormDefinitions.ExecuteDeleteAsync();
|
|
}
|
|
|
|
private async Task<Guid> SeedFormAsync()
|
|
{
|
|
await using var db = fixture.CreateDbContext();
|
|
var formId = Guid.NewGuid();
|
|
db.FormDefinitions.Add(new FormDefinition
|
|
{
|
|
Id = formId,
|
|
Name = "分页导出测试",
|
|
Code = $"export-{formId:N}",
|
|
Version = 1,
|
|
SchemaJson = Schema,
|
|
Status = FormStatus.Published,
|
|
});
|
|
await db.SaveChangesAsync();
|
|
return formId;
|
|
}
|
|
|
|
private async Task SeedRowsAsync(Guid formId, int count)
|
|
{
|
|
await using var db = fixture.CreateDbContext();
|
|
var start = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
|
db.FormData.AddRange(Enumerable.Range(0, count).Select(index => new FormData
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
FormDefinitionId = formId,
|
|
InstanceId = Guid.NewGuid(),
|
|
DataJson = $$"""{"name":"row-{{index:D4}}"}""",
|
|
CreatedAt = start.AddMilliseconds(index),
|
|
UpdatedAt = start.AddMilliseconds(index),
|
|
}));
|
|
await db.SaveChangesAsync();
|
|
}
|
|
|
|
private const string Schema = """
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"name": { "type": "string", "title": "名称", "x-component": "Input" }
|
|
}
|
|
}
|
|
""";
|
|
}
|