convertor/tests/e2e/format-matrix.e2e.test.ts
Your Name a2512bf29b fix: 修復 lint 錯誤和代碼格式化
- 移除未使用的導入 (readdirSync, basename, readFileSync, execSync)
- 將不需要重新賦值的 let 改為 const
- 修復 switch case 區塊中的詞法聲明問題
- 移除未使用的 SAMPLING_RATE 和 sampled 變量
- 移除未使用的 _timeout 參數
- 運行 Prettier 格式化所有文件
2026-01-23 12:27:22 +08:00

739 lines
22 KiB
TypeScript
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.

/**
* 格式轉換矩陣測試
*
* 測試目標:驗證 1000+ 種格式轉換組合
*
* 這個測試會:
* 1. 自動發現所有轉換器支援的格式
* 2. 生成所有可能的轉換組合
* 3. 執行抽樣測試(避免耗時過長)
* 4. 生成詳細的測試報告
*
* 執行方式:
* bun test tests/e2e/format-matrix.e2e.test.ts
*/
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { spawnSync } from "node:child_process";
// =============================================================================
// 配置
// =============================================================================
const E2E_OUTPUT_DIR = "tests/e2e/output/format-matrix";
const TIMEOUT = 60_000; // 60 秒超時
// 每個轉換器最大測試數
const MAX_TESTS_PER_CONVERTER = 20;
// =============================================================================
// 類型定義
// =============================================================================
interface ConverterInfo {
name: string;
available: boolean;
from: string[];
to: string[];
combinations: [string, string][];
}
interface TestResult {
converter: string;
from: string;
to: string;
success: boolean;
duration: number;
error?: string;
}
interface MatrixReport {
generatedAt: string;
converters: ConverterInfo[];
totalCombinations: number;
testedCombinations: number;
passedTests: number;
failedTests: number;
skippedTests: number;
results: TestResult[];
}
// =============================================================================
// 工具函數
// =============================================================================
function ensureDir(dir: string): void {
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
}
function isToolAvailable(command: string, args: string[] = ["--version"]): boolean {
try {
const result = spawnSync(command, args, {
timeout: 5000,
encoding: "utf-8",
stdio: ["pipe", "pipe", "pipe"],
});
return result.status === 0;
} catch {
return false;
}
}
/**
* 格式別名映射 - 將檔案副檔名轉換為 Pandoc 認識的格式名稱
*/
function normalizeFormatForPandoc(format: string): string {
const aliases: Record<string, string> = {
md: "markdown",
txt: "plain",
tex: "latex",
};
return aliases[format.toLowerCase()] || format.toLowerCase();
}
/**
* 判斷格式是否需要特殊處理(如二進制格式需要真實檔案)
*/
function isComplexFormat(format: string): boolean {
const complexFormats = ["epub", "docx", "odt", "pdf", "pptx", "xlsx"];
return complexFormats.includes(format.toLowerCase());
}
// 根據格式生成測試內容
function createTestContent(format: string): Buffer | string {
switch (format.toLowerCase()) {
// 圖像格式
case "svg":
return `<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
<circle cx="50" cy="50" r="40" fill="blue"/>
</svg>`;
case "png":
return Buffer.from([
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44,
0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00, 0x00, 0x90,
0x77, 0x53, 0xde, 0x00, 0x00, 0x00, 0x0c, 0x49, 0x44, 0x41, 0x54, 0x08, 0xd7, 0x63, 0xf8,
0xcf, 0xc0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x01, 0x00, 0x05, 0xfe, 0xd4, 0xef, 0x00, 0x00,
0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
]);
case "bmp": {
const bmp = Buffer.alloc(70);
bmp.write("BM", 0);
bmp.writeUInt32LE(70, 2);
bmp.writeUInt32LE(54, 10);
bmp.writeUInt32LE(40, 14);
bmp.writeInt32LE(1, 18);
bmp.writeInt32LE(1, 22);
bmp.writeUInt16LE(1, 26);
bmp.writeUInt16LE(24, 28);
bmp[54] = 0xff;
bmp[55] = 0x00;
bmp[56] = 0x00;
return bmp;
}
// 文檔格式
case "md":
case "markdown":
return `# 測試文檔
這是測試內容。
## 列表
- 項目 1
- 項目 2
`;
case "html":
case "htm":
return `<!DOCTYPE html>
<html><head><title>Test</title></head>
<body><h1>測試</h1><p>內容</p></body>
</html>`;
case "txt":
case "text":
return "測試文字內容\nTest content\n";
case "rst":
return `測試標題
========
這是 reStructuredText 測試。
`;
case "latex":
case "tex":
return `\\documentclass{article}
\\begin{document}
\\section{測試}
這是 LaTeX 測試。
\\end{document}`;
// 資料格式
case "json":
return JSON.stringify({ name: "test", value: 42 }, null, 2);
case "yaml":
case "yml":
return "name: test\nvalue: 42\n";
case "toml":
return 'name = "test"\nvalue = 42\n';
case "xml":
return '<?xml version="1.0"?>\n<root><name>test</name></root>';
case "csv":
return "name,value\ntest,42\n";
case "tsv":
return "name\tvalue\ntest\t42\n";
default:
return `Test content for ${format}`;
}
}
// =============================================================================
// 轉換器配置
// =============================================================================
interface ConverterConfig {
name: string;
command: string;
commandArgs?: string[];
from: string[];
to: string[];
needsXvfb?: boolean;
}
const CONVERTERS: ConverterConfig[] = [
{
name: "inkscape",
command: "inkscape",
from: ["svg", "svgz"],
to: ["png", "pdf", "eps", "ps", "emf", "wmf"],
needsXvfb: true,
},
{
name: "imagemagick",
command: "magick",
from: ["png", "jpg", "jpeg", "gif", "bmp", "tiff", "tif", "webp", "ico", "ppm", "pgm", "pbm"],
to: [
"png",
"jpg",
"jpeg",
"gif",
"bmp",
"tiff",
"tif",
"webp",
"ico",
"ppm",
"pgm",
"pbm",
"pdf",
],
},
{
name: "graphicsmagick",
command: "gm",
commandArgs: ["version"],
from: ["png", "jpg", "jpeg", "gif", "bmp", "tiff"],
to: ["png", "jpg", "jpeg", "gif", "bmp", "tiff", "pdf"],
},
{
name: "pandoc",
command: "pandoc",
from: ["markdown", "html", "rst", "latex"],
to: ["html", "docx", "odt", "rst", "latex", "mediawiki", "asciidoc", "org"],
},
{
name: "dasel",
command: "dasel",
from: ["json", "yaml", "yml", "toml", "xml", "csv"],
to: ["json", "yaml", "yml", "toml", "xml", "csv"],
},
{
name: "potrace",
command: "potrace",
commandArgs: ["-v"],
from: ["bmp", "pbm", "pgm", "ppm", "pnm"],
to: ["svg", "eps", "ps", "pdf", "dxf"],
},
{
name: "vtracer",
command: "vtracer",
from: ["png", "jpg", "jpeg", "bmp"],
to: ["svg"],
},
{
name: "resvg",
command: "resvg",
from: ["svg", "svgz"],
to: ["png", "pdf"],
},
{
name: "vips",
command: "vips",
from: ["png", "jpg", "jpeg", "tiff", "webp", "heif", "avif"],
to: ["png", "jpg", "jpeg", "tiff", "webp", "heif", "avif"],
},
{
name: "libreoffice",
command: "soffice",
from: ["doc", "docx", "xls", "xlsx", "ppt", "pptx", "odt", "ods", "odp", "rtf", "html"],
to: ["pdf", "docx", "xlsx", "pptx", "odt", "ods", "odp", "html", "txt"],
},
{
name: "calibre",
command: "ebook-convert",
from: ["epub", "mobi", "azw", "azw3", "pdf", "html", "txt", "rtf", "docx"],
to: ["epub", "mobi", "azw3", "pdf", "html", "txt", "docx"],
needsXvfb: true,
},
{
name: "ffmpeg",
command: "ffmpeg",
commandArgs: ["-version"],
from: ["mp3", "wav", "ogg", "flac", "aac", "m4a", "mp4", "mkv", "avi", "mov", "webm"],
to: ["mp3", "wav", "ogg", "flac", "aac", "m4a", "mp4", "mkv", "avi", "mov", "webm", "gif"],
},
{
name: "libheif",
command: "heif-convert",
from: ["heic", "heif", "avif"],
to: ["jpg", "jpeg", "png"],
},
{
name: "libjxl",
command: "cjxl",
from: ["png", "jpg", "jpeg", "gif", "apng"],
to: ["jxl"],
},
];
// =============================================================================
// 主測試
// =============================================================================
const report: MatrixReport = {
generatedAt: new Date().toISOString(),
converters: [],
totalCombinations: 0,
testedCombinations: 0,
passedTests: 0,
failedTests: 0,
skippedTests: 0,
results: [],
};
beforeAll(() => {
ensureDir(E2E_OUTPUT_DIR);
console.log("\n🔍 發現轉換器... Discovering converters...\n");
});
afterAll(() => {
// 生成報告
const reportPath = join(E2E_OUTPUT_DIR, "matrix-report.json");
writeFileSync(reportPath, JSON.stringify(report, null, 2));
const summaryPath = join(E2E_OUTPUT_DIR, "matrix-summary.md");
const summary = generateMarkdownSummary(report);
writeFileSync(summaryPath, summary);
console.log("\n" + "=".repeat(70));
console.log("📊 格式轉換矩陣測試報告 Format Matrix Test Report");
console.log("=".repeat(70));
console.log(`轉換器數量 Converters: ${report.converters.length}`);
console.log(`總組合數 Total Combinations: ${report.totalCombinations}`);
console.log(`測試數 Tested: ${report.testedCombinations}`);
console.log(`通過 Passed: ${report.passedTests}`);
console.log(`失敗 Failed: ${report.failedTests}`);
console.log(`跳過 Skipped: ${report.skippedTests}`);
console.log(
`成功率 Success Rate: ${((report.passedTests / (report.testedCombinations || 1)) * 100).toFixed(1)}%`,
);
console.log("=".repeat(70));
console.log(`📁 報告路徑: ${reportPath}`);
console.log(`📄 摘要路徑: ${summaryPath}`);
});
function generateMarkdownSummary(report: MatrixReport): string {
let md = `# 格式轉換矩陣測試報告\n\n`;
md += `生成時間: ${report.generatedAt}\n\n`;
md += `## 摘要\n\n`;
md += `| 指標 | 值 |\n`;
md += `|------|----|\n`;
md += `| 轉換器數量 | ${report.converters.length} |\n`;
md += `| 總組合數 | ${report.totalCombinations} |\n`;
md += `| 測試數 | ${report.testedCombinations} |\n`;
md += `| 通過 | ${report.passedTests} |\n`;
md += `| 失敗 | ${report.failedTests} |\n`;
md += `| 跳過 | ${report.skippedTests} |\n`;
md += `| 成功率 | ${((report.passedTests / (report.testedCombinations || 1)) * 100).toFixed(1)}% |\n`;
md += `\n## 轉換器詳情\n\n`;
for (const converter of report.converters) {
md += `### ${converter.name}\n\n`;
md += `- 狀態: ${converter.available ? "✅ 可用" : "❌ 不可用"}\n`;
md += `- 輸入格式 (${converter.from.length}): ${converter.from.join(", ")}\n`;
md += `- 輸出格式 (${converter.to.length}): ${converter.to.join(", ")}\n`;
md += `- 組合數: ${converter.combinations.length}\n\n`;
}
md += `## 失敗的測試\n\n`;
const failedResults = report.results.filter((r) => !r.success);
if (failedResults.length === 0) {
md += `無失敗的測試 ✅\n`;
} else {
md += `| 轉換器 | 來源 | 目標 | 錯誤 |\n`;
md += `|--------|------|------|------|\n`;
for (const result of failedResults) {
md += `| ${result.converter} | ${result.from} | ${result.to} | ${result.error || "Unknown"} |\n`;
}
}
return md;
}
// =============================================================================
// 測試套件
// =============================================================================
describe("📊 格式轉換矩陣 Format Conversion Matrix", () => {
describe("發現可用轉換器 Discover Available Converters", () => {
test("檢測所有轉換器", () => {
for (const config of CONVERTERS) {
const available = isToolAvailable(config.command, config.commandArgs);
const combinations: [string, string][] = [];
// 生成所有組合
for (const from of config.from) {
for (const to of config.to) {
if (from !== to) {
combinations.push([from, to]);
}
}
}
const info: ConverterInfo = {
name: config.name,
available,
from: config.from,
to: config.to,
combinations,
};
report.converters.push(info);
report.totalCombinations += combinations.length;
const icon = available ? "✅" : "❌";
console.log(
` ${icon} ${config.name}: ${combinations.length} 組合 (${available ? "可用" : "不可用"})`,
);
}
console.log(`\n 📈 總組合數: ${report.totalCombinations}`);
expect(report.converters.length).toBeGreaterThan(0);
});
});
describe("圖像格式轉換 Image Format Conversions", () => {
const imageConverters = [
"inkscape",
"imagemagick",
"graphicsmagick",
"potrace",
"vtracer",
"resvg",
"vips",
];
for (const converterName of imageConverters) {
describe(`${converterName}`, () => {
test(
`測試格式轉換`,
async () => {
const converter = report.converters.find((c) => c.name === converterName);
if (!converter) {
report.skippedTests++;
console.log(`${converterName}: 轉換器未找到`);
return;
}
if (!converter.available) {
report.skippedTests += converter.combinations.length;
console.log(
`${converterName}: 工具不可用,跳過 ${converter.combinations.length} 個測試`,
);
return;
}
// 抽樣測試
const sampled = sampleCombinations(converter.combinations, MAX_TESTS_PER_CONVERTER);
console.log(
` 🧪 ${converterName}: 測試 ${sampled.length}/${converter.combinations.length} 個組合`,
);
for (const [from, to] of sampled) {
report.testedCombinations++;
const startTime = Date.now();
try {
// 建立測試檔案
const inputDir = join(E2E_OUTPUT_DIR, converterName);
ensureDir(inputDir);
const inputPath = join(inputDir, `input.${from}`);
const outputPath = join(inputDir, `output_${from}_to_${to}.${to}`);
const content = createTestContent(from);
if (Buffer.isBuffer(content)) {
writeFileSync(inputPath, content);
} else {
writeFileSync(inputPath, content, "utf-8");
}
// 動態導入並執行轉換
const module = await import(`../../src/converters/${converterName}`);
await module.convert(inputPath, from, to, outputPath);
const duration = Date.now() - startTime;
const success = existsSync(outputPath);
report.results.push({
converter: converterName,
from,
to,
success,
duration,
});
if (success) {
report.passedTests++;
} else {
report.failedTests++;
console.log(`${from}${to}: 輸出檔案不存在`);
}
} catch (error) {
const duration = Date.now() - startTime;
report.failedTests++;
report.results.push({
converter: converterName,
from,
to,
success: false,
duration,
error: String(error),
});
console.log(`${from}${to}: ${error}`);
}
}
},
TIMEOUT * sampled.length,
);
});
}
});
describe("文檔格式轉換 Document Format Conversions", () => {
const docConverters = ["pandoc", "libreoffice", "calibre"];
for (const converterName of docConverters) {
describe(`${converterName}`, () => {
test(
`測試格式轉換`,
async () => {
const converter = report.converters.find((c) => c.name === converterName);
if (!converter) {
report.skippedTests++;
console.log(`${converterName}: 轉換器未找到`);
return;
}
if (!converter.available) {
report.skippedTests += converter.combinations.length;
console.log(
`${converterName}: 工具不可用,跳過 ${converter.combinations.length} 個測試`,
);
return;
}
// 抽樣測試
const sampled = sampleCombinations(converter.combinations, MAX_TESTS_PER_CONVERTER);
console.log(
` 🧪 ${converterName}: 測試 ${sampled.length}/${converter.combinations.length} 個組合`,
);
for (const [from, to] of sampled) {
report.testedCombinations++;
const startTime = Date.now();
try {
const inputDir = join(E2E_OUTPUT_DIR, converterName);
ensureDir(inputDir);
const inputPath = join(inputDir, `input.${from}`);
const outputPath = join(inputDir, `output_${from}_to_${to}.${to}`);
// 跳過複雜的二進制輸入格式(需要真實檔案)
if (isComplexFormat(from)) {
report.skippedTests++;
continue;
}
const content = createTestContent(from);
if (Buffer.isBuffer(content)) {
writeFileSync(inputPath, content);
} else {
writeFileSync(inputPath, content, "utf-8");
}
const module = await import(`../../src/converters/${converterName}`);
// 對於 Pandoc正規化格式名稱
const normalizedFrom =
converterName === "pandoc" ? normalizeFormatForPandoc(from) : from;
const normalizedTo = converterName === "pandoc" ? normalizeFormatForPandoc(to) : to;
await module.convert(inputPath, normalizedFrom, normalizedTo, outputPath);
const duration = Date.now() - startTime;
const success = existsSync(outputPath);
report.results.push({
converter: converterName,
from,
to,
success,
duration,
});
if (success) {
report.passedTests++;
} else {
report.failedTests++;
console.log(`${from}${to}: 輸出檔案不存在`);
}
} catch (error) {
const duration = Date.now() - startTime;
report.failedTests++;
report.results.push({
converter: converterName,
from,
to,
success: false,
duration,
error: String(error),
});
console.log(`${from}${to}: ${error}`);
}
}
},
TIMEOUT * sampled.length,
);
});
}
});
describe("資料格式轉換 Data Format Conversions", () => {
const dataConverters = ["dasel"];
for (const converterName of dataConverters) {
describe(`${converterName}`, () => {
test(
`測試格式轉換`,
async () => {
const converter = report.converters.find((c) => c.name === converterName);
if (!converter) {
report.skippedTests++;
console.log(`${converterName}: 轉換器未找到`);
return;
}
if (!converter.available) {
report.skippedTests += converter.combinations.length;
console.log(
`${converterName}: 工具不可用,跳過 ${converter.combinations.length} 個測試`,
);
return;
}
const sampled = sampleCombinations(converter.combinations, MAX_TESTS_PER_CONVERTER);
console.log(
` 🧪 ${converterName}: 測試 ${sampled.length}/${converter.combinations.length} 個組合`,
);
for (const [from, to] of sampled) {
report.testedCombinations++;
const startTime = Date.now();
try {
const inputDir = join(E2E_OUTPUT_DIR, converterName);
ensureDir(inputDir);
const inputPath = join(inputDir, `input.${from}`);
const outputPath = join(inputDir, `output_${from}_to_${to}.${to}`);
const content = createTestContent(from);
writeFileSync(inputPath, content, "utf-8");
const module = await import(`../../src/converters/${converterName}`);
await module.convert(inputPath, from, to, outputPath);
const duration = Date.now() - startTime;
const success = existsSync(outputPath);
report.results.push({
converter: converterName,
from,
to,
success,
duration,
});
if (success) {
report.passedTests++;
} else {
report.failedTests++;
}
} catch (error) {
const duration = Date.now() - startTime;
report.failedTests++;
report.results.push({
converter: converterName,
from,
to,
success: false,
duration,
error: String(error),
});
}
}
},
TIMEOUT * sampled.length,
);
});
}
});
});
// =============================================================================
// 輔助函數
// =============================================================================
function sampleCombinations(
combinations: [string, string][],
maxCount: number,
): [string, string][] {
if (combinations.length <= maxCount) {
return combinations;
}
// 隨機抽樣
const shuffled = [...combinations].sort(() => Math.random() - 0.5);
return shuffled.slice(0, maxCount);
}