'use strict' process.setMaxListeners(1000000); const fs = require('fs') const {test} = require('tap') const requireInject = require('require-inject') // defining mock for fs so its functions can be modified 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(new Error('ENOOPEN')) cb(null, tmpfile) }, write (fd) { const cb = arguments[arguments.length - 1] if (/nowrite/.test(fd)) return cb(new Error('ENOWRITE')) cb() }, fsync (fd, cb) { if (/nofsync/.test(fd)) return cb(new Error('ENOFSYNC')) cb() }, close (fd, cb) { cb() }, chown (tmpfile, uid, gid, cb) { if (/nochown/.test(tmpfile)) return cb(new Error('ENOCHOWN')) cb() }, chmod (tmpfile, mode, cb) { if (/nochmod/.test(tmpfile)) return cb(new Error('ENOCHMOD')) cb() }, rename (tmpfile, filename, cb) { if (/norename/.test(tmpfile)) return cb(new Error('ENORENAME')) cb() }, unlink (tmpfile, cb) { if (/nounlink/.test(tmpfile)) return cb(new Error('ENOUNLINK')) cb() }, stat (tmpfile, cb) { if (/nostat/.test(tmpfile)) return cb(new Error('ENOSTAT')) cb() }, /* SYNC */ mkdirSync (filename) {}, realpathSync (filename, cb) { return filename }, openSync (tmpfile, options) { if (/noopen/.test(tmpfile)) throw new Error('ENOOPEN') return tmpfile }, writeSync (fd) { if (/nowrite/.test(fd)) throw new Error('ENOWRITE') }, fsyncSync (fd) { if (/nofsync/.test(fd)) throw new Error('ENOFSYNC') }, closeSync () {}, chownSync (tmpfile, uid, gid) { if (/nochown/.test(tmpfile)) throw new Error('ENOCHOWN') }, chmodSync (tmpfile, mode) { if (/nochmod/.test(tmpfile)) throw new Error('ENOCHMOD') }, renameSync (tmpfile, filename) { if (/norename/.test(tmpfile)) throw new Error('ENORENAME') }, unlinkSync (tmpfile) { if (/nounlink/.test(tmpfile)) throw new Error('ENOUNLINK') }, statSync (tmpfile) { if (/nostat/.test(tmpfile)) throw new Error('ENOSTAT') } }) const {writeFile: writeFileAtomic} = requireInject('../dist', { fs: fsMock }); // preserve original functions const oldRealPath = fsMock.realpath const oldRename = fsMock.rename test('ensure writes to the same file are serial', t => { let fileInUse = false const ops = 5 // count for how many concurrent write ops to request t.plan(ops * 3 + 3) fsMock.realpath = (...args) => { t.false(fileInUse, 'file not in use') fileInUse = true oldRealPath(...args) } fsMock.rename = (...args) => { t.true(fileInUse, 'file in use') fileInUse = false oldRename(...args) } const {writeFile: writeFileAtomic} = requireInject('../dist', { fs: fsMock }); for (let i = 0; i < ops; i++) { writeFileAtomic('test', 'test', err => { if (err) t.fail(err) else t.pass('wrote without error') }) } setTimeout(() => { writeFileAtomic('test', 'test', err => { if (err) t.fail(err) else t.pass('successive writes after delay') }) }, 500) }) test('allow write to multiple files in parallel, but same file writes are serial', t => { const filesInUse = [] const ops = 5 let wasParallel = false fsMock.realpath = (filename, ...args) => { filesInUse.push(filename) const firstOccurence = filesInUse.indexOf(filename) t.equal(filesInUse.indexOf(filename, firstOccurence + 1), -1, 'serial writes') // check for another occurence after the first if (filesInUse.length > 1) wasParallel = true // remember that a parallel operation took place oldRealPath(filename, ...args) } fsMock.rename = (filename, ...args) => { filesInUse.splice(filesInUse.indexOf(filename), 1) oldRename(filename, ...args) } const {writeFile: writeFileAtomic} = requireInject('../dist', { fs: fsMock }); t.plan(ops * 2 * 2 + 1) let opCount = 0 for (let i = 0; i < ops; i++) { writeFileAtomic('test', 'test', err => { if (err) t.fail(err, 'wrote without error') else t.pass('wrote without error') }) writeFileAtomic('test2', 'test', err => { opCount++ if (opCount === ops) t.true(wasParallel, 'parallel writes') if (err) t.fail(err, 'wrote without error') else t.pass('wrote without error') }) } })