Skip to content

Commit 7abb16a

Browse files
committed
test(IE): 新增 DrawingsFlush + ImageByteConsistency 回归测试
走 IExporter.Export API(不绕过 IE 直接调 EPPlus),覆盖图片导出 兜底路径和 SHA1 改动后字节一致性: DrawingsFlush_Tests: - LargeExport_AllImagesPersisted: 50 行 1 列走 IExporter.Export, reload 后验证 sheet.Drawings.Count 一致 - ReloadAfterExport_AllImagesRestored: 20 行导出后 reload, 验证 drawings 数量 + From 坐标不丢 - LargeExportViaBytes_AllImagesPersisted: ExportAsByteArray 路径 同样完整写出所有 drawings(MemoryStream 喂回 EPPlus 校验) ImageByteConsistency_Tests: - SingleVsRepeatedDrawings_ImageBytesIdentical: 1 行 vs 100 行 都走 IExporter.Export 生成 xlsx,对比 /xl/media/image1.* 的 zip-level CRC32,验证 SHA1 改动未影响图片字节内容 DTO OnePicRowDto 本地化在 DrawingsFlush_Tests 内部,避免与既有 ExportTestDataWithPricture(注意拼写)冲突。
1 parent c303b18 commit 7abb16a

1 file changed

Lines changed: 199 additions & 0 deletions

