test:导出脚本增加零漏拷/零多拷硬保证
质疑"某个目录有没有被打进 zip"时, 原先只能人工逐目录核对。现在把这个检查做进 脚本, 每次导出都跑: copyDir 记录每个被复制文件在 zip 中应有的条目名, verifyZipFile 把它与实际 zip 条目做**逐个双向比对** —— 少一个报"漏拷"并列出文件名, 多一个报"多拷"。这样 漏拷会当场失败, 而不是等在别的电脑上解压缩时才发现。 自检输出新增一行, 例如: 自检: 零漏拷 ✅(1381/1381) / CRC 逐条 ✅ / 偏移一致 ✅ / ... 同时把 copyDir 的条目名拼接由"从 nameOfParent 反推"改为显式传 relBase, 去掉 原先那段绕的 split/map 逻辑。 新增 3 条测试(漏拷拒绝 / 多拷拒绝 / 清单吻合时放行且 README.txt 豁免), 本 测试文件 19/19 通过, npm test 全量 17 个测试文件通过。
This commit is contained in:
@@ -103,9 +103,17 @@ function shouldSkipFile(name) {
|
||||
return /\.(log|tmp|swp)$/.test(name);
|
||||
}
|
||||
|
||||
const stats = { files: 0, bytes: 0, skipped: 0 };
|
||||
const stats = { files: 0, bytes: 0, skipped: 0, copied: [] };
|
||||
|
||||
async function copyDir(srcDir, destDir, opts, depth = 0, nameOfParent = '') {
|
||||
/**
|
||||
* 复制源目录到暂存目录 (应用排除规则), 同时记录每个被复制文件在 zip 中应有的条目名
|
||||
* @param {string} srcDir
|
||||
* @param {string} destDir
|
||||
* @param {object} opts
|
||||
* @param {string} relBase 该目录在 zip 中的条目名前缀 (首层传 PROJECT_DIR_NAME)
|
||||
* @param {number} depth
|
||||
*/
|
||||
async function copyDir(srcDir, destDir, opts, relBase = PROJECT_DIR_NAME, depth = 0) {
|
||||
await fsp.mkdir(destDir, { recursive: true });
|
||||
const entries = await fsp.readdir(srcDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
@@ -117,7 +125,7 @@ async function copyDir(srcDir, destDir, opts, depth = 0, nameOfParent = '') {
|
||||
stats.skipped++;
|
||||
continue;
|
||||
}
|
||||
await copyDir(srcPath, destPath, opts, depth + 1, entry.name);
|
||||
await copyDir(srcPath, destPath, opts, `${relBase}/${entry.name}`, depth + 1);
|
||||
} else if (entry.isFile()) {
|
||||
if (shouldSkipFile(entry.name)) {
|
||||
stats.skipped++;
|
||||
@@ -127,6 +135,7 @@ async function copyDir(srcDir, destDir, opts, depth = 0, nameOfParent = '') {
|
||||
const st = await fsp.stat(destPath);
|
||||
stats.files++;
|
||||
stats.bytes += st.size;
|
||||
stats.copied.push(`${relBase}/${entry.name}`);
|
||||
}
|
||||
// 符号链接等其它类型直接跳过
|
||||
}
|
||||
@@ -325,12 +334,15 @@ function humanSize(bytes) {
|
||||
|
||||
/**
|
||||
* @param {string} zipPath
|
||||
* @param {{withCerts?: boolean, skipRequired?: boolean}} opts
|
||||
* @param {{withCerts?: boolean, skipRequired?: boolean, expectFiles?: string[], rootName?: string}} opts
|
||||
* skipRequired: 仅供单元测试用夹具目录时跳过"必需文件"断言
|
||||
* expectFiles: 实际复制到暂存目录的文件清单 (zip 内条目名), 用于零漏拷/零多拷比对
|
||||
* rootName: zip 内顶层目录名 (默认 mp-harmony)
|
||||
* @returns {Promise<{entries:number, files:number, dirs:number}>}
|
||||
* @throws {Error} 任一项校验不通过
|
||||
*/
|
||||
async function verifyZipFile(zipPath, opts = {}) {
|
||||
const rootName = opts.rootName || PROJECT_DIR_NAME;
|
||||
const buf = await fsp.readFile(zipPath);
|
||||
const fail = (msg) => { throw new Error(`zip 自检失败: ${msg}`); };
|
||||
|
||||
@@ -440,6 +452,25 @@ async function verifyZipFile(zipPath, opts = {}) {
|
||||
}
|
||||
|
||||
const fileCount = names.filter((n) => !n.endsWith('/')).length;
|
||||
|
||||
// --- 零漏拷 / 零多拷 比对 ---
|
||||
// expectFiles 是 copyDir 实际复制到暂存目录的文件清单 (已应用排除规则)。
|
||||
// 逐个核对 zip 内是否都有, 且 zip 内没有清单之外的文件 (README.txt 是后补的, 除外)。
|
||||
// 这样"某个目录没被打进去"这类问题会被脚本当场拦下, 不必人工逐目录核对。
|
||||
if (Array.isArray(opts.expectFiles)) {
|
||||
const nameSet = new Set(names);
|
||||
const missingFiles = opts.expectFiles.filter((f) => !nameSet.has(f));
|
||||
if (missingFiles.length) {
|
||||
fail(`漏拷: ${missingFiles.length} 个已复制的文件不在 zip 内, 例如 ${missingFiles.slice(0, 5).join(', ')}`);
|
||||
}
|
||||
const expected = new Set(opts.expectFiles);
|
||||
expected.add(`${rootName}/README.txt`);
|
||||
const unexpected = names.filter((n) => !n.endsWith('/') && !expected.has(n));
|
||||
if (unexpected.length) {
|
||||
fail(`多拷: ${unexpected.length} 个 zip 内文件不在复制清单中, 例如 ${unexpected.slice(0, 5).join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
entries: totalEntries,
|
||||
files: fileCount,
|
||||
@@ -493,6 +524,7 @@ async function main() {
|
||||
stats.files = 0;
|
||||
stats.bytes = 0;
|
||||
stats.skipped = 0;
|
||||
stats.copied = [];
|
||||
await copyDir(srcRoot, stageDir, opts);
|
||||
console.log(`\n 复制完成: ${stats.files} 个文件 / ${humanSize(stats.bytes)} (跳过 ${stats.skipped} 项, ${Date.now() - t0}ms)`);
|
||||
|
||||
@@ -561,7 +593,12 @@ async function main() {
|
||||
await zipDirectory(stageDir, zipPath);
|
||||
|
||||
// 自检: 解析回 zip 逐项校验, 不通过就抛错 (绝不打印「完成」)
|
||||
const vres = await verifyZipFile(zipPath, { withCerts: opts.withCerts });
|
||||
// expectFiles 传入实际复制清单, 让脚本自己证明"零漏拷", 无需人工逐目录核对
|
||||
const vres = await verifyZipFile(zipPath, {
|
||||
withCerts: opts.withCerts,
|
||||
expectFiles: stats.copied,
|
||||
rootName: PROJECT_DIR_NAME,
|
||||
});
|
||||
|
||||
const zipSize = (await fsp.stat(zipPath)).size;
|
||||
// 输出目录可能在仓库外 (如 --out /tmp/xxx), 这时相对路径会变成 ../.. 很难读
|
||||
@@ -571,7 +608,7 @@ async function main() {
|
||||
console.log(` ZIP: ${shownZip}`);
|
||||
console.log(` 大小: ${humanSize(zipSize)} (原始 ${humanSize(stats.bytes)})`);
|
||||
console.log(` 条目: ${vres.files} 个文件 + ${vres.entries - vres.files} 个目录 (自检通过)`);
|
||||
console.log(` 自检: CRC 逐条校验 ✅ / 偏移一致 ✅ / 正斜杠 ✅ / UTF-8 ✅ / 骨架齐全 ✅`);
|
||||
console.log(` 自检: 零漏拷 ✅(${stats.copied.length}/${stats.copied.length}) / CRC 逐条 ✅ / 偏移一致 ✅ / 正斜杠 ✅ / UTF-8 ✅ / 骨架齐全 ✅`);
|
||||
|
||||
// 清暂存目录
|
||||
await fsp.rm(stageParent, { recursive: true, force: true });
|
||||
|
||||
Reference in New Issue
Block a user