feat: 解码 worker 化、Playwright E2E 测试体系与文件夹对比(0.6.0)
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
||||
import { mkdtemp, mkdir, writeFile, rm, symlink } from 'fs/promises'
|
||||
import { tmpdir } from 'os'
|
||||
import { join } from 'path'
|
||||
import { scanFolders, type FolderEntry } from './folderScan'
|
||||
|
||||
/** 以临时目录为根构造两个待对比文件夹 */
|
||||
let leftDir: string
|
||||
let rightDir: string
|
||||
|
||||
beforeAll(async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'difflens-scan-'))
|
||||
leftDir = join(root, 'left')
|
||||
rightDir = join(root, 'right')
|
||||
await mkdir(leftDir, { recursive: true })
|
||||
await mkdir(rightDir, { recursive: true })
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await rm(join(leftDir, '..'), { recursive: true, force: true })
|
||||
})
|
||||
|
||||
async function put(dir: 'left' | 'right', rel: string, content: string | Buffer): Promise<void> {
|
||||
const path = join(dir === 'left' ? leftDir : rightDir, rel)
|
||||
const idx = rel.lastIndexOf('/')
|
||||
if (idx >= 0) await mkdir(join(path, '..'), { recursive: true })
|
||||
await writeFile(path, content)
|
||||
}
|
||||
|
||||
const byRel = (entries: FolderEntry[], rel: string): FolderEntry | undefined =>
|
||||
entries.find((e) => e.rel === rel)
|
||||
|
||||
describe('scanFolders - 状态判定', () => {
|
||||
it('两侧内容一致判为 same,内容不同(同大小)判为 different,大小不同判为 different', async () => {
|
||||
await put('left', 'same.txt', 'hello')
|
||||
await put('right', 'same.txt', 'hello')
|
||||
await put('left', 'content-diff.txt', 'abc')
|
||||
await put('right', 'content-diff.txt', 'abd')
|
||||
await put('left', 'size-diff.txt', 'short')
|
||||
await put('right', 'size-diff.txt', 'much longer text')
|
||||
|
||||
const r = await scanFolders(leftDir, rightDir)
|
||||
expect(r.truncated).toBe(false)
|
||||
expect(byRel(r.entries, 'same.txt')).toMatchObject({ status: 'same', leftSize: 5, rightSize: 5 })
|
||||
expect(byRel(r.entries, 'content-diff.txt')?.status).toBe('different')
|
||||
expect(byRel(r.entries, 'size-diff.txt')).toMatchObject({ status: 'different' })
|
||||
})
|
||||
|
||||
it('单侧缺失判为 only,大小字段另一侧为 null', async () => {
|
||||
await put('left', 'only-left.txt', 'L')
|
||||
await put('right', 'only-right.txt', 'R')
|
||||
const r = await scanFolders(leftDir, rightDir)
|
||||
expect(byRel(r.entries, 'only-left.txt')).toMatchObject({
|
||||
status: 'left-only',
|
||||
leftSize: 1,
|
||||
rightSize: null
|
||||
})
|
||||
expect(byRel(r.entries, 'only-right.txt')).toMatchObject({
|
||||
status: 'right-only',
|
||||
leftSize: null,
|
||||
rightSize: 1
|
||||
})
|
||||
})
|
||||
|
||||
it('递归子目录条目按相对路径对齐(统一 / 分隔)', async () => {
|
||||
await put('left', 'src/core/a.ts', 'export const a = 1\n')
|
||||
await put('right', 'src/core/a.ts', 'export const a = 2\n')
|
||||
await put('right', 'src/util/b.ts', 'export const b = 1\n')
|
||||
const r = await scanFolders(leftDir, rightDir)
|
||||
expect(byRel(r.entries, 'src/core/a.ts')?.status).toBe('different')
|
||||
expect(byRel(r.entries, 'src/util/b.ts')?.status).toBe('right-only')
|
||||
})
|
||||
|
||||
it('二进制内容同样按字节判定(含 NUL 字节文件)', async () => {
|
||||
await put('left', 'bin.dat', Buffer.from([0x00, 0x01, 0x02, 0x03]))
|
||||
await put('right', 'bin.dat', Buffer.from([0x00, 0x01, 0x02, 0x04]))
|
||||
const r = await scanFolders(leftDir, rightDir)
|
||||
expect(byRel(r.entries, 'bin.dat')?.status).toBe('different')
|
||||
})
|
||||
|
||||
it('symlink 被跳过(防环且不参与判定)', async () => {
|
||||
// 建一个指向已存在子目录的 symlink,不应出现在结果中也不应导致递归
|
||||
await symlink(join(leftDir, 'src'), join(leftDir, 'src-link'), 'dir').catch(() => {})
|
||||
const r = await scanFolders(leftDir, rightDir)
|
||||
expect(r.entries.some((e) => e.rel.startsWith('src-link/'))).toBe(false)
|
||||
})
|
||||
|
||||
it('条目按相对路径字典序排序', async () => {
|
||||
const r = await scanFolders(leftDir, rightDir)
|
||||
const rels = r.entries.map((e) => e.rel)
|
||||
expect([...rels].sort()).toEqual(rels)
|
||||
})
|
||||
|
||||
it('total 为两侧枚举文件数的较大值', async () => {
|
||||
const r = await scanFolders(leftDir, rightDir)
|
||||
const leftCount = r.entries.filter((e) => e.leftSize !== null).length
|
||||
const rightCount = r.entries.filter((e) => e.rightSize !== null).length
|
||||
expect(r.total).toBeGreaterThanOrEqual(Math.max(leftCount, rightCount))
|
||||
})
|
||||
})
|
||||
|
||||
describe('scanFolders - 边界与选项', () => {
|
||||
it('两个空目录返回空结果', async () => {
|
||||
const emptyL = await mkdtemp(join(tmpdir(), 'difflens-empty-l-'))
|
||||
const emptyR = await mkdtemp(join(tmpdir(), 'difflens-empty-r-'))
|
||||
try {
|
||||
const r = await scanFolders(emptyL, emptyR)
|
||||
expect(r.entries).toEqual([])
|
||||
expect(r.total).toBe(0)
|
||||
expect(r.truncated).toBe(false)
|
||||
} finally {
|
||||
await rm(emptyL, { recursive: true, force: true })
|
||||
await rm(emptyR, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('目录不存在时抛出异常(由 IPC 层转为错误结果)', async () => {
|
||||
await expect(scanFolders(join(leftDir, 'not-exist'), rightDir)).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('maxFiles 截断:条目不完整且 truncated 为 true', async () => {
|
||||
const l = await mkdtemp(join(tmpdir(), 'difflens-trunc-l-'))
|
||||
const r = await mkdtemp(join(tmpdir(), 'difflens-trunc-r-'))
|
||||
try {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await writeFile(join(l, `f${i}.txt`), String(i))
|
||||
await writeFile(join(r, `f${i}.txt`), String(i))
|
||||
}
|
||||
const res = await scanFolders(l, r, { maxFiles: 2 })
|
||||
expect(res.truncated).toBe(true)
|
||||
expect(res.total).toBe(5)
|
||||
expect(res.entries.length).toBeLessThanOrEqual(2)
|
||||
} finally {
|
||||
await rm(l, { recursive: true, force: true })
|
||||
await rm(r, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('超过 maxContentBytes 的同大小文件:头部采样一致判 same 且带 approximate 标注', async () => {
|
||||
const l = await mkdtemp(join(tmpdir(), 'difflens-big-l-'))
|
||||
const r = await mkdtemp(join(tmpdir(), 'difflens-big-r-'))
|
||||
try {
|
||||
// 100KB 文件、maxContentBytes 压到 1KB:走采样路径;头部 8KB 一致 → same + approximate
|
||||
const head = Buffer.alloc(8 * 1024, 0x41)
|
||||
const tailL = Buffer.alloc(100 * 1024 - head.length, 0x01)
|
||||
const tailR = Buffer.alloc(100 * 1024 - head.length, 0x02)
|
||||
await writeFile(join(l, 'big.bin'), Buffer.concat([head, tailL]))
|
||||
await writeFile(join(r, 'big.bin'), Buffer.concat([head, tailR]))
|
||||
const res = await scanFolders(l, r, { maxContentBytes: 1024 })
|
||||
expect(byRel(res.entries, 'big.bin')).toMatchObject({ status: 'same', approximate: true })
|
||||
} finally {
|
||||
await rm(l, { recursive: true, force: true })
|
||||
await rm(r, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('采样头部不同直接判 different', async () => {
|
||||
const l = await mkdtemp(join(tmpdir(), 'difflens-head-l-'))
|
||||
const r = await mkdtemp(join(tmpdir(), 'difflens-head-r-'))
|
||||
try {
|
||||
await writeFile(join(l, 'big.bin'), Buffer.alloc(100 * 1024, 0x01))
|
||||
await writeFile(join(r, 'big.bin'), Buffer.alloc(100 * 1024, 0x02))
|
||||
const res = await scanFolders(l, r, { maxContentBytes: 1024 })
|
||||
expect(byRel(res.entries, 'big.bin')?.status).toBe('different')
|
||||
} finally {
|
||||
await rm(l, { recursive: true, force: true })
|
||||
await rm(r, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user