40 lines
1.8 KiB
JavaScript
40 lines
1.8 KiB
JavaScript
import { expect, test, vi } from 'vitest'
|
|
|
|
// rmTree retries ENOTEMPTY from Finder/Spotlight dropping files into a
|
|
// directory mid-removal (#122803). Node's rmSync is the only seam that can
|
|
// reproduce that race deterministically, so replace it and keep the rest of
|
|
// node:fs real.
|
|
const { rmSync } = vi.hoisted(() => ({ rmSync: vi.fn() }))
|
|
|
|
vi.mock('node:fs', async (importOriginal) => {
|
|
const actual = await importOriginal()
|
|
return { ...actual, rmSync }
|
|
})
|
|
|
|
const enotempty = () => { throw Object.assign(new Error('directory not empty'), { code: 'ENOTEMPTY' }) }
|
|
|
|
test('rmTree retries ENOTEMPTY removals until the directory clears', async () => {
|
|
const { rmTree } = await import('../scripts/build/frontend-common.mjs')
|
|
rmSync.mockReset()
|
|
rmSync.mockImplementationOnce(enotempty).mockImplementationOnce(enotempty).mockImplementationOnce(() => {})
|
|
await expect(rmTree('/tmp/product-scratch')).resolves.toBeUndefined()
|
|
expect(rmSync).toHaveBeenCalledTimes(3)
|
|
})
|
|
|
|
test('rmTree stops retrying after a bounded number of attempts', async () => {
|
|
const { rmTree } = await import('../scripts/build/frontend-common.mjs')
|
|
rmSync.mockReset()
|
|
rmSync.mockImplementation(enotempty)
|
|
await expect(rmTree('/tmp/product-scratch')).rejects.toThrow('directory not empty')
|
|
expect(rmSync.mock.calls.length).toBeGreaterThanOrEqual(3)
|
|
expect(rmSync.mock.calls.length).toBeLessThanOrEqual(5)
|
|
})
|
|
|
|
test('rmTree does not retry unrelated failures like ENOENT or EPERM', async () => {
|
|
const { rmTree } = await import('../scripts/build/frontend-common.mjs')
|
|
rmSync.mockReset()
|
|
rmSync.mockImplementationOnce(() => { throw Object.assign(new Error('no such file'), { code: 'ENOENT' }) })
|
|
await expect(rmTree('/tmp/product-scratch')).rejects.toThrow('no such file')
|
|
expect(rmSync).toHaveBeenCalledTimes(1)
|
|
})
|