diff --git a/cp-file-error.js b/cp-file-error.js new file mode 100644 index 0000000..f82c96c --- /dev/null +++ b/cp-file-error.js @@ -0,0 +1,13 @@ +'use strict'; +const NestedError = require('nested-error-stacks'); + +class CpFileError extends NestedError { + constructor(message, nested) { + super(message, nested); + Object.assign(this, nested); + } +} + +CpFileError.prototype.name = 'CpFileError'; + +module.exports = CpFileError; diff --git a/fs.js b/fs.js new file mode 100644 index 0000000..9b277a3 --- /dev/null +++ b/fs.js @@ -0,0 +1,97 @@ +'use strict'; +const fs = require('graceful-fs'); +const mkdirp = require('mkdirp'); +const pify = require('pify'); +const CpFileError = require('./cp-file-error'); + +const fsP = pify(fs); +const mkdirpP = pify(mkdirp); + +exports.closeSync = fs.closeSync.bind(fs); +exports.createWriteStream = fs.createWriteStream.bind(fs); + +exports.createReadStream = (path, options) => new Promise((resolve, reject) => { + const read = fs.createReadStream(path, options); + + read.on('error', err => { + reject(new CpFileError(`cannot read from \`${path}\`: ${err.message}`, err)); + }); + + read.on('readable', () => { + resolve(read); + }); + + read.on('end', () => { + resolve(read); + }); +}); + +exports.stat = path => fsP.stat(path).catch(err => { + throw new CpFileError(`cannot stat path \`${path}\`: ${err.message}`, err); +}); + +exports.lstat = path => fsP.lstat(path).catch(err => { + throw new CpFileError(`lstat \`${path}\` failed: ${err.message}`, err); +}); + +exports.utimes = (path, atime, mtime) => fsP.utimes(path, atime, mtime).catch(err => { + throw new CpFileError(`utimes \`${path}\` failed: ${err.message}`, err); +}); + +exports.openSync = (path, flags, mode) => { + try { + return fs.openSync(path, flags, mode); + } catch (err) { + if (flags.includes('w')) { + throw new CpFileError(`cannot write to \`${path}\`: ${err.message}`, err); + } + + throw new CpFileError(`cannot open \`${path}\`: ${err.message}`, err); + } +}; + +// eslint-disable-next-line max-params +exports.readSync = (fd, buffer, offset, length, position, path) => { + try { + return fs.readSync(fd, buffer, offset, length, position); + } catch (err) { + throw new CpFileError(`cannot read from \`${path}\`: ${err.message}`, err); + } +}; + +// eslint-disable-next-line max-params +exports.writeSync = (fd, buffer, offset, length, position, path) => { + try { + return fs.writeSync(fd, buffer, offset, length, position); + } catch (err) { + throw new CpFileError(`cannot write to \`${path}\`: ${err.message}`, err); + } +}; + +exports.fstatSync = (fd, path) => { + try { + return fs.fstatSync(fd); + } catch (err) { + throw new CpFileError(`stat \`${path}\` failed: ${err.message}`, err); + } +}; + +exports.futimesSync = (fd, atime, mtime, path) => { + try { + return fs.futimesSync(fd, atime, mtime, path); + } catch (err) { + throw new CpFileError(`utimes \`${path}\` failed: ${err.message}`, err); + } +}; + +exports.mkdirp = path => mkdirpP(path, {fs}).catch(err => { + throw new CpFileError(`cannot create directory \`${path}\`: ${err.message}`, err); +}); + +exports.mkdirpSync = path => { + try { + mkdirp.sync(path, {fs}); + } catch (err) { + throw new CpFileError(`cannot create directory \`${path}\`: ${err.message}`, err); + } +}; diff --git a/index.js b/index.js index 598c27e..6cae813 100644 --- a/index.js +++ b/index.js @@ -1,120 +1,53 @@ 'use strict'; -var EventEmitter = require('events').EventEmitter; -var path = require('path'); -var util = require('util'); -var stream = require('readable-stream'); -var pify = require('pify'); -var fs = require('graceful-fs'); -var mkdirp = require('mkdirp'); -var NestedError = require('nested-error-stacks'); +const path = require('path'); +const CpFileError = require('./cp-file-error'); +const fs = require('./fs'); +const ProgressEmitter = require('./progress-emitter'); -var mkdirpP = pify(mkdirp, Promise); -var fsP = pify(fs, Promise); - -function CpFileError(message, nested) { - NestedError.call(this, message, nested); - Object.assign(this, nested); -} - -util.inherits(CpFileError, NestedError); - -CpFileError.prototype.name = 'CpFileError'; - -module.exports = function (src, dest, opts) { +module.exports = (src, dest, opts) => { if (!src || !dest) { return Promise.reject(new CpFileError('`src` and `dest` required')); } opts = Object.assign({overwrite: true}, opts); - var size = 0; - var progressEmitter = new EventEmitter(); - - var absoluteSrc = path.resolve(src); - var absoluteDest = path.resolve(dest); - - var promise = fsP.stat(src).then(function (stat) { - size = stat.size; - }).catch(function (err) { - throw new CpFileError('cannot stat path `' + src + '`: ' + err.message, err); - }).then(function () { - var read = new stream.Readable().wrap(fs.createReadStream(src)); - return new Promise(function (resolve, reject) { - read.on('error', function (err) { - reject(new CpFileError('cannot read from `' + src + '`: ' + err.message, err)); - }); - - read.on('readable', function () { - resolve(read); - }); - - read.on('end', function () { - resolve(read); - }); - }); - }).then(function mkdirpDestDirectory(read) { - return mkdirpP(path.dirname(dest)).then(function () { - return read; - }).catch(function (err) { - if (err.code !== 'EEXIST') { - throw new CpFileError('cannot create directory `' + path.dirname(dest) + '`: ' + err.message, err); - } + const progressEmitter = new ProgressEmitter(path.resolve(src), path.resolve(dest)); - return read; - }); - }).then(function (read) { - return new Promise(function pipeToDest(resolve, reject) { - var write = fs.createWriteStream(dest, {flags: opts.overwrite ? 'w' : 'wx'}); + const promise = fs + .stat(src) + .then(stat => { + progressEmitter.size = stat.size; + }) + .then(() => fs.createReadStream(src)) + .then(read => fs.mkdirp(path.dirname(dest)).then(() => read)) + .then(read => new Promise((resolve, reject) => { + const write = fs.createWriteStream(dest, {flags: opts.overwrite ? 'w' : 'wx'}); - read.on('data', function () { - if (size === 0) { - return; - } - var written = write.bytesWritten; - var percent = written / size; - progressEmitter.emit('progress', { - src: absoluteSrc, - dest: absoluteDest, - size: size, - written: written, - percent: percent - }); + read.on('data', () => { + progressEmitter.written = write.bytesWritten; }); - write.on('error', function (err) { + write.on('error', err => { if (!opts.overwrite && err.code === 'EEXIST') { resolve(false); return; } - reject(new CpFileError('cannot write to `' + dest + '`: ' + err.message, err)); + reject(new CpFileError(`cannot write to \`${dest}\`: ${err.message}`, err)); }); - write.on('close', function () { - progressEmitter.emit('progress', { - src: absoluteSrc, - dest: absoluteDest, - size: size, - written: size, - percent: 1 - }); - + write.on('close', () => { + progressEmitter.written = progressEmitter.size; resolve(true); }); read.pipe(write); + })) + .then(updateTimes => { + if (updateTimes) { + return fs.lstat(src).then(stats => fs.utimes(dest, stats.atime, stats.mtime)); + } }); - }).then(function (updateTimes) { - if (updateTimes) { - return fsP.lstat(src).catch(function (err) { - throw new CpFileError('lstat `' + src + '` failed: ' + err.message, err); - }).then(function (stats) { - return fsP.utimes(dest, stats.atime, stats.mtime).catch(function (err) { - throw new CpFileError('utimes `' + dest + '` failed: ' + err.message, err); - }); - }); - } - }); promise.on = function () { progressEmitter.on.apply(progressEmitter, arguments); @@ -131,45 +64,18 @@ module.exports.sync = function (src, dest, opts) { opts = Object.assign({overwrite: true}, opts); - var read; - var bytesRead; - var pos; - var write; - var stat; - var BUF_LENGTH = 100 * 1024; - var buf = new Buffer(BUF_LENGTH); - - function readSync(pos) { - try { - return fs.readSync(read, buf, 0, BUF_LENGTH, pos); - } catch (err) { - throw new CpFileError('cannot read from `' + src + '`: ' + err.message, err); - } - } - - function writeSync() { - try { - fs.writeSync(write, buf, 0, bytesRead); - } catch (err) { - throw new CpFileError('cannot write to `' + dest + '`: ' + err.message, err); - } - } - - try { - read = fs.openSync(src, 'r'); - } catch (err) { - throw new CpFileError('cannot open `' + src + '`: ' + err.message, err); - } + let read; + let bytesRead; + let pos; + let write; + const BUF_LENGTH = 100 * 1024; + const buf = new Buffer(BUF_LENGTH); + const readSync = pos => fs.readSync(read, buf, 0, BUF_LENGTH, pos, src); + const writeSync = () => fs.writeSync(write, buf, 0, bytesRead, undefined, dest); + read = fs.openSync(src, 'r'); pos = bytesRead = readSync(0); - - try { - mkdirp.sync(path.dirname(dest)); - } catch (err) { - if (err.code !== 'EEXIST') { - throw new CpFileError('cannot create directory `' + path.dirname(dest) + '`: ' + err.message, err); - } - } + fs.mkdirpSync(path.dirname(dest)); try { write = fs.openSync(dest, opts.overwrite ? 'w' : 'wx'); @@ -178,7 +84,7 @@ module.exports.sync = function (src, dest, opts) { return; } - throw new CpFileError('cannot write to `' + dest + '`: ' + err.message, err); + throw err; } writeSync(); @@ -189,18 +95,8 @@ module.exports.sync = function (src, dest, opts) { pos += bytesRead; } - try { - stat = fs.fstatSync(read); - } catch (err) { - throw new CpFileError('stat `' + src + '` failed: ' + err.message, err); - } - - try { - fs.futimesSync(write, stat.atime, stat.mtime); - } catch (err) { - throw new CpFileError('utimes `' + dest + '` failed: ' + err.message, err); - } - + const stat = fs.fstatSync(read, src); + fs.futimesSync(write, stat.atime, stat.mtime, dest); fs.closeSync(read); fs.closeSync(write); }; diff --git a/package.json b/package.json index 55568f5..35f96fd 100644 --- a/package.json +++ b/package.json @@ -44,14 +44,14 @@ "graceful-fs": "^4.1.2", "mkdirp": "^0.5.0", "nested-error-stacks": "^1.0.1", - "pify": "^2.3.0", - "readable-stream": "^2.1.4" + "pify": "^2.3.0" }, "devDependencies": { "ava": "*", "coveralls": "^2.11.14", + "clear-require": "^1.0.1", "nyc": "^8.3.0", - "rewire": "^2.3.1", + "require-uncached": "^1.0.2", "rimraf": "^2.3.2", "uuid": "^2.0.3", "xo": "*" diff --git a/progress-emitter.js b/progress-emitter.js new file mode 100644 index 0000000..eed7236 --- /dev/null +++ b/progress-emitter.js @@ -0,0 +1,35 @@ +'use strict'; +const EventEmitter = require('events').EventEmitter; + +class ProgressEmitter extends EventEmitter { + constructor(src, dest) { + super(); + this.src = src; + this.dest = dest; + + let written; + Object.defineProperty(this, 'written', { + enumerable: true, + configurable: true, + get: () => written, + set: value => { + written = value; + this.emitProgress(); + } + }); + } + + emitProgress() { + const size = this.size; + const written = this.written; + this.emit('progress', { + src: this.src, + dest: this.dest, + size, + written, + percent: written === size ? 1 : written / size + }); + } +} + +module.exports = ProgressEmitter; diff --git a/test/_fs-errors.js b/test/_fs-errors.js new file mode 100644 index 0000000..f31fd9a --- /dev/null +++ b/test/_fs-errors.js @@ -0,0 +1,23 @@ +'use strict'; + +exports.buildEACCES = path => Object.assign(new Error(`EACCES: permission denied '${path}'`), { + errno: -13, + code: 'EACCES', + path +}); + +exports.buildENOSPC = () => Object.assign(new Error('ENOSPC, write'), { + errno: -28, + code: 'ENOSPC' +}); + +exports.buildENOENT = path => Object.assign(new Error(`ENOENT: no such file or directory '${path}'`), { + errno: -2, + code: 'ENOENT', + path +}); + +exports.buildEBADF = () => Object.assign(new Error(`EBADF: bad file descriptor`), { + errno: -9, + code: 'EBADF' +}); diff --git a/test/async.js b/test/async.js index 85fd743..cda09a2 100644 --- a/test/async.js +++ b/test/async.js @@ -1,15 +1,15 @@ import crypto from 'crypto'; import path from 'path'; import fs from 'graceful-fs'; -import pify from 'pify'; -import rewire from 'rewire'; +import requireUncached from 'require-uncached'; +import clearRequire from 'clear-require'; import rimraf from 'rimraf'; import test from 'ava'; import uuid from 'uuid'; import m from '../'; import {assertDateEqual} from './_assert'; +import {buildEACCES, buildENOSPC, buildENOENT} from './_fs-errors'; -const fsP = pify(fs); const THREE_HUNDRED_KILO = (100 * 3 * 1024) + 1; test.before(() => { @@ -78,15 +78,15 @@ test('do not overwrite when disabled', async t => { test('do not create dest on unreadable src', async t => { const err = await t.throws(m('node_modules', t.context.dest)); - t.is(err.name, 'CpFileError', 'wrong Error name: ' + err.stack); - t.is(err.code, 'EISDIR'); + t.is(err.name, 'CpFileError', err); + t.is(err.code, 'EISDIR', err); t.throws(() => fs.statSync(t.context.dest), /ENOENT/); }); test('do not create dest directory on unreadable src', async t => { - const err = await t.throws(m('node_modules', 'subdir/tmp')); - t.is(err.name, 'CpFileError', 'wrong Error name: ' + err.stack); - t.is(err.code, 'EISDIR'); + const err = await t.throws(m('node_modules', 'subdir/' + uuid.v4())); + t.is(err.name, 'CpFileError', err); + t.is(err.code, 'EISDIR', err); t.throws(() => fs.statSync('subdir'), /ENOENT/); }); @@ -100,118 +100,108 @@ test('preserve timestamps', async t => { test('throw an Error if `src` does not exists', async t => { const err = await t.throws(m('NO_ENTRY', t.context.dest)); - t.is(err.name, 'CpFileError', 'wrong Error name: ' + err.stack); - t.is(err.code, 'ENOENT'); - t.regex(err.message, /`NO_ENTRY`/); - t.regex(err.stack, /`NO_ENTRY`/); + t.is(err.name, 'CpFileError', err); + t.is(err.code, 'ENOENT', err); + t.regex(err.message, /`NO_ENTRY`/, err); + t.regex(err.stack, /`NO_ENTRY`/, err); }); test('rethrow mkdirp EACCES errors', async t => { - const sut = rewire('../'); - const dirPath = '/root/NO_ACCESS'; - const mkdirError = new Error(`EACCES, permission denied '${dirPath}'`); - + const mkdir = fs.mkdir; + const dirPath = '/root/NO_ACCESS_' + uuid.v4(); + const dest = dirPath + '/' + uuid.v4(); + const mkdirError = buildEACCES(dirPath); let called = 0; - mkdirError.errno = -13; - mkdirError.code = 'EACCES'; - mkdirError.path = dirPath; - - sut.__set__('mkdirpP', () => { - called++; - return Promise.reject(mkdirError); - }); - - const err = await t.throws(sut('license', dirPath + '/tmp')); - t.is(called, 1); - t.is(err.name, 'CpFileError', 'wrong Error name: ' + err.stack); - t.is(err.errno, mkdirError.errno); - t.is(err.code, mkdirError.code); - t.is(err.path, mkdirError.path); -}); - -test('ignore mkdirp EEXIST errors', async t => { - const sut = rewire('../'); - const dirPath = '/root/NO_ACCESS'; - const mkdirError = new Error(`EEXIST, mkdir '${dirPath}'`); - let called = 0; - - mkdirError.errno = -17; - mkdirError.code = 'EEXIST'; - mkdirError.path = dirPath; + fs.mkdir = (path, mode, cb) => { + if (path === dirPath) { + called++; + cb(mkdirError); + return; + } - sut.__set__('mkdirpP', () => { - called++; - return Promise.reject(mkdirError); - }); + mkdir(path, mode, cb); + }; - await sut('license', t.context.dest); + const err = await t.throws(m('license', dest)); + t.is(err.name, 'CpFileError', err); + t.is(err.errno, mkdirError.errno, err); + t.is(err.code, mkdirError.code, err); + t.is(err.path, mkdirError.path, err); t.is(called, 1); - t.is(fs.readFileSync(t.context.dest, 'utf8'), fs.readFileSync('license', 'utf8')); }); test('rethrow ENOSPC errors', async t => { - const sut = rewire('../'); - const noSpaceError = new Error('ENOSPC, write'); + const createWriteStream = fs.createWriteStream; + const noSpaceError = buildENOSPC(); let called = false; - noSpaceError.errno = -28; - noSpaceError.code = 'ENOSPC'; - - sut.__set__('fs', Object.assign({}, fs, { - createWriteStream: (path, options) => { - const stream = fs.createWriteStream(path, options); + fs.createWriteStream = (path, options) => { + const stream = createWriteStream(path, options); + if (path === t.context.dest) { stream.on('pipe', () => { if (!called) { called = true; stream.emit('error', noSpaceError); } }); - return stream; } - })); - - const err = await t.throws(sut('license', t.context.dest)); + return stream; + }; + + clearRequire('../fs'); + const uncached = requireUncached('../'); + const err = await t.throws(uncached('license', t.context.dest)); + t.is(err.name, 'CpFileError', err); + t.is(err.errno, noSpaceError.errno, err); + t.is(err.code, noSpaceError.code, err); t.true(called); - t.is(err.name, 'CpFileError', 'wrong Error name: ' + err.stack); - t.is(err.errno, noSpaceError.errno); - t.is(err.code, noSpaceError.code); }); test('rethrow stat errors', async t => { - const sut = rewire('../'); + const lstat = fs.lstat; + const fstatError = buildENOENT(); let called = 0; - sut.__set__('fsP', Object.assign({}, fsP, { - lstat: () => { + fs.writeFileSync(t.context.src, ''); + fs.lstat = (path, cb) => { + if (path === t.context.src) { called++; - - // reject Error: - return fsP.lstat(uuid.v4()); + cb(fstatError); + return; } - })); - const err = await t.throws(sut('license', t.context.dest)); + lstat(path, cb); + }; + + clearRequire('../fs'); + const uncached = requireUncached('../'); + const err = await t.throws(uncached(t.context.src, t.context.dest)); + t.is(err.name, 'CpFileError', err); + t.is(err.errno, fstatError.errno, err); + t.is(err.code, fstatError.code, err); t.is(called, 1); - t.is(err.name, 'CpFileError', 'wrong Error name: ' + err.stack); - t.is(err.code, 'ENOENT'); }); test('rethrow utimes errors', async t => { - const sut = rewire('../'); + const utimes = fs.utimes; + const utimesError = buildENOENT(); let called = 0; - sut.__set__('fsP', Object.assign({}, fsP, { - utimes: (path, atime, mtime) => { + fs.utimes = (path, atime, mtime, cb) => { + if (path === t.context.dest) { called++; - - // reject Error: - return fsP.utimes(uuid.v4(), atime, mtime); + cb(utimesError); + return; } - })); - const err = await t.throws(sut('license', t.context.dest)); + utimes(path, atime, mtime, cb); + }; + + clearRequire('../fs'); + const uncached = requireUncached('../'); + const err = await t.throws(uncached('license', t.context.dest)); t.is(called, 1); - t.is(err.name, 'CpFileError', 'wrong Error name: ' + err.stack); - t.is(err.code, 'ENOENT'); + t.is(err.name, 'CpFileError', err); + t.is(err.code, 'ENOENT', err); }); diff --git a/test/progress.js b/test/progress.js index 8f0006c..bd24674 100644 --- a/test/progress.js +++ b/test/progress.js @@ -40,7 +40,7 @@ test('report progress', async t => { t.true(calls > 0); }); -test('report progress of 100 on end', async t => { +test('report progress of 100% on end', async t => { const buf = crypto.pseudoRandomBytes(THREE_HUNDRED_KILO); fs.writeFileSync(t.context.src, buf); diff --git a/test/sync.js b/test/sync.js index a1adca9..4513f29 100644 --- a/test/sync.js +++ b/test/sync.js @@ -1,12 +1,12 @@ import crypto from 'crypto'; import path from 'path'; import fs from 'graceful-fs'; -import rewire from 'rewire'; import rimraf from 'rimraf'; import test from 'ava'; import uuid from 'uuid'; import m from '../'; import {assertDateEqual} from './_assert'; +import {buildEACCES, buildENOSPC, buildEBADF} from './_fs-errors'; const THREE_HUNDRED_KILO = (100 * 3 * 1024) + 1; @@ -76,15 +76,15 @@ test('do not overwrite when disabled', t => { test('do not create dest on unreadable src', t => { const err = t.throws(() => m.sync('node_modules', t.context.dest)); - t.is(err.name, 'CpFileError', 'wrong Error name: ' + err.stack); - t.is(err.code, 'EISDIR'); + t.is(err.name, 'CpFileError', err); + t.is(err.code, 'EISDIR', err); t.throws(() => fs.statSync(t.context.dest), /ENOENT/); }); test('do not create dest directory on unreadable src', t => { - const err = t.throws(() => m.sync('node_modules', 'subdir/tmp')); - t.is(err.name, 'CpFileError', 'wrong Error name: ' + err.stack); - t.is(err.code, 'EISDIR'); + const err = t.throws(() => m.sync('node_modules', 'subdir/' + uuid.v4())); + t.is(err.name, 'CpFileError', err); + t.is(err.code, 'EISDIR', err); t.throws(() => fs.statSync('subdir'), /ENOENT/); }); @@ -98,144 +98,142 @@ test('preserve timestamps', t => { test('throw an Error if `src` does not exists', t => { const err = t.throws(() => m.sync('NO_ENTRY', t.context.dest)); - t.is(err.name, 'CpFileError', 'wrong Error name: ' + err.stack); - t.is(err.code, 'ENOENT'); - t.regex(err.message, /`NO_ENTRY`/); - t.regex(err.stack, /`NO_ENTRY`/); + t.is(err.name, 'CpFileError', err); + t.is(err.code, 'ENOENT', err); + t.regex(err.message, /`NO_ENTRY`/, err); + t.regex(err.stack, /`NO_ENTRY`/, err); }); test('rethrow mkdirp EACCES errors', t => { - const sut = rewire('../'); - const dirPath = '/root/NO_ACCESS'; - const mkdirError = new Error(`EACCES, permission denied '${dirPath}'`); + const mkdirSync = fs.mkdirSync; + const dirPath = '/root/NO_ACCESS_' + uuid.v4(); + const dest = dirPath + '/' + uuid.v4(); + const mkdirError = buildEACCES(dirPath); let called = 0; - mkdirError.errno = -13; - mkdirError.code = 'EACCES'; - mkdirError.path = dirPath; - - sut.__set__('mkdirp', { - sync: () => { + fs.mkdirSync = (path, mode) => { + if (path === dirPath) { called++; throw mkdirError; } - }); - - const err = t.throws(() => sut.sync('license', dirPath + '/tmp')); - t.is(called, 1); - t.is(err.name, 'CpFileError', 'wrong Error name: ' + err.stack); - t.is(err.errno, mkdirError.errno); - t.is(err.code, mkdirError.code); - t.is(err.path, mkdirError.path); -}); -test('ignore mkdirp EEXIST errors', t => { - const sut = rewire('../'); - const dirPath = '/root/NO_ACCESS'; - const mkdirError = new Error(`EEXIST, mkdir '${dirPath}'`); - let called = 0; - - mkdirError.errno = -17; - mkdirError.code = 'EEXIST'; - mkdirError.path = dirPath; + return mkdirSync(path, mode); + }; - sut.__set__('mkdirp', { - sync: () => { - called++; - throw mkdirError; - } - }); - - sut.sync('license', t.context.dest); + const err = t.throws(() => m.sync('license', dest)); + t.is(err.name, 'CpFileError', err); + t.is(err.errno, mkdirError.errno, err); + t.is(err.code, mkdirError.code, err); + t.is(err.path, mkdirError.path, err); t.is(called, 1); - t.is(fs.readFileSync(t.context.dest, 'utf8'), fs.readFileSync('license', 'utf8')); }); test('rethrow ENOSPC errors', t => { - const sut = rewire('../'); - const noSpaceError = new Error('ENOSPC, write'); - let called = false; - - noSpaceError.errno = -28; - noSpaceError.code = 'ENOSPC'; + const openSync = fs.openSync; + const writeSync = fs.writeSync; + const fds = new Map(); + const noSpaceError = buildENOSPC(); + let called = 0; - sut.__set__('fs', Object.assign({}, fs, { - writeSync: () => { - called = true; + fs.writeFileSync(t.context.src, ''); + fs.openSync = (path, flags, mode) => { + const fd = openSync(path, flags, mode); + fds.set(fd, path); + return fd; + }; + // eslint-disable-next-line max-params + fs.writeSync = (fd, buffer, offset, length, position) => { + if (fds.get(fd) === t.context.dest) { + called++; + // throw Error: throw noSpaceError; } - })); - const err = t.throws(() => sut.sync('license', t.context.dest)); - t.true(called); - t.is(err.name, 'CpFileError', 'wrong Error name: ' + err.stack); - t.is(err.errno, noSpaceError.errno); - t.is(err.code, noSpaceError.code); + return writeSync(fd, buffer, offset, length, position); + }; + + const err = t.throws(() => m.sync('license', t.context.dest)); + t.is(err.name, 'CpFileError', err); + t.is(err.errno, noSpaceError.errno, err); + t.is(err.code, noSpaceError.code, err); + t.is(called, 1); }); test('rethrow stat errors', t => { - const sut = rewire('../'); + const openSync = fs.openSync; + const fstatSync = fs.fstatSync; + const fstatError = buildEBADF(); + const fds = new Map(); let called = 0; - sut.__set__('fs', Object.assign({}, fs, { - fstatSync: () => { + fs.writeFileSync(t.context.src, ''); + fs.openSync = (path, flags, mode) => { + const fd = openSync(path, flags, mode); + fds.set(fd, path); + return fd; + }; + fs.fstatSync = fd => { + if (fds.get(fd) === t.context.src) { called++; - - // throw Error: - return fs.statSync(uuid.v4()); + throw fstatError; } - })); - const err = t.throws(() => sut.sync('license', t.context.dest)); + return fstatSync(fd); + }; + + const err = t.throws(() => m.sync(t.context.src, t.context.dest)); + t.is(err.name, 'CpFileError', err); + t.is(err.errno, fstatError.errno, err); + t.is(err.code, fstatError.code, err); t.is(called, 1); - t.is(err.name, 'CpFileError', 'wrong Error name: ' + err.stack); - t.is(err.code, 'ENOENT'); }); test('rethrow utimes errors', t => { - const sut = rewire('../'); + const openSync = fs.openSync; + const futimesSync = fs.futimesSync; + const futimesError = buildEBADF(); + const fds = new Map(); let called = 0; - sut.__set__('fs', Object.assign({}, fs, { - futimesSync: (path, atime, mtime) => { + fs.openSync = (path, flags, mode) => { + const fd = openSync(path, flags, mode); + fds.set(fd, path); + return fd; + }; + fs.futimesSync = (fd, atime, mtime) => { + if (fds.get(fd) === t.context.dest) { called++; - - // throw Error: - return fs.utimesSync(uuid.v4(), atime, mtime); + throw futimesError; } - })); - const err = t.throws(() => sut.sync('license', t.context.dest)); + return futimesSync(path, atime, mtime); + }; + + const err = t.throws(() => m.sync('license', t.context.dest)); + t.is(err.name, 'CpFileError', err); + t.is(err.errno, futimesError.errno, err); + t.is(err.code, futimesError.code, err); t.is(called, 1); - t.is(err.name, 'CpFileError', 'wrong Error name: ' + err.stack); - t.is(err.code, 'ENOENT'); }); test('rethrow EACCES errors of dest', async t => { - const sut = rewire('../'); - const dirPath = '/root/NO_ACCESS'; - const openError = new Error(`EACCES, permission denied '${dirPath}'`); + const openSync = fs.openSync; + const openError = buildEACCES(t.context.dest); let called = 0; - openError.errno = -13; - openError.code = 'EACCES'; - openError.path = dirPath; - - sut.__set__('fs', Object.assign({}, fs, { - openSync: (path, flags, mode) => { - if (path === t.context.dest) { - called++; - throw openError; - } - - return fs.openSync(path, flags, mode); + fs.openSync = (path, flags, mode) => { + if (path === t.context.dest) { + called++; + throw openError; } - })); - const err = t.throws(() => sut.sync('license', t.context.dest)); + return openSync(path, flags, mode); + }; + + const err = t.throws(() => m.sync('license', t.context.dest)); + t.is(err.name, 'CpFileError', err); + t.is(err.errno, openError.errno, err); + t.is(err.code, openError.code, err); + t.is(err.path, openError.path, err); t.is(called, 1); - t.is(err.name, 'CpFileError', 'wrong Error name: ' + err.stack); - t.is(err.errno, openError.errno); - t.is(err.code, openError.code); - t.is(err.path, openError.path); });