hexo/node_modules/atomically/test/basic.js

511 lines
18 KiB
JavaScript

'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')
})