File tree

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.IO.Compression;
5+
using System.Linq;
6+
using System.Threading.Tasks;
7+
using Magicodes.ExporterAndImporter.Core;
8+
using Magicodes.ExporterAndImporter.Excel;
9+
using OfficeOpenXml;
10+
using OfficeOpenXml.Drawing;
11+
using Shouldly;
12+
using Xunit;
13+
using Xunit.Abstractions;
14+
15+
namespace Magicodes.ExporterAndImporter.Tests
16+
{
17+
/// <summary>
18+
/// 走 IE 导出器 (IExporter.Export) 而非裸调 EPPlus 的回归测试, 验证:
19+
/// 1. 大量图片导出后 drawings 全部持久化 (兜底 SaveAs)
20+
/// 2. 重新打开后 drawings 数量 / From 坐标不丢
21+
/// 3. 1 张图 vs 100 张同图的 xlsx 里 /xl/media/image1.* 内容字节级一致 (SHA1 改动未影响 byte)
22+
/// </summary>
23+
public class DrawingsFlush_Tests : TestBase
24+
{
25+
private readonly ITestOutputHelper _output;
26+
27+
public DrawingsFlush_Tests(ITestOutputHelper output)
28+
{
29+
_output = output;
30+
}
31+
32+
private static string GetSamplePngPath() =>
33+
Path.Combine("TestFiles", "ExporterTest.png");
34+
35+
[Fact(DisplayName = "DrawingsFlush_LargeExport_AllImagesPersisted")]
36+
public async Task LargeExport_AllImagesPersisted()
37+
{
38+
// 走 IExporter.Export 而不是裸调 EPPlus.AddPicture,
39+
// 验证 IE 自身导出路径能完整写出所有 drawings.
40+
// 兜底由 SaveAs 提供, 任何路径都不依赖手动 Flush.
41+
var filePath = GetTestFilePath($"{nameof(LargeExport_AllImagesPersisted)}.xlsx");
42+
DeleteFile(filePath);
43+
44+
const int rowCount = 50;
45+
var exporter = new ExcelExporter();
46+
var data = BuildOnePicRowDto(rowCount, GetSamplePngPath());
47+
48+
var result = await exporter.Export(filePath, data);
49+
result.ShouldNotBeNull();
50+
File.Exists(filePath).ShouldBeTrue();
51+
52+
// EPPlus 在这里只用于读回断言, 验证 IE 写入的 drawings 完整
53+
using (var pck = new ExcelPackage(new FileInfo(filePath)))
54+
{
55+
var sheet = pck.Workbook.Worksheets.First();
56+
sheet.Drawings.Count.ShouldBe(rowCount);
57+
_output.WriteLine($"IE Export {rowCount} 行后, sheet.Drawings.Count = {sheet.Drawings.Count}");
58+
}
59+
}
60+
61+
[Fact(DisplayName = "DrawingsFlush_ReloadAfterExport_AllImagesRestored")]
62+
public async Task ReloadAfterExport_AllImagesRestored()
63+
{
64+
// 验证 IE 导出含图片的 xlsx 后, 关闭再重新打开:
65+
// - drawings 数量不丢
66+
// - 每个 drawing 的 From 坐标 (column index) 持久化正确
67+
var filePath = GetTestFilePath($"{nameof(ReloadAfterExport_AllImagesRestored)}.xlsx");
68+
DeleteFile(filePath);
69+
70+
const int rowCount = 20;
71+
var exporter = new ExcelExporter();
72+
var data = BuildOnePicRowDto(rowCount, GetSamplePngPath());
73+
74+
await exporter.Export(filePath, data);
75+
76+
using (var pck = new ExcelPackage(new FileInfo(filePath)))
77+
{
78+
var sheet = pck.Workbook.Worksheets.First();
79+
sheet.Drawings.Count.ShouldBe(rowCount);
80+
81+
// 走 ExportHelper.AddPictures 路径时, picture 默认 EditAs.OneCell,
82+
// From.Column 应当稳定 (本仓库使用 column index = Img 列位置).
83+
foreach (ExcelPicture pic in sheet.Drawings)
84+
{
85+
pic.ShouldNotBeNull();
86+
pic.From.Column.ShouldBeGreaterThanOrEqualTo(0);
87+
}
88+
_output.WriteLine($"Reload 后 {sheet.Drawings.Count} 个 drawing 全部还原");
89+
}
90+
}
91+
92+
[Fact(DisplayName = "DrawingsFlush_LargeExportViaBytes_AllImagesPersisted")]
93+
public async Task LargeExportViaBytes_AllImagesPersisted()
94+
{
95+
// 验证 IE 的 ExportAsByteArray 路径同样完整写出所有 drawings.
96+
// 这条路径不写盘, 直接拿 byte[], 用 MemoryStream 喂给 EPPlus 校验.
97+
const int rowCount = 50;
98+
var exporter = new ExcelExporter();
99+
var data = BuildOnePicRowDto(rowCount, GetSamplePngPath());
100+
101+
var bytes = await exporter.ExportAsByteArray(data);
102+
bytes.ShouldNotBeNull();
103+
bytes.Length.ShouldBeGreaterThan(0);
104+
105+
using (var ms = new MemoryStream(bytes))
106+
using (var pck = new ExcelPackage(ms))
107+
{
108+
var sheet = pck.Workbook.Worksheets.First();
109+
sheet.Drawings.Count.ShouldBe(rowCount);
110+
_output.WriteLine($"IE ExportAsByteArray {rowCount} 行后, sheet.Drawings.Count = {sheet.Drawings.Count}");
111+
}
112+
}
113+
114+
/// <summary>
115+
/// 构造 N 行 DTO, 每行一个 [ExportImageField] 字段 (string) 指向同一张本地 png.
116+
/// DTO 本地化避免与既有 ExportTestDataWithPricture (注意拼写) 冲突.
117+
/// 暴露 internal 以供同程序集内的 byte-consistency 测试复用.
118+
/// </summary>
119+
internal static List<OnePicRowDto> BuildOnePicRowDto(int rowCount, string imagePath)
120+
{
121+
var list = new List<OnePicRowDto>(rowCount);
122+
for (int i = 0; i < rowCount; i++)
123+
{
124+
list.Add(new OnePicRowDto { Img = imagePath });
125+
}
126+
return list;
127+
}
128+
129+
/// <summary>
130+
/// 单图片列 DTO, 用于走 IE 导出器而非裸 EPPlus 的回归测试.
131+
/// </summary>
132+
[ExcelExporter(Name = "图片导出", ExcelOutputType = ExcelOutputTypes.None)]
133+
public class OnePicRowDto
134+
{
135+
[ExportImageField(Width = 50, Height = 15)]
136+
[ExporterHeader(DisplayName = "图")]
137+
public string Img { get; set; }
138+
}
139+
}
140+
141+
/// <summary>
142+
/// 验证 SHA1 改动后图片字节内容完全一致:
143+
/// 1 张图 vs 100 张同图的 xlsx (两个文件都走 IExporter.Export 生成),
144+
/// 提取 /xl/media/image1.* 的 zip-level CRC32, 应完全一致.
145+
/// </summary>
146+
public class ImageByteConsistency_Tests : TestBase
147+
{
148+
private readonly ITestOutputHelper _output;
149+
150+
public ImageByteConsistency_Tests(ITestOutputHelper output)
151+
{
152+
_output = output;
153+
}
154+
155+
private static string GetSamplePngPath() =>
156+
Path.Combine("TestFiles", "ExporterTest.png");
157+
158+
[Fact(DisplayName = "ImageByteConsistency_SingleVsRepeatedDrawings_ImageBytesIdentical")]
159+
public async Task SingleVsRepeatedDrawings_ImageBytesIdentical()
160+
{
161+
// baseline: 1 行 1 列 = 1 张图
162+
// repeat: 100 行 1 列 = 100 张同图 (走 IE 同图去重路径)
163+
// 两个 xlsx 都走 IExporter.Export 生成 (与生产路径一致),
164+
// 用 ZipArchive 提取 /xl/media/image1.*, 对比 zip-level CRC32,
165+
// 应完全一致 → 确认 SHA1 改动未改变图片字节内容.
166+
var singlePath = GetTestFilePath($"{nameof(SingleVsRepeatedDrawings_ImageBytesIdentical)}_single.xlsx");
167+
var repeatPath = GetTestFilePath($"{nameof(SingleVsRepeatedDrawings_ImageBytesIdentical)}_repeat.xlsx");
168+
DeleteFile(singlePath);
169+
DeleteFile(repeatPath);
170+
171+
var exporter = new ExcelExporter();
172+
173+
var singleData = DrawingsFlush_Tests.BuildOnePicRowDto(1, GetSamplePngPath());
174+
await exporter.Export(singlePath, singleData);
175+
176+
var repeatData = DrawingsFlush_Tests.BuildOnePicRowDto(100, GetSamplePngPath());
177+
await exporter.Export(repeatPath, repeatData);
178+
179+
var singleCrc = ExtractFirstImageCrc32(singlePath);
180+
var repeatCrc = ExtractFirstImageCrc32(repeatPath);
181+
182+
singleCrc.ShouldBe(repeatCrc);
183+
_output.WriteLine($"single (1 行) image1 CRC32 = 0x{singleCrc:X8}, repeat (100 行) image1 CRC32 = 0x{repeatCrc:X8}");
184+
}
185+
186+
/// <summary>
187+
/// 从 xlsx zip 中提取第一个 /xl/media/image* 条目的 CRC32 (zip-level).
188+
/// </summary>
189+
private static uint ExtractFirstImageCrc32(string xlsxPath)
190+
{
191+
using var archive = ZipFile.OpenRead(xlsxPath);
192+
var imageEntry = archive.Entries
193+
.Where(e => e.FullName.StartsWith("xl/media/", StringComparison.OrdinalIgnoreCase))
194+
.OrderBy(e => e.FullName, StringComparer.Ordinal)
195+
.First();
196+
return imageEntry.Crc32;
197+
}
198+
}
199+
}

0 commit comments

Comments
 (0)