Skip to content

Commit 734bb76

Browse files
committed
test: add test to ensure copyDirectory copies files atomically
Assisted-by: Claude Opus 4.6 Signed-off-by: David Sanders <dsanders11@ucsbalum.com>
1 parent 9d4cb30 commit 734bb76

1 file changed

Lines changed: 86 additions & 0 deletions

File tree

test/test-copy-directory.js

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
'use strict'
2+
3+
const { describe, it, afterEach } = require('mocha')
4+
const assert = require('assert')
5+
const path = require('path')
6+
const fs = require('fs')
7+
const { promises: fsp } = fs
8+
const os = require('os')
9+
const { FULL_TEST, platformTimeout } = require('./common')
10+
const copyDirectory = require('../lib/copy-directory')
11+
12+
describe('copyDirectory', function () {
13+
let timer
14+
let tmpDir
15+
16+
afterEach(async () => {
17+
if (tmpDir) {
18+
await fsp.rm(tmpDir, { recursive: true, force: true })
19+
tmpDir = null
20+
}
21+
clearInterval(timer)
22+
})
23+
24+
it('large file appears atomically (no partial writes visible)', async function () {
25+
if (!FULL_TEST) {
26+
return this.skip('Skipping due to test environment configuration')
27+
}
28+
29+
this.timeout(platformTimeout(5, { win32: 10 }))
30+
31+
tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'node-gyp-copy-test-'))
32+
const srcDir = path.join(tmpDir, 'src')
33+
const destDir = path.join(tmpDir, 'dest')
34+
await fsp.mkdir(srcDir)
35+
36+
const fileName = 'large.bin'
37+
const srcFile = path.join(srcDir, fileName)
38+
const destFile = path.join(destDir, fileName)
39+
40+
// Create a 5 GB sparse file — instant to create, consumes no real
41+
// disk, but fs.copyFile still has to process the full extent map so
42+
// the destination file is visible at size 0 and grows over time.
43+
// fs.rename() is atomic at the VFS level: the file either does not
44+
// exist at the destination or appears at its full size in one step.
45+
const fileSize = 5 * 1024 * 1024 * 1024
46+
const handle = await fsp.open(srcFile, 'w')
47+
await handle.truncate(fileSize)
48+
await handle.close()
49+
50+
// Tight synchronous poll: stat the destination on every event-loop
51+
// turn while copyDirectory runs concurrently.
52+
//
53+
// With the old fs.copyFile approach the dest file is created at
54+
// size 0 and grows → this loop catches it at a partial size.
55+
//
56+
// With fs.rename the file either isn't there yet (ENOENT) or is
57+
// already at its full size → zero violations.
58+
let polls = 0
59+
const violations = []
60+
61+
timer = setInterval(() => {
62+
try {
63+
const stat = fs.statSync(destFile)
64+
polls++
65+
if (stat.size !== fileSize) {
66+
violations.push({ poll: polls, size: stat.size })
67+
}
68+
} catch (err) {
69+
if (err.code !== 'ENOENT') throw err
70+
}
71+
}, 0)
72+
73+
await copyDirectory(srcDir, destDir)
74+
75+
clearInterval(timer)
76+
timer = undefined
77+
78+
console.log(` ${polls} stats observed the file during the operation`)
79+
80+
assert.strictEqual(violations.length, 0, 'file must never be observed at a partial size')
81+
82+
const finalStat = await fsp.stat(destFile)
83+
assert.strictEqual(finalStat.size, fileSize,
84+
'destination file should have the correct final size')
85+
})
86+
})

0 commit comments

Comments
 (0)