|
1 | 1 | import {zip, brotliCompress} from './archiver.js' |
2 | | -import {fileExists, inTemporaryDirectory, mkdir, touchFile} from './fs.js' |
| 2 | +import {fileExists, inTemporaryDirectory, mkdir, touchFile, removeFile, readFile} from './fs.js' |
3 | 3 | import {joinPath, dirname} from './path.js' |
4 | 4 | import {exec} from './system.js' |
5 | | -import {describe, expect, test} from 'vitest' |
| 5 | +import {describe, expect, test, vi} from 'vitest' |
6 | 6 | import StreamZip from 'node-stream-zip' |
7 | 7 | import brotli from 'brotli' |
8 | 8 | import fs from 'fs' |
| 9 | +import {glob} from './fs.js' |
9 | 10 |
|
10 | 11 | describe('zip', () => { |
11 | 12 | test('zips a directory', async () => { |
@@ -57,6 +58,51 @@ describe('zip', () => { |
57 | 58 | expect(expectedEntries.sort()).toEqual(archiveEntries.sort()) |
58 | 59 | }) |
59 | 60 | }) |
| 61 | + |
| 62 | + test('gracefully handles files deleted during archiving', async () => { |
| 63 | + await inTemporaryDirectory(async (tmpDir) => { |
| 64 | + // Given |
| 65 | + const zipPath = joinPath(tmpDir, 'output.zip') |
| 66 | + const outputDirectoryName = 'output' |
| 67 | + const outputDirectoryPath = joinPath(tmpDir, outputDirectoryName) |
| 68 | + const structure = ['file1.js', 'file2.js', 'file3.js'] |
| 69 | + |
| 70 | + await createFiles(structure, outputDirectoryPath) |
| 71 | + |
| 72 | + const file2Path = joinPath(outputDirectoryPath, 'file2.js') |
| 73 | + |
| 74 | + // Spy on readFile to simulate a file being deleted during archiving |
| 75 | + // We'll make readFile throw ENOENT for file2.js specifically |
| 76 | + const fsModule = await import('./fs.js') |
| 77 | + const originalReadFile = fsModule.readFile |
| 78 | + const readFileSpy = vi.spyOn(fsModule, 'readFile') |
| 79 | + readFileSpy.mockImplementation(async (path: string) => { |
| 80 | + if (path === file2Path) { |
| 81 | + const error: NodeJS.ErrnoException = new Error(`ENOENT: no such file or directory, open '${path}'`) |
| 82 | + error.code = 'ENOENT' |
| 83 | + throw error |
| 84 | + } |
| 85 | + return originalReadFile(path) |
| 86 | + }) |
| 87 | + |
| 88 | + // When - should not throw even though file2.js throws ENOENT |
| 89 | + await expect( |
| 90 | + zip({ |
| 91 | + inputDirectory: outputDirectoryPath, |
| 92 | + outputZipPath: zipPath, |
| 93 | + }), |
| 94 | + ).resolves.not.toThrow() |
| 95 | + |
| 96 | + // Then - archive should contain remaining files only |
| 97 | + const archiveEntries = await readArchiveFiles(zipPath) |
| 98 | + expect(archiveEntries).toContain('file1.js') |
| 99 | + expect(archiveEntries).toContain('file3.js') |
| 100 | + // file2.js should not be in archive since readFile failed |
| 101 | + expect(archiveEntries).not.toContain('file2.js') |
| 102 | + |
| 103 | + readFileSpy.mockRestore() |
| 104 | + }) |
| 105 | + }) |
60 | 106 | }) |
61 | 107 |
|
62 | 108 | describe('brotliCompress', () => { |
@@ -141,6 +187,48 @@ describe('brotliCompress', () => { |
141 | 187 | expect(fs.existsSync(joinPath(extractPath, 'test.json'))).toBeFalsy() |
142 | 188 | }) |
143 | 189 | }) |
| 190 | + |
| 191 | + test('gracefully handles files deleted during compression', async () => { |
| 192 | + await inTemporaryDirectory(async (tmpDir) => { |
| 193 | + // Given |
| 194 | + const brotliPath = joinPath(tmpDir, 'output.br') |
| 195 | + const outputDirectoryName = 'output' |
| 196 | + const outputDirectoryPath = joinPath(tmpDir, outputDirectoryName) |
| 197 | + const structure = ['file1.js', 'file2.js', 'file3.js'] |
| 198 | + |
| 199 | + await createFiles(structure, outputDirectoryPath) |
| 200 | + |
| 201 | + const file2Path = joinPath(outputDirectoryPath, 'file2.js') |
| 202 | + |
| 203 | + // Spy on readFile to simulate a file being deleted during compression |
| 204 | + // We'll make readFile throw ENOENT for file2.js specifically |
| 205 | + const fsModule = await import('./fs.js') |
| 206 | + const originalReadFile = fsModule.readFile |
| 207 | + const readFileSpy = vi.spyOn(fsModule, 'readFile') |
| 208 | + readFileSpy.mockImplementation(async (path: string) => { |
| 209 | + if (path === file2Path) { |
| 210 | + const error: NodeJS.ErrnoException = new Error(`ENOENT: no such file or directory, open '${path}'`) |
| 211 | + error.code = 'ENOENT' |
| 212 | + throw error |
| 213 | + } |
| 214 | + return originalReadFile(path) |
| 215 | + }) |
| 216 | + |
| 217 | + // When - should not throw even though file2.js throws ENOENT |
| 218 | + await expect( |
| 219 | + brotliCompress({ |
| 220 | + inputDirectory: outputDirectoryPath, |
| 221 | + outputPath: brotliPath, |
| 222 | + }), |
| 223 | + ).resolves.not.toThrow() |
| 224 | + |
| 225 | + // Then - compressed file should exist and be valid |
| 226 | + const exists = await fileExists(brotliPath) |
| 227 | + expect(exists).toBeTruthy() |
| 228 | + |
| 229 | + readFileSpy.mockRestore() |
| 230 | + }) |
| 231 | + }) |
144 | 232 | }) |
145 | 233 |
|
146 | 234 | async function createFiles(structure: string[], directory: string) { |
|
0 commit comments