2023-10-03 11:14:36 +08:00
|
|
|
'use strict'
|
|
|
|
|
|
|
|
process.setMaxListeners(1000000);
|
|
|
|
|
|
|
|
const _ = require('lodash')
|
|
|
|
const fs = require('fs')
|
|
|
|
const os = require('os')
|
|
|
|
const path = require('path')
|
|
|
|
const {test} = require('tap')
|
|
|
|
const requireInject = require('require-inject')
|
|
|
|
|
|
|
|
let expectClose = 0
|
|
|
|
let closeCalled = 0
|
|
|
|
let expectCloseSync = 0
|
|
|
|
let closeSyncCalled = 0
|
|
|
|
const createErr = code => Object.assign(new Error(code), { code })
|
|
|
|
|
|
|
|
let unlinked = []
|
|
|
|
|
|
|
|
const fsMock = Object.assign ( {}, fs, {
|
|
|
|
/* ASYNC */
|
|
|
|
mkdir (filename, opts, cb) {
|
|
|
|
return cb(null);
|
|
|
|
},
|
|
|
|
realpath (filename, cb) {
|
|
|
|
return cb(null, filename)
|
|
|
|
},
|
|
|
|
open (tmpfile, options, mode, cb) {
|
|
|
|
if (/noopen/.test(tmpfile)) return cb(createErr('ENOOPEN'))
|
|
|
|
expectClose++
|
|
|
|
cb(null, tmpfile)
|
|
|
|
},
|
|
|
|
write (fd) {
|
|
|
|
const cb = arguments[arguments.length - 1]
|
|
|
|
if (/nowrite/.test(fd)) return cb(createErr('ENOWRITE'))
|
|
|
|
cb()
|
|
|
|
},
|
|
|
|
fsync (fd, cb) {
|
|
|
|
if (/nofsync/.test(fd)) return cb(createErr('ENOFSYNC'))
|
|
|
|
cb()
|
|
|
|
},
|
|
|
|
close (fd, cb) {
|
|
|
|
closeCalled++
|
|
|
|
cb()
|
|
|
|
},
|
|
|
|
chown (tmpfile, uid, gid, cb) {
|
|
|
|
if (/nochown/.test(tmpfile)) return cb(createErr('ENOCHOWN'))
|
|
|
|
if (/enosys/.test(tmpfile)) return cb(createErr('ENOSYS'))
|
|
|
|
if (/einval/.test(tmpfile)) return cb(createErr('EINVAL'))
|
|
|
|
if (/eperm/.test(tmpfile)) return cb(createErr('EPERM'))
|
|
|
|
cb()
|
|
|
|
},
|
|
|
|
chmod (tmpfile, mode, cb) {
|
|
|
|
if (/nochmod/.test(tmpfile)) return cb(createErr('ENOCHMOD'))
|
|
|
|
if (/enosys/.test(tmpfile)) return cb(createErr('ENOSYS'))
|
|
|
|
if (/eperm/.test(tmpfile)) return cb(createErr('EPERM'))
|
|
|
|
if (/einval/.test(tmpfile)) return cb(createErr('EINVAL'))
|
|
|
|
cb()
|
|
|
|
},
|
|
|
|
rename (tmpfile, filename, cb) {
|
|
|
|
if (/norename/.test(tmpfile)) return cb(createErr('ENORENAME'))
|
|
|
|
cb()
|
|
|
|
},
|
|
|
|
unlink (tmpfile, cb) {
|
|
|
|
if (/nounlink/.test(tmpfile)) return cb(createErr('ENOUNLINK'))
|
|
|
|
cb()
|
|
|
|
},
|
|
|
|
stat (tmpfile, cb) {
|
|
|
|
if (/nostat/.test(tmpfile)) return cb(createErr('ENOSTAT'))
|
|
|
|
if (/statful/.test(tmpfile)) return cb(null, fs.statSync('/'));
|
|
|
|
cb()
|
|
|
|
},
|
|
|
|
/* SYNC */
|
|
|
|
mkdirSync (filename) {},
|
|
|
|
realpathSync (filename, cb) {
|
|
|
|
return filename
|
|
|
|
},
|
|
|
|
openSync (tmpfile, options) {
|
|
|
|
if (/noopen/.test(tmpfile)) throw createErr('ENOOPEN')
|
|
|
|
expectCloseSync++
|
|
|
|
return tmpfile
|
|
|
|
},
|
|
|
|
writeSync (fd) {
|
|
|
|
if (/nowrite/.test(fd)) throw createErr('ENOWRITE')
|
|
|
|
},
|
|
|
|
fsyncSync (fd) {
|
|
|
|
if (/nofsync/.test(fd)) throw createErr('ENOFSYNC')
|
|
|
|
},
|
|
|
|
closeSync (fd) {
|
|
|
|
closeSyncCalled++
|
|
|
|
},
|
|
|
|
chownSync (tmpfile, uid, gid) {
|
|
|
|
if (/nochown/.test(tmpfile)) throw createErr('ENOCHOWN')
|
|
|
|
if (/enosys/.test(tmpfile)) throw createErr('ENOSYS')
|
|
|
|
if (/einval/.test(tmpfile)) throw createErr('EINVAL')
|
|
|
|
if (/eperm/.test(tmpfile)) throw createErr('EPERM')
|
|
|
|
},
|
|
|
|
chmodSync (tmpfile, mode) {
|
|
|
|
if (/nochmod/.test(tmpfile)) throw createErr('ENOCHMOD')
|
|
|
|
if (/enosys/.test(tmpfile)) throw createErr('ENOSYS')
|
|
|
|
if (/einval/.test(tmpfile)) throw createErr('EINVAL')
|
|
|
|
if (/eperm/.test(tmpfile)) throw createErr('EPERM')
|
|
|
|
},
|
|
|
|
renameSync (tmpfile, filename) {
|
|
|
|
if (/norename/.test(tmpfile)) throw createErr('ENORENAME')
|
|
|
|
},
|
|
|
|
unlinkSync (tmpfile) {
|
|
|
|
if (/nounlink/.test(tmpfile)) throw createErr('ENOUNLINK')
|
|
|
|
unlinked.push(tmpfile)
|
|
|
|
},
|
|
|
|
statSync (tmpfile) {
|
|
|
|
if (/nostat/.test(tmpfile)) throw createErr('ENOSTAT')
|
|
|
|
if (/statful/.test(tmpfile)) return fs.statSync('/');
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
const makeUnstableAsyncFn = function () {
|
|
|
|
return function () {
|
|
|
|
if ( Math.random () <= .75 ) {
|
|
|
|
const code = _.shuffle ([ 'EMFILE', 'ENFILE', 'EAGAIN', 'EBUSY', 'EACCESS', 'EPERM' ])[0];
|
|
|
|
throw createErr ( code );
|
|
|
|
}
|
|
|
|
return arguments[arguments.length -1](null, arguments[0]);
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
const makeUnstableSyncFn = function ( fn ) {
|
|
|
|
return function () {
|
|
|
|
if ( Math.random () <= .75 ) {
|
|
|
|
const code = _.shuffle ([ 'EMFILE', 'ENFILE', 'EAGAIN', 'EBUSY', 'EACCESS', 'EPERM' ])[0];
|
|
|
|
throw createErr ( code );
|
|
|
|
}
|
|
|
|
return fn.apply(undefined, arguments)
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
const fsMockUnstable = Object.assign ( {}, fsMock, {
|
|
|
|
open: makeUnstableAsyncFn (),
|
|
|
|
write: makeUnstableAsyncFn (),
|
|
|
|
fsync: makeUnstableAsyncFn (),
|
|
|
|
close: makeUnstableAsyncFn (),
|
|
|
|
rename: makeUnstableAsyncFn (),
|
|
|
|
openSync: makeUnstableSyncFn ( _.identity ),
|
|
|
|
writeSync: makeUnstableSyncFn ( _.noop ),
|
|
|
|
fsyncSync: makeUnstableSyncFn ( _.noop ),
|
|
|
|
closeSync: makeUnstableSyncFn ( _.noop ),
|
|
|
|
renameSync: makeUnstableSyncFn ( _.noop )
|
|
|
|
});
|
|
|
|
|
|
|
|
const {writeFile: writeFileAtomic, writeFileSync: writeFileAtomicSync} = requireInject('../dist', { fs: fsMock });
|
|
|
|
|
|
|
|
test('async tests', t => {
|
|
|
|
t.plan(2)
|
|
|
|
|
|
|
|
expectClose = 0
|
|
|
|
closeCalled = 0
|
|
|
|
t.teardown(() => {
|
|
|
|
t.parent.equal(closeCalled, expectClose, 'async tests closed all files')
|
|
|
|
expectClose = 0
|
|
|
|
closeCalled = 0
|
|
|
|
})
|
|
|
|
|
|
|
|
t.test('non-root tests', t => {
|
|
|
|
t.plan(28)
|
|
|
|
|
|
|
|
writeFileAtomic('good', 'test', { mode: '0777' }, err => {
|
|
|
|
t.notOk(err, 'No errors occur when passing in options')
|
|
|
|
})
|
|
|
|
writeFileAtomic('good', 'test', 'utf8', err => {
|
|
|
|
t.notOk(err, 'No errors occur when passing in options as string')
|
|
|
|
})
|
|
|
|
writeFileAtomic('good', 'test', undefined, err => {
|
|
|
|
t.notOk(err, 'No errors occur when NOT passing in options')
|
|
|
|
})
|
|
|
|
writeFileAtomic('good', 'test', err => {
|
|
|
|
t.notOk(err)
|
|
|
|
})
|
|
|
|
writeFileAtomic('noopen', 'test', err => {
|
|
|
|
t.is(err && err.message, 'ENOOPEN', 'fs.open failures propagate')
|
|
|
|
})
|
|
|
|
writeFileAtomic('nowrite', 'test', err => {
|
|
|
|
t.is(err && err.message, 'ENOWRITE', 'fs.writewrite failures propagate')
|
|
|
|
})
|
|
|
|
writeFileAtomic('nowrite', Buffer.from('test', 'utf8'), err => {
|
|
|
|
t.is(err && err.message, 'ENOWRITE', 'fs.writewrite failures propagate for buffers')
|
|
|
|
})
|
|
|
|
writeFileAtomic('nochown', 'test', { chown: { uid: 100, gid: 100 } }, err => {
|
|
|
|
t.is(err && err.message, 'ENOCHOWN', 'Chown failures propagate')
|
|
|
|
})
|
|
|
|
writeFileAtomic('nochown', 'test', err => {
|
|
|
|
t.notOk(err, 'No attempt to chown when no uid/gid passed in')
|
|
|
|
})
|
|
|
|
writeFileAtomic('nochmod', 'test', { mode: parseInt('741', 8) }, err => {
|
|
|
|
t.is(err && err.message, 'ENOCHMOD', 'Chmod failures propagate')
|
|
|
|
})
|
|
|
|
writeFileAtomic('nofsyncopt', 'test', { fsync: false }, err => {
|
|
|
|
t.notOk(err, 'fsync skipped if options.fsync is false')
|
|
|
|
})
|
|
|
|
writeFileAtomic('norename', 'test', err => {
|
|
|
|
t.is(err && err.message, 'ENORENAME', 'Rename errors propagate')
|
|
|
|
})
|
|
|
|
writeFileAtomic('norename nounlink', 'test', err => {
|
|
|
|
t.is(err && err.message, 'ENORENAME', 'Failure to unlink the temp file does not clobber the original error')
|
|
|
|
})
|
|
|
|
writeFileAtomic('nofsync', 'test', err => {
|
|
|
|
t.is(err && err.message, 'ENOFSYNC', 'Fsync failures propagate')
|
|
|
|
})
|
|
|
|
writeFileAtomic('enosys', 'test', err => {
|
|
|
|
t.notOk(err, 'No errors on ENOSYS')
|
|
|
|
})
|
|
|
|
writeFileAtomic('einval', 'test', { mode: 0o741 }, err => {
|
|
|
|
t.notOk(err, 'No errors on EINVAL for non root')
|
|
|
|
})
|
|
|
|
writeFileAtomic('eperm', 'test', { mode: 0o741 }, err => {
|
|
|
|
t.notOk(err, 'No errors on EPERM for non root')
|
|
|
|
})
|
|
|
|
writeFileAtomic('einval', 'test', { chown: { uid: 100, gid: 100 } }, err => {
|
|
|
|
t.notOk(err, 'No errors on EINVAL for non root')
|
|
|
|
})
|
|
|
|
writeFileAtomic('eperm', 'test', { chown: { uid: 100, gid: 100 } }, err => {
|
|
|
|
t.notOk(err, 'No errors on EPERM for non root')
|
|
|
|
})
|
|
|
|
const optionsImmutable = {};
|
|
|
|
writeFileAtomic('statful', 'test', optionsImmutable, err => {
|
|
|
|
t.notOk(err);
|
|
|
|
t.deepEquals(optionsImmutable, {});
|
|
|
|
});
|
|
|
|
const schedule = filePath => {
|
|
|
|
t.is(filePath, 'good');
|
|
|
|
return new Promise ( resolve => {
|
|
|
|
resolve ( () => {
|
|
|
|
t.is(true,true);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
};
|
|
|
|
writeFileAtomic('good','test', {schedule}, err => {
|
|
|
|
t.notOk(err);
|
|
|
|
});
|
|
|
|
const tmpCreate = filePath => `.${filePath}.custom`;
|
|
|
|
const tmpCreated = filePath => t.is ( filePath, '.good.custom' );
|
|
|
|
writeFileAtomic('good','test', {tmpCreate, tmpCreated}, err => {
|
|
|
|
t.notOk(err)
|
|
|
|
})
|
|
|
|
const longPath = path.join(os.tmpdir(),'.012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789.txt');
|
|
|
|
const {writeFile: writeFileAtomicNative} = requireInject('../dist', { fs });
|
|
|
|
writeFileAtomicNative(longPath,'test', err => {
|
|
|
|
t.notOk(err)
|
|
|
|
})
|
|
|
|
const pathMissingFolders = path.join(os.tmpdir(),String(Math.random()),String(Math.random()),String(Math.random()),'foo.txt');
|
|
|
|
writeFileAtomicNative(pathMissingFolders,'test', err => {
|
|
|
|
t.notOk(err)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
t.test('errors for root', t => {
|
|
|
|
const { getuid } = process
|
|
|
|
process.getuid = () => 0
|
|
|
|
t.teardown(() => {
|
|
|
|
process.getuid = getuid
|
|
|
|
})
|
|
|
|
const {writeFile: writeFileAtomic} = requireInject('../dist', { fs: fsMock });
|
|
|
|
t.plan(2)
|
|
|
|
writeFileAtomic('einval', 'test', { chown: { uid: 100, gid: 100 } }, err => {
|
|
|
|
t.match(err, { code: 'EINVAL' })
|
|
|
|
})
|
|
|
|
writeFileAtomic('einval', 'test', { mode: 0o741 }, err => {
|
|
|
|
t.match(err, { code: 'EINVAL' })
|
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
test('unstable async tests', t => {
|
|
|
|
t.plan(2);
|
|
|
|
const {writeFile: writeFileAtomic} = requireInject('../dist', { fs: fsMockUnstable });
|
|
|
|
writeFileAtomic('good', 'test', err => {
|
|
|
|
t.notOk(err, 'No errors occur when retryable errors are thrown')
|
|
|
|
})
|
|
|
|
writeFileAtomic('good', 'test', { timeout: 0 }, err => {
|
|
|
|
t.is(!!err.code, true, 'Retrying can be disabled')
|
|
|
|
})
|
|
|
|
});
|
|
|
|
|
|
|
|
test('sync tests', t => {
|
|
|
|
t.plan(2)
|
|
|
|
closeSyncCalled = 0
|
|
|
|
expectCloseSync = 0
|
|
|
|
t.teardown(() => {
|
|
|
|
t.parent.equal(closeSyncCalled, expectCloseSync, 'sync closed all files')
|
|
|
|
expectCloseSync = 0
|
|
|
|
closeSyncCalled = 0
|
|
|
|
})
|
|
|
|
|
|
|
|
const throws = function (t, shouldthrow, msg, todo) {
|
|
|
|
let err
|
|
|
|
try { todo() } catch (e) { err = e }
|
|
|
|
t.is(shouldthrow, err && err.message, msg)
|
|
|
|
}
|
|
|
|
const noexception = function (t, msg, todo) {
|
|
|
|
let err
|
|
|
|
try { todo() } catch (e) { err = e }
|
|
|
|
t.ifError(err, msg)
|
|
|
|
}
|
|
|
|
let tmpfile
|
|
|
|
|
|
|
|
t.test('non-root', t => {
|
|
|
|
t.plan(38)
|
|
|
|
noexception(t, 'No errors occur when passing in options', () => {
|
|
|
|
writeFileAtomicSync('good', 'test', { mode: '0777' })
|
|
|
|
})
|
|
|
|
noexception(t, 'No errors occur when passing in options as string', () => {
|
|
|
|
writeFileAtomicSync('good', 'test', 'utf8')
|
|
|
|
})
|
|
|
|
noexception(t, 'No errors occur when NOT passing in options', () => {
|
|
|
|
writeFileAtomicSync('good', 'test')
|
|
|
|
})
|
|
|
|
noexception(t, 'fsync never called if options.fsync is falsy', () => {
|
|
|
|
writeFileAtomicSync('good', 'test', { fsync: false })
|
|
|
|
})
|
|
|
|
noexception(t, 'tmpCreated is called on success', () => {
|
|
|
|
writeFileAtomicSync('good', 'test', {
|
|
|
|
tmpCreated (gottmpfile) {
|
|
|
|
tmpfile = gottmpfile
|
|
|
|
}
|
|
|
|
})
|
|
|
|
t.match(tmpfile, /^good\.tmp-\w+$/, 'tmpCreated called for success')
|
|
|
|
t.match(tmpfile, /^good\.tmp-\d{10}[a-f0-9]{6}$/, 'tmpCreated format')
|
|
|
|
})
|
|
|
|
|
|
|
|
tmpfile = undefined
|
|
|
|
throws(t, 'ENOOPEN', 'fs.openSync failures propagate', () => {
|
|
|
|
writeFileAtomicSync('noopen', 'test', {
|
|
|
|
tmpCreated (gottmpfile) {
|
|
|
|
tmpfile = gottmpfile
|
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
t.is(tmpfile, undefined, 'tmpCreated not called for open failure')
|
|
|
|
|
|
|
|
throws(t, 'ENOWRITE', 'fs.writeSync failures propagate', () => {
|
|
|
|
writeFileAtomicSync('nowrite', 'test', {
|
|
|
|
tmpCreated (gottmpfile) {
|
|
|
|
tmpfile = gottmpfile
|
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
t.match(tmpfile, /^nowrite\.tmp-\w+$/, 'tmpCreated called for failure after open')
|
|
|
|
|
|
|
|
throws(t, 'ENOCHOWN', 'Chown failures propagate', () => {
|
|
|
|
writeFileAtomicSync('nochown', 'test', { chown: { uid: 100, gid: 100 } })
|
|
|
|
})
|
|
|
|
noexception(t, 'No attempt to chown when false passed in', () => {
|
|
|
|
writeFileAtomicSync('nochown', 'test', { chown: false })
|
|
|
|
})
|
|
|
|
noexception(t, 'No errors occured when chown is undefined and original file owner used', () => {
|
|
|
|
writeFileAtomicSync('chowncopy', 'test', { chown: undefined })
|
|
|
|
})
|
|
|
|
throws(t, 'ENORENAME', 'Rename errors propagate', () => {
|
|
|
|
writeFileAtomicSync('norename', 'test')
|
|
|
|
})
|
|
|
|
throws(t, 'ENORENAME', 'Failure to unlink the temp file does not clobber the original error', () => {
|
|
|
|
writeFileAtomicSync('norename nounlink', 'test')
|
|
|
|
})
|
|
|
|
throws(t, 'ENOFSYNC', 'Fsync errors propagate', () => {
|
|
|
|
writeFileAtomicSync('nofsync', 'test')
|
|
|
|
})
|
|
|
|
noexception(t, 'No errors on ENOSYS', () => {
|
|
|
|
writeFileAtomicSync('enosys', 'test', { chown: { uid: 100, gid: 100 } })
|
|
|
|
})
|
|
|
|
noexception(t, 'No errors on EINVAL for non root', () => {
|
|
|
|
writeFileAtomicSync('einval', 'test', { chown: { uid: 100, gid: 100 } })
|
|
|
|
})
|
|
|
|
noexception(t, 'No errors on EPERM for non root', () => {
|
|
|
|
writeFileAtomicSync('eperm', 'test', { chown: { uid: 100, gid: 100 } })
|
|
|
|
})
|
|
|
|
|
|
|
|
throws(t, 'ENOCHMOD', 'Chmod failures propagate', () => {
|
|
|
|
writeFileAtomicSync('nochmod', 'test', { mode: 0o741 })
|
|
|
|
})
|
|
|
|
noexception(t, 'No errors on EPERM for non root', () => {
|
|
|
|
writeFileAtomicSync('eperm', 'test', { mode: 0o741 })
|
|
|
|
})
|
|
|
|
noexception(t, 'No attempt to chmod when no mode provided', () => {
|
|
|
|
writeFileAtomicSync('nochmod', 'test', { mode: false })
|
|
|
|
})
|
|
|
|
const optionsImmutable = {};
|
|
|
|
noexception(t, 'options are immutable', () => {
|
|
|
|
writeFileAtomicSync('statful', 'test', optionsImmutable)
|
|
|
|
})
|
|
|
|
t.deepEquals(optionsImmutable, {});
|
|
|
|
const tmpCreate = filePath => `.${filePath}.custom`;
|
|
|
|
const tmpCreated = filePath => t.is ( filePath, '.good.custom' );
|
|
|
|
noexception(t, 'custom temp creator', () => {
|
|
|
|
writeFileAtomicSync('good', 'test', {tmpCreate, tmpCreated})
|
|
|
|
})
|
|
|
|
const path0 = path.join(os.tmpdir(),'atomically-test-0');
|
|
|
|
const tmpPath0 = path0 + '.temp';
|
|
|
|
noexception(t, 'temp files are purged on success', () => {
|
|
|
|
const {writeFileSync: writeFileAtomicSync} = requireInject('../dist', { fs });
|
|
|
|
writeFileAtomicSync(path0, 'test', {tmpCreate: () => tmpPath0})
|
|
|
|
})
|
|
|
|
t.is(true,fs.existsSync(path0));
|
|
|
|
t.is(false,fs.existsSync(tmpPath0));
|
|
|
|
const path1 = path.join(os.tmpdir(),'atomically-test-norename-1');
|
|
|
|
const tmpPath1 = path1 + '.temp';
|
|
|
|
throws(t, 'ENORENAME', 'temp files are purged on error', () => {
|
|
|
|
const {writeFileSync: writeFileAtomicSync} = requireInject('../dist', { fs: Object.assign ( {}, fs, { renameSync: fsMock.renameSync })});
|
|
|
|
writeFileAtomicSync(path1, 'test', {tmpCreate: () => tmpPath1})
|
|
|
|
})
|
|
|
|
t.is(false,fs.existsSync(path1));
|
|
|
|
t.is(false,fs.existsSync(tmpPath1));
|
|
|
|
const path2 = path.join(os.tmpdir(),'atomically-test-norename-2');
|
|
|
|
const tmpPath2 = path2 + '.temp';
|
|
|
|
throws(t, 'ENORENAME', 'temp files can also not be purged on error', () => {
|
|
|
|
const {writeFileSync: writeFileAtomicSync} = requireInject('../dist', { fs: Object.assign ( {}, fs, { renameSync: fsMock.renameSync })});
|
|
|
|
writeFileAtomicSync(path2, 'test', {tmpCreate: () => tmpPath2,tmpPurge: false})
|
|
|
|
})
|
|
|
|
t.is(false,fs.existsSync(path2));
|
|
|
|
t.is(true,fs.existsSync(tmpPath2));
|
|
|
|
const longPath = path.join(os.tmpdir(),'.012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789.txt');
|
|
|
|
noexception(t, 'temp files are truncated', () => {
|
|
|
|
const {writeFileSync: writeFileAtomicSync} = requireInject('../dist', { fs });
|
|
|
|
writeFileAtomicSync(longPath, 'test')
|
|
|
|
})
|
|
|
|
const pathMissingFolders = path.join(os.tmpdir(),String(Math.random()),String(Math.random()),String(Math.random()),'foo.txt');
|
|
|
|
noexception(t, 'parent folders are created', () => {
|
|
|
|
const {writeFileSync: writeFileAtomicSync} = requireInject('../dist', { fs });
|
|
|
|
writeFileAtomicSync(pathMissingFolders, 'test')
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
t.test('errors for root', t => {
|
|
|
|
const { getuid } = process
|
|
|
|
process.getuid = () => 0
|
|
|
|
t.teardown(() => {
|
|
|
|
process.getuid = getuid
|
|
|
|
})
|
|
|
|
const {writeFileSync: writeFileAtomicSync} = requireInject('../dist', { fs: fsMock });
|
|
|
|
t.plan(2)
|
|
|
|
throws(t, 'EINVAL', 'Chown error as root user', () => {
|
|
|
|
writeFileAtomicSync('einval', 'test', { chown: { uid: 100, gid: 100 } })
|
|
|
|
})
|
|
|
|
throws(t, 'EINVAL', 'Chmod error as root user', () => {
|
|
|
|
writeFileAtomicSync('einval', 'test', { mode: 0o741 })
|
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
test('unstable sync tests', t => {
|
|
|
|
t.plan(2);
|
|
|
|
|
|
|
|
const throws = function (t, msg, todo) {
|
|
|
|
let err
|
|
|
|
try { todo() } catch (e) { err = e }
|
|
|
|
t.is(!!err.code, true, msg)
|
|
|
|
}
|
|
|
|
const noexception = function (t, msg, todo) {
|
|
|
|
let err
|
|
|
|
try { todo() } catch (e) { err = e }
|
|
|
|
t.ifError(err, msg)
|
|
|
|
}
|
|
|
|
|
|
|
|
noexception(t, 'No errors occur when retryable errors are thrown', () => {
|
|
|
|
const {writeFileSync: writeFileAtomicSync} = requireInject('../dist', { fs: fsMockUnstable });
|
|
|
|
writeFileAtomicSync('good', 'test')
|
|
|
|
})
|
|
|
|
|
|
|
|
throws(t, 'retrying can be disabled', () => {
|
|
|
|
const {writeFileSync: writeFileAtomicSync} = requireInject('../dist', { fs: fsMockUnstable });
|
|
|
|
writeFileAtomicSync('good', 'test', { timeout: 0 })
|
|
|
|
})
|
|
|
|
});
|
|
|
|
|
|
|
|
test('promises', async t => {
|
|
|
|
let tmpfile
|
|
|
|
closeCalled = 0
|
|
|
|
expectClose = 0
|
|
|
|
t.teardown(() => {
|
|
|
|
t.parent.equal(closeCalled, expectClose, 'promises closed all files')
|
|
|
|
closeCalled = 0
|
|
|
|
expectClose = 0
|
|
|
|
})
|
|
|
|
|
|
|
|
await writeFileAtomic('good', 'test', {
|
|
|
|
tmpCreated (gottmpfile) {
|
|
|
|
tmpfile = gottmpfile
|
|
|
|
}
|
|
|
|
})
|
|
|
|
t.match(tmpfile, /^good\.tmp-\w+$/, 'tmpCreated is called for success')
|
|
|
|
|
|
|
|
await writeFileAtomic('good', 'test', {
|
|
|
|
tmpCreated (gottmpfile) {
|
|
|
|
return Promise.resolve()
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
tmpfile = undefined
|
|
|
|
await t.rejects(writeFileAtomic('noopen', 'test', {
|
|
|
|
tmpCreated (gottmpfile) {
|
|
|
|
tmpfile = gottmpfile
|
|
|
|
}
|
|
|
|
}))
|
|
|
|
t.is(tmpfile, undefined, 'tmpCreated is not called on open failure')
|
|
|
|
|
|
|
|
await t.rejects(writeFileAtomic('nowrite', 'test', {
|
|
|
|
tmpCreated (gottmpfile) {
|
|
|
|
tmpfile = gottmpfile
|
|
|
|
}
|
|
|
|
}))
|
|
|
|
t.match(tmpfile, /^nowrite\.tmp-\w+$/, 'tmpCreated is called if failure is after open')
|
|
|
|
})
|