diff --git a/.travis.yml b/.travis.yml index 7831337c..8b179380 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,13 @@ -sudo: false +sudo: required language: node_js node_js: - 'stable' - - '4.1' - - '4.0' + - '4' - '0.12' - '0.10' before_script: - find test -type d -exec chmod g+s {} \; + - sudo chown root test/not-owned/ + - sudo chown root test/not-owned/not-owned.txt after_script: - npm run coveralls diff --git a/README.md b/README.md index 88d1f67a..914e3657 100644 --- a/README.md +++ b/README.md @@ -143,11 +143,16 @@ Any through2-related options are documented in [through2]. Takes a folder path string or a function as the first argument and an options object as the second. If given a function, it will be called with each [vinyl] `File` object and must return a folder path. Returns a stream that accepts [vinyl] `File` objects, writes them to disk at the folder/cwd specified, and passes them downstream so you can keep piping these around. +Once the file is written to disk, an attempt is made to determine if the `stat.mode`, `stat.mtime` and `stat.atime` of the [vinyl] `File` object differ from the file on the filesystem. +If they differ and the running process owns the file, the corresponding filesystem metadata is updated. +If they don't differ or the process doesn't own the file, the attempt is skipped silently. +__This functionality is disabled on Windows operating systems or any other OS that doesn't support `process.getuid` or `process.geteuid` in node. This is due to Windows having very unexpected results through usage of `fs.fchmod` and `fs.futimes`.__ + If the file has a `symlink` attribute specifying a target path, then a symlink will be created. __Note: The file will be modified after being written to this stream.__ - `cwd`, `base`, and `path` will be overwritten to match the folder. - - `stat.mode` will be overwritten if you used a mode parameter. + - `stat` will be updated to match the file on the filesystem. - `contents` will have it's position reset to the beginning if it is a stream. #### Options diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 00000000..ebd6fc07 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,24 @@ +# http://www.appveyor.com/docs/appveyor-yml +# http://www.appveyor.com/docs/lang/nodejs-iojs + +environment: + matrix: + # node.js + - nodejs_version: "0.10" + - nodejs_version: "0.12" + - nodejs_version: "4" + - nodejs_version: "5" + +install: + - ps: Install-Product node $env:nodejs_version + - npm install + +test_script: + - node --version + - npm --version + - cmd: npm test + +build: off + +# build version format +version: "{build}" diff --git a/lib/dest/index.js b/lib/dest/index.js index 1393621c..7f23a839 100644 --- a/lib/dest/index.js +++ b/lib/dest/index.js @@ -32,7 +32,7 @@ function dest(outFolder, opt) { var sourcemapOpt = opt.sourcemaps; if (typeof sourcemapOpt === 'boolean') { - sourcemapOpt = { sourcemaps: sourcemapOpt }; + sourcemapOpt = {}; } var mapStream = sourcemaps.write(sourcemapOpt.path, sourcemapOpt); diff --git a/lib/dest/writeContents/index.js b/lib/dest/writeContents/index.js index 220acb44..1382d956 100644 --- a/lib/dest/writeContents/index.js +++ b/lib/dest/writeContents/index.js @@ -1,24 +1,11 @@ 'use strict'; -var fs = require('graceful-fs'); -var assign = require('object-assign'); - var writeDir = require('./writeDir'); var writeStream = require('./writeStream'); var writeBuffer = require('./writeBuffer'); var writeSymbolicLink = require('./writeSymbolicLink'); -// TODO include sticky/setuid/setgid, i.e. 7777? -var MASK_MODE = parseInt('0777', 8); - - -// http://stackoverflow.com/a/10589791/586382 -function validDate(date) { - return date instanceof Date && !isNaN(date.valueOf()); -} - - -function writeContents(writePath, file, cb) { +function writeContents(writePath, file, callback) { // If directory then mkdirp it if (file.isDirectory()) { return writeDir(writePath, file, written); @@ -41,124 +28,32 @@ function writeContents(writePath, file, cb) { // If no contents then do nothing if (file.isNull()) { - return finish(); + return written(); } - // This is invoked by the various writeXxx modules when they've finished // writing the contents. - // The third argument, if present, should be invoked to indicate we're done - // with the file descriptor. - function written(err, fd, close) { - close = close || function(err, cb) { - cb(err); - }; - - if (err && !(err.code === 'EEXIST' && file.flag === 'wx')) { - return close(err, finish); - } - - // TODO handle symlinks properly - if (!file.stat || file.symlink) { - return close(null, finish); + function written(err) { + if (isErrorFatal(err)) { + return callback(err); } - if (typeof fd === 'number') { - return stat(fd, close); - } - - // No file descriptor given (writeDir or writeSymbolicLink) so create one. - fs.open(writePath, 'r', function(err, fd) { - if (err) { - return close(err, finish); - } - - stat(fd, function(err1) { - fs.close(fd, function(err2) { - finish(err1 || err2); - }); - }); - }); + callback(null, file); } + function isErrorFatal(err) { + if (!err) { + return false; + } - // Set mode and/or atime, mtime. - function stat(fd, close) { - fs.fstat(fd, function(err, stat) { - if (err) { - return close(err, finish); - } - - // Check if mode needs to be updated - var modeDiff = 0; - if (typeof file.stat.mode === 'number') { - modeDiff = (file.stat.mode ^ stat.mode) & MASK_MODE; - } - - // Check if atime/mtime need to be updated - var timesDiff = null; - if (validDate(file.stat.mtime)) { - timesDiff = { - mtime: file.stat.mtime, - atime: validDate(file.stat.atime) ? file.stat.atime : stat.atime, - }; - } - - // Set file.stat to the reflect current state on disk - assign(file.stat, stat); - - // Nothing to do - if (!modeDiff && !timesDiff) { - return close(null, finish); - } - - // Check access, `futimes` and `fchmod` only work if we own the file, - // or if we are effectively root. - var uid = process.geteuid ? process.geteuid() : process.getuid(); - if (stat.uid !== uid && uid !== 0) { - return close(null, finish); - } - - if (modeDiff) { - return mode(); - } - times(); - - function mode() { - var mode = stat.mode ^ modeDiff; - fs.fchmod(fd, mode, function(err) { - if (!err) { - file.stat.mode = mode; - file.stat.ctime.setTime(Date.now()); - } - if (timesDiff) { - return times(err); - } - close(err, finish); - }); - } - - function times(err1) { - fs.futimes(fd, timesDiff.atime, timesDiff.mtime, function(err2) { - if (!err2) { - file.stat.atime = timesDiff.atime; - file.stat.mtime = timesDiff.mtime; - file.stat.ctime.setTime(Date.now()); - } - close(err1 || err2, finish); - }); - } - }); - } - + if (err.code === 'EEXIST' && file.flag === 'wx') { + // Handle scenario for file overwrite failures. + return false; // "These aren't the droids you're looking for" + } - // This is invoked by the close callbacks; we're all done. - function finish(err) { - cb(err, file); + // Otherwise, this is a fatal error + return true; } - - - } module.exports = writeContents; diff --git a/lib/dest/writeContents/writeBuffer.js b/lib/dest/writeContents/writeBuffer.js index d58c666f..f23a4aa0 100644 --- a/lib/dest/writeContents/writeBuffer.js +++ b/lib/dest/writeContents/writeBuffer.js @@ -1,23 +1,26 @@ 'use strict'; -var fs = require('graceful-fs'); +var fo = require('../../fileOperations'); function writeBuffer(writePath, file, written) { - var stat = file.stat; + var opt = { + mode: file.stat.mode, + flag: file.flag, + }; - fs.open(writePath, file.flag, stat.mode, function(err, fd) { - if (err) { - return written(err); + fo.writeFile(writePath, file.contents, opt, onWriteFile); + + function onWriteFile(writeErr, fd) { + if (writeErr) { + return fo.closeFd(writeErr, fd, written); } - fs.write(fd, file.contents, 0, file.contents.length, 0, function(err) { - written(err, fd, function(err1, finish) { - fs.close(fd, function(err2) { - finish(err1 || err2); - }); - }); - }); - }); + fo.updateMetadata(fd, file, onUpdate); + } + + function onUpdate(statErr, fd) { + fo.closeFd(statErr, fd, written); + } } module.exports = writeBuffer; diff --git a/lib/dest/writeContents/writeDir.js b/lib/dest/writeContents/writeDir.js index 81b75de6..ef7622c9 100644 --- a/lib/dest/writeContents/writeDir.js +++ b/lib/dest/writeContents/writeDir.js @@ -1,9 +1,53 @@ 'use strict'; +var fs = require('graceful-fs'); var mkdirp = require('mkdirp'); +var fo = require('../../fileOperations'); + function writeDir(writePath, file, written) { - mkdirp(writePath, file.stat.mode, written); + var mkdirpOpts = { + mode: file.stat.mode, + fs: fs, + }; + mkdirp(writePath, mkdirpOpts, onMkdirp); + + function onMkdirp(mkdirpErr) { + if (mkdirpErr) { + return written(mkdirpErr); + } + + fs.open(writePath, 'r', onOpen); + } + + function onOpen(openErr, fd) { + // If we don't have access, just move along + if (isInaccessible(openErr)) { + return fo.closeFd(null, fd, written); + } + + if (openErr) { + return fo.closeFd(openErr, fd, written); + } + + fo.updateMetadata(fd, file, onUpdate); + } + + function onUpdate(statErr, fd) { + fo.closeFd(statErr, fd, written); + } } +function isInaccessible(err) { + if (!err) { + return false; + } + + if (err.code === 'EACCES') { + return true; + } + + return false; + } + module.exports = writeDir; diff --git a/lib/dest/writeContents/writeStream.js b/lib/dest/writeContents/writeStream.js index 10b3daa5..e9af5f1d 100644 --- a/lib/dest/writeContents/writeStream.js +++ b/lib/dest/writeContents/writeStream.js @@ -2,64 +2,55 @@ var fs = require('graceful-fs'); +var fo = require('../../fileOperations'); var streamFile = require('../../src/getContents/streamFile'); function writeStream(writePath, file, written) { - var stat = file.stat; - var mode = stat.mode; + var opt = { + mode: file.stat.mode, + flag: file.flag, + }; - var outStream; - var outFD; + var outStream = fs.createWriteStream(writePath, opt); - fs.open(writePath, 'w', mode, function(err, fd) { - if (err) { - written(err); - } - - var opt = { - mode: mode, - flag: file.flag, - autoClose: false, - fd: fd, - }; - - outFD = fd; - outStream = fs.createWriteStream(null, opt); + file.contents.once('error', complete); + file.contents.once('end', readStreamEnd); + outStream.once('error', complete); + outStream.once('finish', complete); - file.contents.once('error', complete); - file.contents.once('end', readStreamEnd); - outStream.once('error', complete); - outStream.once('finish', complete); - - // Streams are piped with end disabled, this prevents the - // WriteStream from closing the file descriptor after all - // data is written. - file.contents.pipe(outStream, { end: false }); - }); + // Streams are piped with end disabled, this prevents the + // WriteStream from closing the file descriptor after all + // data is written. + file.contents.pipe(outStream, { end: false }); function readStreamEnd() { - streamFile(file, {}, function(error) { - if (error) { - return complete(error); - } + streamFile(file, complete); + } + + function end(propagatedErr) { + outStream.end(onEnd); - complete(); - }); + function onEnd(endErr) { + written(propagatedErr || endErr); + } } // Cleanup - function complete(err) { + function complete(streamErr) { file.contents.removeListener('error', complete); file.contents.removeListener('end', readStreamEnd); - if (outStream) { - outStream.removeListener('error', complete); - outStream.removeListener('finish', complete); + outStream.removeListener('error', complete); + outStream.removeListener('finish', complete); + + if (streamErr) { + return end(streamErr); } - written(err, outFD, function(err1, finish) { - outStream.end(function(err2) { - finish(err1 || err2); - }); - }); + + if (typeof outStream.fd !== 'number') { + return end(); + } + + fo.updateMetadata(outStream.fd, file, end); } } diff --git a/lib/dest/writeContents/writeSymbolicLink.js b/lib/dest/writeContents/writeSymbolicLink.js index 04a8f750..b0969692 100644 --- a/lib/dest/writeContents/writeSymbolicLink.js +++ b/lib/dest/writeContents/writeSymbolicLink.js @@ -3,13 +3,18 @@ var fs = require('graceful-fs'); function writeSymbolicLink(writePath, file, written) { + // TODO handle symlinks properly fs.symlink(file.symlink, writePath, function(err) { - if (err && err.code !== 'EEXIST') { - return cb(err); + if (isFatalError(err)) { + return written(err); } written(); }); } +function isFatalError(err) { + return (err && err.code !== 'EEXIST'); +} + module.exports = writeSymbolicLink; diff --git a/lib/fileOperations.js b/lib/fileOperations.js new file mode 100644 index 00000000..4c194d4d --- /dev/null +++ b/lib/fileOperations.js @@ -0,0 +1,207 @@ +'use strict'; + +var fs = require('graceful-fs'); +var assign = require('object-assign'); +var isEqual = require('lodash.isequal'); +var isBuffer = require('is-buffer'); +var isValidDate = require('vali-date'); + +// TODO shared module +// TODO include sticky/setuid/setgid, i.e. 7777? +var MASK_MODE = parseInt('0777', 8); +var DEFAULT_FILE_MODE = parseInt('0666', 8); +var APPEND_MODE_REGEXP = /a/; + +function closeFd(propagatedErr, fd, callback) { + if (typeof fd !== 'number') { + return callback(propagatedErr); + } + + fs.close(fd, onClosed); + + function onClosed(closeErr) { + if (propagatedErr || closeErr) { + return callback(propagatedErr || closeErr); + } + + callback(); + } +} + +function getModeDiff(fsMode, vinylMode) { + var modeDiff = 0; + + if (typeof vinylMode === 'number') { + modeDiff = (vinylMode ^ fsMode) & MASK_MODE; + } + + return modeDiff; +} + +function getTimesDiff(fsStat, vinylStat) { + + if (!isValidDate(vinylStat.mtime)) { + return; + } + + if (isEqual(vinylStat.mtime, fsStat.mtime) && + isEqual(vinylStat.atime, fsStat.atime)) { + return; + } + + var atime; + if (isValidDate(vinylStat.atime)) { + atime = vinylStat.atime; + } else { + atime = fsStat.atime; + } + + if (!isValidDate(atime)) { + atime = undefined; + } + + var timesDiff = { + mtime: vinylStat.mtime, + atime: atime, + }; + + return timesDiff; +} + +function isOwner(fsStat) { + var hasGetuid = (typeof process.getuid === 'function'); + var hasGeteuid = (typeof process.geteuid === 'function'); + + // If we don't have either, assume we don't have permissions. + // This should only happen on Windows. + // Windows basically noops fchmod and errors on futimes called on directories. + if (!hasGeteuid && !hasGetuid) { + return false; + } + + var uid; + if (hasGeteuid) { + uid = process.geteuid(); + } else { + uid = process.getuid(); + } + + if (fsStat.uid !== uid && uid !== 0) { + return false; + } + + return true; +} + +function updateMetadata(fd, file, callback) { + + fs.fstat(fd, onStat); + + function onStat(err, stat) { + if (err) { + return callback(err, fd); + } + + // Check if mode needs to be updated + var modeDiff = getModeDiff(stat.mode, file.stat.mode); + + // Check if atime/mtime need to be updated + var timesDiff = getTimesDiff(stat, file.stat); + + // Set file.stat to the reflect current state on disk + assign(file.stat, stat); + + // Nothing to do + if (!modeDiff && !timesDiff) { + return callback(null, fd); + } + + // Check access, `futimes` and `fchmod` only work if we own the file, + // or if we are effectively root. + if (!isOwner(stat)) { + return callback(null, fd); + } + + if (modeDiff) { + return mode(); + } + times(); + + function mode() { + var mode = stat.mode ^ modeDiff; + + fs.fchmod(fd, mode, onFchmod); + + function onFchmod(fchmodErr) { + if (!fchmodErr) { + file.stat.mode = mode; + } + if (timesDiff) { + return times(fchmodErr); + } + callback(fchmodErr, fd); + } + } + + function times(fchmodErr) { + fs.futimes(fd, timesDiff.atime, timesDiff.mtime, onFutimes); + + function onFutimes(futimesErr) { + if (!futimesErr) { + file.stat.atime = timesDiff.atime; + file.stat.mtime = timesDiff.mtime; + } + callback(fchmodErr || futimesErr, fd); + } + } + } +} + +/* + Custom writeFile implementation because we need access to the + file descriptor after the write is complete. + Most of the implementation taken from node core. + */ +function writeFile(path, data, options, callback) { + if (typeof options === 'function') { + callback = options; + options = {}; + } + + if (!isBuffer(data)) { + callback(new TypeError('Data must be a Buffer')); + return; + } + + if (!options) { + options = {}; + } + + // Default the same as node + var mode = options.mode || DEFAULT_FILE_MODE; + var flag = options.flag || 'w'; + var position = APPEND_MODE_REGEXP.test(flag) ? null : 0; + + fs.open(path, flag, mode, onOpen); + + function onOpen(err, fd) { + if (err) { + return onComplete(err); + } + + fs.write(fd, data, 0, data.length, position, onComplete); + + function onComplete(err) { + callback(err, fd); + } + } +} + +module.exports = { + closeFd: closeFd, + getModeDiff: getModeDiff, + getTimesDiff: getTimesDiff, + isOwner: isOwner, + updateMetadata: updateMetadata, + writeFile: writeFile, +}; diff --git a/lib/prepareWrite.js b/lib/prepareWrite.js index 31bcd8a3..50d86cd1 100644 --- a/lib/prepareWrite.js +++ b/lib/prepareWrite.js @@ -54,7 +54,11 @@ function prepareWrite(outFolder, file, opt, cb) { file.path = writePath; // Mkdirp the folder the file is going in - mkdirp(writeFolder, options.dirMode, function(err) { + var mkdirpOpts = { + mode: options.dirMode, + fs: fs, + }; + mkdirp(writeFolder, mkdirpOpts, function(err) { if (err) { return cb(err); } diff --git a/lib/src/getContents/streamFile.js b/lib/src/getContents/streamFile.js index bcc78b0e..43dc1263 100644 --- a/lib/src/getContents/streamFile.js +++ b/lib/src/getContents/streamFile.js @@ -5,6 +5,11 @@ var stripBom = require('strip-bom-stream'); var lazystream = require('lazystream'); function streamFile(file, opt, cb) { + if (typeof opt === 'function') { + cb = opt; + opt = {}; + } + var filePath = file.path; file.contents = new lazystream.Readable(function() { diff --git a/lib/src/index.js b/lib/src/index.js index b8790c6a..a20f7052 100644 --- a/lib/src/index.js +++ b/lib/src/index.js @@ -1,5 +1,6 @@ 'use strict'; +var path = require('path'); var assign = require('object-assign'); var through2 = require('through2'); var gs = require('glob-stream'); @@ -13,6 +14,21 @@ var isValidGlob = require('is-valid-glob'); var getContents = require('./getContents'); var resolveSymlinks = require('./resolveSymlinks'); +function normalizePath(options) { + + function normalize(globFile, enc, cb) { + // TODO: probably move this somewhere + // Ref https://github.com/gulpjs/vinyl/issues/80 + var normalizedFile = assign({}, globFile, { + path: path.normalize(globFile.path), + }); + + cb(null, normalizedFile); + } + + return through2.obj(options, normalize); +} + function createFile(globFile, enc, cb) { cb(null, new File(globFile)); } @@ -36,6 +52,7 @@ function src(glob, opt) { var globStream = gs.create(glob, options); var outputStream = globStream + .pipe(normalizePath(options)) .pipe(resolveSymlinks(options)) .pipe(through2.obj(opt, createFile)); diff --git a/package.json b/package.json index 69fa7665..db0d5082 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,10 @@ "glob-stream": "^5.2.0", "graceful-fs": "^4.0.0", "gulp-sourcemaps": "^1.5.2", + "is-buffer": "^1.1.2", "is-valid-glob": "^0.3.0", "lazystream": "^1.0.0", + "lodash.isequal": "^4.0.0", "merge-stream": "^1.0.0", "mkdirp": "^0.5.0", "object-assign": "^4.0.0", @@ -25,13 +27,16 @@ "strip-bom-stream": "^1.0.0", "through2": "^2.0.0", "through2-filter": "^2.0.0", + "vali-date": "^1.0.0", "vinyl": "^1.0.0" }, "devDependencies": { "buffer-equal": "^0.0.1", + "default-resolution": "^1.0.1", "del": "^2.2.0", "eslint": "^1.10.3", "eslint-config-gulp": "^2.0.0", + "expect": "^1.14.0", "github-changes": "^1.0.1", "istanbul": "^0.3.0", "istanbul-coveralls": "^1.0.1", @@ -46,7 +51,8 @@ "scripts": { "lint": "eslint . && jscs index.js lib/ test/", "test": "npm run lint && mocha", - "coveralls": "istanbul cover _mocha && istanbul-coveralls", + "cover": "istanbul cover _mocha", + "coveralls": "npm run cover && istanbul-coveralls", "changelog": "github-changes -o gulpjs -r vinyl-fs -b master -f ./CHANGELOG.md --order-semver --use-commit-body" }, "engines": { diff --git a/test/dest.js b/test/dest.js index e90e3508..2aaa9df2 100644 --- a/test/dest.js +++ b/test/dest.js @@ -8,10 +8,12 @@ var fstatSpy = spies.fstatSpy; var vfs = require('../'); +var os = require('os'); var path = require('path'); var fs = require('graceful-fs'); var del = require('del'); var Writeable = require('readable-stream/writable'); +var expect = require('expect'); var bufEqual = require('buffer-equal'); var through = require('through2'); @@ -27,8 +29,13 @@ var wipeOut = function() { chmodSpy.reset(); fchmodSpy.reset(); futimesSpy.reset(); - del.sync(path.join(__dirname, './fixtures/highwatermark')); - del.sync(path.join(__dirname, './out-fixtures/')); + expect.restoreSpies(); + + // Async del to get sort-of-fix for https://github.com/isaacs/rimraf/issues/72 + return del(path.join(__dirname, './fixtures/highwatermark')) + .then(function() { + return del(path.join(__dirname, './out-fixtures/')); + }); }; var dataWrap = function(fn) { @@ -48,10 +55,10 @@ describe('dest stream', function() { beforeEach(wipeOut); afterEach(wipeOut); - it('should explode on invalid folder (empty)', function(done) { + it.skip('should explode on invalid folder (empty)', function(done) { var stream; try { - stream = gulp.dest(); + stream = vfs.dest(); } catch (err) { should.exist(err); should.not.exist(stream); @@ -59,10 +66,10 @@ describe('dest stream', function() { } }); - it('should explode on invalid folder (empty string)', function(done) { + it.skip('should explode on invalid folder (empty string)', function(done) { var stream; try { - stream = gulp.dest(''); + stream = vfs.dest(''); } catch (err) { should.exist(err); should.not.exist(stream); @@ -96,6 +103,38 @@ describe('dest stream', function() { stream.end(); }); + it('should not explode if sourcemap option is an object', function(done) { + var inputPath = path.join(__dirname, './fixtures/test.coffee'); + + var options = { + sourcemaps: { + addComment: false, + }, + }; + + var expectedFile = new File({ + base: __dirname, + cwd: __dirname, + path: inputPath, + contents: null, + }); + + var buffered = []; + + var onEnd = function() { + buffered.length.should.equal(1); + buffered[0].should.equal(expectedFile); + done(); + }; + + var bufferStream = through.obj(dataWrap(buffered.push.bind(buffered)), onEnd); + + var stream = vfs.dest(path.join(__dirname, './out-fixtures/'), options); + stream.pipe(bufferStream); + stream.write(expectedFile); + stream.end(); + }); + it('should pass through writes with cwd', function(done) { var inputPath = path.join(__dirname, './fixtures/test.coffee'); @@ -265,16 +304,12 @@ describe('dest stream', function() { var expectedContents = fs.readFileSync(inputPath); var expectedCwd = __dirname; var expectedBase = path.join(__dirname, './out-fixtures'); - var expectedMode = parseInt('655', 8); var expectedFile = new File({ base: inputBase, cwd: __dirname, path: inputPath, contents: expectedContents, - stat: { - mode: expectedMode, - }, }); var buffered = []; @@ -287,7 +322,6 @@ describe('dest stream', function() { buffered[0].path.should.equal(expectedPath, 'path should have changed'); fs.existsSync(expectedPath).should.equal(true); bufEqual(fs.readFileSync(expectedPath), expectedContents).should.equal(true); - realMode(fs.lstatSync(expectedPath).mode).should.equal(expectedMode); done(); }; @@ -306,7 +340,6 @@ describe('dest stream', function() { var expectedContents = fs.readFileSync(inputPath); var expectedCwd = __dirname; var expectedBase = path.join(__dirname, './out-fixtures'); - var expectedMode = parseInt('655', 8); var contentStream = through.obj(); var expectedFile = new File({ @@ -314,9 +347,6 @@ describe('dest stream', function() { cwd: __dirname, path: inputPath, contents: contentStream, - stat: { - mode: expectedMode, - }, }); var buffered = []; @@ -329,13 +359,13 @@ describe('dest stream', function() { buffered[0].path.should.equal(expectedPath, 'path should have changed'); fs.existsSync(expectedPath).should.equal(true); bufEqual(fs.readFileSync(expectedPath), expectedContents).should.equal(true); - realMode(fs.lstatSync(expectedPath).mode).should.equal(expectedMode); done(); }; var stream = vfs.dest('./out-fixtures/', { cwd: __dirname }); - var bufferStream = through.obj(dataWrap(buffered.push.bind(buffered)), onEnd); + var bufferStream = through.obj(dataWrap(buffered.push.bind(buffered))); + bufferStream.on('finish', onEnd); stream.pipe(bufferStream); stream.write(expectedFile); setTimeout(function() { @@ -351,7 +381,6 @@ describe('dest stream', function() { var expectedPath = path.join(__dirname, './out-fixtures/test'); var expectedCwd = __dirname; var expectedBase = path.join(__dirname, './out-fixtures'); - var expectedMode = parseInt('655', 8); var expectedFile = new File({ base: inputBase, @@ -362,7 +391,6 @@ describe('dest stream', function() { isDirectory: function() { return true; }, - mode: expectedMode, }, }); @@ -376,7 +404,6 @@ describe('dest stream', function() { buffered[0].path.should.equal(expectedPath, 'path should have changed'); fs.existsSync(expectedPath).should.equal(true); fs.lstatSync(expectedPath).isDirectory().should.equal(true); - realMode(fs.lstatSync(expectedPath).mode).should.equal(expectedMode); done(); }; @@ -394,399 +421,50 @@ describe('dest stream', function() { var inputBase = path.join(__dirname, './out-fixtures/'); var srcPath = path.join(__dirname, './fixtures/test.coffee'); var stream1 = vfs.dest('./out-fixtures/', { cwd: __dirname }); - var stream2 = vfs.dest('./out-fixtures/', { cwd: __dirname }); - var content = fs.readFileSync(srcPath); - var rename = through.obj(function(file, _, next) { - file.path = inputPath2; - this.push(file); - next(); - }); - - stream1.on('data', function(file) { - file.path.should.equal(inputPath1); - }); - - stream1.pipe(rename).pipe(stream2); - stream2.on('data', function(file) { - file.path.should.equal(inputPath2); - }).once('end', function() { - fs.readFileSync(inputPath1, 'utf8').should.equal(content.toString()); - fs.readFileSync(inputPath2, 'utf8').should.equal(content.toString()); - done(); - }); - - var file = new File({ - base: inputBase, - path: inputPath1, - cwd: __dirname, - contents: content, - }); - - stream1.write(file); - stream1.end(); - }); - - it('should write new files with the default user mode', function(done) { - var inputPath = path.join(__dirname, './fixtures/test.coffee'); - var inputBase = path.join(__dirname, './fixtures/'); - var expectedPath = path.join(__dirname, './out-fixtures/test.coffee'); - var expectedContents = fs.readFileSync(inputPath); - var expectedMode = parseInt('666', 8) & (~process.umask()); - - var expectedFile = new File({ - base: inputBase, - cwd: __dirname, - path: inputPath, - contents: expectedContents, - }); - - var buffered = []; - - var onEnd = function() { - buffered.length.should.equal(1); - buffered[0].should.equal(expectedFile); - fs.existsSync(expectedPath).should.equal(true); - realMode(fs.lstatSync(expectedPath).mode).should.equal(expectedMode); - done(); - }; - - chmodSpy.reset(); - var stream = vfs.dest('./out-fixtures/', { cwd: __dirname }); - - var bufferStream = through.obj(dataWrap(buffered.push.bind(buffered)), onEnd); - - stream.pipe(bufferStream); - stream.write(expectedFile); - stream.end(); - }); - - it('should write new files with the specified mode', function(done) { - var inputPath = path.join(__dirname, './fixtures/test.coffee'); - var inputBase = path.join(__dirname, './fixtures/'); - var expectedPath = path.join(__dirname, './out-fixtures/test.coffee'); - var expectedContents = fs.readFileSync(inputPath); - var expectedMode = parseInt('744', 8); - - var expectedFile = new File({ - base: inputBase, - cwd: __dirname, - path: inputPath, - contents: expectedContents, - }); - - var buffered = []; - - var onEnd = function() { - buffered.length.should.equal(1); - buffered[0].should.equal(expectedFile); - fs.existsSync(expectedPath).should.equal(true); - realMode(fs.lstatSync(expectedPath).mode).should.equal(expectedMode); - done(); - }; - - chmodSpy.reset(); - var stream = vfs.dest('./out-fixtures/', { cwd: __dirname, mode: expectedMode }); - - var bufferStream = through.obj(dataWrap(buffered.push.bind(buffered)), onEnd); - - stream.pipe(bufferStream); - stream.write(expectedFile); - stream.end(); - }); - - it('should update file mode to match the vinyl mode', function(done) { - var inputPath = path.join(__dirname, './fixtures/test.coffee'); - var inputBase = path.join(__dirname, './fixtures/'); - var expectedPath = path.join(__dirname, './out-fixtures/test.coffee'); - var expectedContents = fs.readFileSync(inputPath); - var expectedBase = path.join(__dirname, './out-fixtures'); - var startMode = parseInt('0655', 8); - var expectedMode = parseInt('0722', 8); - - var expectedFile = new File({ - base: inputBase, - cwd: __dirname, - path: inputPath, - contents: expectedContents, - stat: { - mode: expectedMode, - }, - }); - - var buffered = []; - - var onEnd = function() { - should(chmodSpy.called).be.ok; - buffered.length.should.equal(1); - buffered[0].should.equal(expectedFile); - fs.existsSync(expectedPath).should.equal(true); - realMode(fs.lstatSync(expectedPath).mode).should.equal(expectedMode); - done(); - }; - - fs.mkdirSync(expectedBase); - fs.closeSync(fs.openSync(expectedPath, 'w')); - fs.chmodSync(expectedPath, startMode); - - chmodSpy.reset(); - var stream = vfs.dest('./out-fixtures/', { cwd: __dirname }); - - var bufferStream = through.obj(dataWrap(buffered.push.bind(buffered)), onEnd); - - stream.pipe(bufferStream); - stream.write(expectedFile); - stream.end(); - }); - - it('should update directory mode to match the vinyl mode', function(done) { - var inputBase = path.join(__dirname, './fixtures/'); - var inputPath = path.join(__dirname, './fixtures/wow'); - var expectedPath = path.join(__dirname, './out-fixtures/wow'); - var expectedCwd = __dirname; - var expectedBase = path.join(__dirname, './out-fixtures'); - - var firstFile = new File({ - base: inputBase, - cwd: __dirname, - path: expectedPath, - stat: fs.statSync(inputPath), - }); - var startMode = firstFile.stat.mode; - var expectedMode = parseInt('727', 8); - - var expectedFile = new File(firstFile); - expectedFile.stat.mode = (startMode & ~parseInt('7777', 8)) | expectedMode; - - var buffered = []; - - var onEnd = function() { - buffered.length.should.equal(2); - buffered[0].should.equal(firstFile); - buffered[1].should.equal(expectedFile); - buffered[0].cwd.should.equal(expectedCwd, 'cwd should have changed'); - buffered[0].base.should.equal(expectedBase, 'base should have changed'); - buffered[0].path.should.equal(expectedPath, 'path should have changed'); - realMode(fs.lstatSync(expectedPath).mode).should.equal(expectedMode); - done(); - }; - - fs.mkdirSync(expectedBase); - - var stream = vfs.dest('./out-fixtures/', { cwd: __dirname }); - - var bufferStream = through.obj(dataWrap(buffered.push.bind(buffered)), onEnd); - - stream.pipe(bufferStream); - stream.write(firstFile); - stream.write(expectedFile); - stream.end(); - }); - - it('should use different modes for files and directories', function(done) { - var inputBase = path.join(__dirname, './fixtures'); - var inputPath = path.join(__dirname, './fixtures/wow/suchempty'); - var expectedBase = path.join(__dirname, './out-fixtures/wow'); - var expectedDirMode = parseInt('755', 8); - var expectedFileMode = parseInt('655', 8); - - var firstFile = new File({ - base: inputBase, - cwd: __dirname, - path: inputPath, - stat: fs.statSync(inputPath), - }); - - var buffered = []; - - var onEnd = function() { - realMode(fs.lstatSync(expectedBase).mode).should.equal(expectedDirMode); - realMode(buffered[0].stat.mode).should.equal(expectedFileMode); - done(); - }; - - var stream = vfs.dest('./out-fixtures/', { - cwd: __dirname, - mode: expectedFileMode, - dirMode: expectedDirMode, - }); - - var bufferStream = through.obj(dataWrap(buffered.push.bind(buffered)), onEnd); - - stream.pipe(bufferStream); - stream.write(firstFile); - stream.end(); - }); - - it('should not call futimes when no mtime is provided on the vinyl stat', function(done) { - var inputPath = path.join(__dirname, './fixtures/test.coffee'); - var inputBase = path.join(__dirname, './fixtures/'); - var expectedPath = path.join(__dirname, './out-fixtures/test.coffee'); - var expectedContents = fs.readFileSync(inputPath); - var expectedAtime = new Date(); - var expectedMtime = new Date(); - - var expectedFile = new File({ - base: inputBase, - cwd: __dirname, - path: inputPath, - contents: expectedContents, - stat: {}, - }); - - var buffered = []; - - var onEnd = function() { - should(futimesSpy.called).be.not.ok; - buffered.length.should.equal(1); - buffered[0].should.equal(expectedFile); - fs.existsSync(expectedPath).should.equal(true); - fs.lstatSync(expectedPath).atime.setMilliseconds(0).should.equal(expectedAtime.setMilliseconds(0)); - fs.lstatSync(expectedPath).mtime.setMilliseconds(0).should.equal(expectedMtime.setMilliseconds(0)); - done(); - }; - - var stream = vfs.dest('./out-fixtures/', { cwd: __dirname }); - - var bufferStream = through.obj(dataWrap(buffered.push.bind(buffered)), onEnd); - - stream.pipe(bufferStream); - stream.write(expectedFile); - stream.end(); - }); - - it('should call futimes when an mtime is provided on the vinyl stat', function(done) { - var inputPath = path.join(__dirname, './fixtures/test.coffee'); - var inputBase = path.join(__dirname, './fixtures/'); - var expectedPath = path.join(__dirname, './out-fixtures/test.coffee'); - var expectedContents = fs.readFileSync(inputPath); - var expectedMtime = fs.lstatSync(inputPath).mtime; - - var expectedFile = new File({ - base: inputBase, - cwd: __dirname, - path: inputPath, - contents: expectedContents, - stat: { - mtime: expectedMtime, - }, - }); - - var buffered = []; - - var onEnd = function() { - should(futimesSpy.called).be.ok; - buffered.length.should.equal(1); - buffered[0].should.equal(expectedFile); - fs.existsSync(expectedPath).should.equal(true); - fs.lstatSync(expectedPath).mtime.getTime().should.equal(expectedMtime.getTime()); - expectedFile.stat.should.have.property('mtime'); - expectedFile.stat.mtime.should.equal(expectedMtime); - done(); - }; - - var stream = vfs.dest('./out-fixtures/', { cwd: __dirname }); - - var bufferStream = through.obj(dataWrap(buffered.push.bind(buffered)), onEnd); - - stream.pipe(bufferStream); - stream.write(expectedFile); - stream.end(); - }); - - it('should not call futimes when provided mtime on the vinyl stat is invalid', function(done) { - var inputPath = path.join(__dirname, './fixtures/test.coffee'); - var inputBase = path.join(__dirname, './fixtures/'); - var expectedPath = path.join(__dirname, './out-fixtures/test.coffee'); - var expectedContents = fs.readFileSync(inputPath); - var expectedMtime = new Date(); - var invalidMtime = new Date(undefined); - - var expectedFile = new File({ - base: inputBase, - cwd: __dirname, - path: inputPath, - contents: expectedContents, - stat: { - mtime: invalidMtime, - }, - }); - - var buffered = []; - - var onEnd = function() { - should(futimesSpy.called).be.not.ok; - buffered.length.should.equal(1); - buffered[0].should.equal(expectedFile); - fs.existsSync(expectedPath).should.equal(true); - fs.lstatSync(expectedPath).mtime.setMilliseconds(0).should.equal(expectedMtime.setMilliseconds(0)); - done(); - }; - - var stream = vfs.dest('./out-fixtures/', { cwd: __dirname }); - - var bufferStream = through.obj(dataWrap(buffered.push.bind(buffered)), onEnd); - - stream.pipe(bufferStream); - stream.write(expectedFile); - stream.end(); - }); - - it('should call futimes when provided mtime on the vinyl stat is valid but provided atime is invalid', function(done) { - var inputPath = path.join(__dirname, './fixtures/test.coffee'); - var inputBase = path.join(__dirname, './fixtures/'); - var expectedPath = path.join(__dirname, './out-fixtures/test.coffee'); - var expectedContents = fs.readFileSync(inputPath); - var expectedMtime = fs.lstatSync(inputPath).mtime; - var invalidAtime = new Date(undefined); - - var expectedFile = new File({ - base: inputBase, - cwd: __dirname, - path: inputPath, - contents: expectedContents, - stat: { - atime: invalidAtime, - mtime: expectedMtime, - }, + var stream2 = vfs.dest('./out-fixtures/', { cwd: __dirname }); + var content = fs.readFileSync(srcPath); + var rename = through.obj(function(file, _, next) { + file.path = inputPath2; + this.push(file); + next(); }); - var buffered = []; + stream1.on('data', function(file) { + file.path.should.equal(inputPath1); + }); - var onEnd = function() { - should(futimesSpy.called).be.ok; - buffered.length.should.equal(1); - buffered[0].should.equal(expectedFile); - fs.existsSync(expectedPath).should.equal(true); - fs.lstatSync(expectedPath).mtime.getTime().should.equal(expectedMtime.getTime()); + stream1.pipe(rename).pipe(stream2); + stream2.on('data', function(file) { + file.path.should.equal(inputPath2); + }).once('end', function() { + fs.readFileSync(inputPath1, 'utf8').should.equal(content.toString()); + fs.readFileSync(inputPath2, 'utf8').should.equal(content.toString()); done(); - }; - - var stream = vfs.dest('./out-fixtures/', { cwd: __dirname }); + }); - var bufferStream = through.obj(dataWrap(buffered.push.bind(buffered)), onEnd); + var file = new File({ + base: inputBase, + path: inputPath1, + cwd: __dirname, + contents: content, + }); - stream.pipe(bufferStream); - stream.write(expectedFile); - stream.end(); + stream1.write(file); + stream1.end(); }); - it('should write file atime and mtime using the vinyl stat', function(done) { + it('should write new files with the default user mode', function(done) { var inputPath = path.join(__dirname, './fixtures/test.coffee'); var inputBase = path.join(__dirname, './fixtures/'); var expectedPath = path.join(__dirname, './out-fixtures/test.coffee'); var expectedContents = fs.readFileSync(inputPath); - var expectedAtime = fs.lstatSync(inputPath).atime; - var expectedMtime = fs.lstatSync(inputPath).mtime; + var expectedMode = parseInt('666', 8) & (~process.umask()); var expectedFile = new File({ base: inputBase, cwd: __dirname, path: inputPath, contents: expectedContents, - stat: { - atime: expectedAtime, - mtime: expectedMtime, - }, }); var buffered = []; @@ -795,15 +473,11 @@ describe('dest stream', function() { buffered.length.should.equal(1); buffered[0].should.equal(expectedFile); fs.existsSync(expectedPath).should.equal(true); - fs.lstatSync(expectedPath).atime.getTime().should.equal(expectedAtime.getTime()); - fs.lstatSync(expectedPath).mtime.getTime().should.equal(expectedMtime.getTime()); - expectedFile.stat.should.have.property('mtime'); - expectedFile.stat.mtime.should.equal(expectedMtime); - expectedFile.stat.should.have.property('atime'); - expectedFile.stat.atime.should.equal(expectedAtime); + realMode(fs.lstatSync(expectedPath).mode).should.equal(expectedMode); done(); }; + chmodSpy.reset(); var stream = vfs.dest('./out-fixtures/', { cwd: __dirname }); var bufferStream = through.obj(dataWrap(buffered.push.bind(buffered)), onEnd); @@ -881,16 +555,12 @@ describe('dest stream', function() { var expectedPath = path.join(__dirname, './out-fixtures/test.coffee'); var expectedContents = fs.readFileSync(inputPath); var expectedBase = path.join(__dirname, './out-fixtures'); - var expectedMode = parseInt('722', 8); var expectedFile = new File({ base: inputBase, cwd: __dirname, path: inputPath, contents: expectedContents, - stat: { - mode: expectedMode, - }, }); fs.mkdirSync(expectedBase); @@ -899,7 +569,7 @@ describe('dest stream', function() { var stream = vfs.dest('./out-fixtures/', { cwd: __dirname }); stream.on('error', function(err) { - err.code.should.equal('EACCES'); + expect(err).toExist(); done(); }); stream.write(expectedFile); @@ -940,137 +610,6 @@ describe('dest stream', function() { stream.write(expectedFile); }); - it('should report chmod errors', function(done) { - var inputPath = path.join(__dirname, './fixtures/test.coffee'); - var inputBase = path.join(__dirname, './fixtures/'); - var expectedPath = path.join(__dirname, './out-fixtures/test.coffee'); - var expectedContents = fs.readFileSync(inputPath); - var expectedBase = path.join(__dirname, './out-fixtures'); - var expectedMode = parseInt('722', 8); - - var expectedFile = new File({ - base: inputBase, - cwd: __dirname, - path: inputPath, - contents: expectedContents, - stat: { - mode: expectedMode, - }, - }); - - fs.mkdirSync(expectedBase); - fs.closeSync(fs.openSync(expectedPath, 'w')); - - spies.setError(function(mod, fn) { - if (fn === 'fchmod' && typeof arguments[2] === 'number') { - return new Error('chmod error'); - } - }); - - var stream = vfs.dest('./out-fixtures/', { cwd: __dirname }); - stream.on('error', function(err) { - err.message.should.equal('chmod error'); - done(); - }); - stream.write(expectedFile); - }); - - it('should not chmod a matching file', function(done) { - var inputPath = path.join(__dirname, './fixtures/test.coffee'); - var inputBase = path.join(__dirname, './fixtures/'); - var expectedPath = path.join(__dirname, './out-fixtures/test.coffee'); - var expectedContents = fs.readFileSync(inputPath); - var expectedBase = path.join(__dirname, './out-fixtures'); - var expectedMode = parseInt('722', 8); - - var expectedFile = new File({ - base: inputBase, - cwd: __dirname, - path: inputPath, - contents: expectedContents, - stat: { - mode: expectedMode, - }, - }); - - var expectedCount = 0; - spies.setError(function(mod, fn) { - if (fn === 'fstat' && typeof arguments[2] === 'number') { - expectedCount++; - } - }); - - var onEnd = function() { - expectedCount.should.equal(1); - should(fchmodSpy.called).be.not.ok; - realMode(fs.lstatSync(expectedPath).mode).should.equal(expectedMode); - done(); - }; - - fs.mkdirSync(expectedBase); - fs.closeSync(fs.openSync(expectedPath, 'w')); - fs.chmodSync(expectedPath, expectedMode); - - fstatSpy.reset(); - fchmodSpy.reset(); - var stream = vfs.dest('./out-fixtures/', { cwd: __dirname }); - - var buffered = []; - var bufferStream = through.obj(dataWrap(buffered.push.bind(buffered)), onEnd); - - stream.pipe(bufferStream); - stream.write(expectedFile); - stream.end(); - }); - - it('should see a file with special chmod (setuid/setgid/sticky) as matching', function(done) { - var inputPath = path.join(__dirname, './fixtures/test.coffee'); - var inputBase = path.join(__dirname, './fixtures/'); - var expectedPath = path.join(__dirname, './out-fixtures/test.coffee'); - var expectedContents = fs.readFileSync(inputPath); - var expectedBase = path.join(__dirname, './out-fixtures'); - var expectedMode = parseInt('3722', 8); - var normalMode = parseInt('722', 8); - - var expectedFile = new File({ - base: inputBase, - cwd: __dirname, - path: inputPath, - contents: expectedContents, - stat: { - mode: normalMode, - }, - }); - - var expectedCount = 0; - spies.setError(function(mod, fn) { - if (fn === 'fstat' && typeof arguments[2] === 'number') { - expectedCount++; - } - }); - - var onEnd = function() { - expectedCount.should.equal(1); - should(fchmodSpy.called).be.not.ok; - done(); - }; - - fs.mkdirSync(expectedBase); - fs.closeSync(fs.openSync(expectedPath, 'w')); - fs.chmodSync(expectedPath, expectedMode); - - fstatSpy.reset(); - fchmodSpy.reset(); - var stream = vfs.dest('./out-fixtures/', { cwd: __dirname }); - - var buffered = []; - var bufferStream = through.obj(dataWrap(buffered.push.bind(buffered)), onEnd); - - stream.pipe(bufferStream); - stream.write(expectedFile); - stream.end(); - }); - it('should not overwrite files with overwrite option set to false', function(done) { var inputPath = path.join(__dirname, './fixtures/test.coffee'); var inputBase = path.join(__dirname, './fixtures/'); @@ -1420,6 +959,9 @@ describe('dest stream', function() { }); it('sinks the stream if all the data event handlers are removed', function(done) { + + this.timeout(10000); + fs.mkdirSync(path.join(__dirname, './fixtures/highwatermark')); for (var idx = 0; idx < 17; idx++) { fs.writeFileSync(path.join(__dirname, './fixtures/highwatermark/', 'file' + idx + '.txt')); @@ -1488,14 +1030,13 @@ describe('dest stream', function() { // This can be a very slow test on boxes with slow disk i/o this.timeout(0); - // Make a ton of hard links + // Make a ton of files. Changed from hard links due to Windows failures var numFiles = 6000; - var srcFile = path.join(__dirname, './fixtures/test.coffee'); fs.mkdirSync(path.join(__dirname, './out-fixtures')); fs.mkdirSync(path.join(__dirname, './out-fixtures/in/')); for (var idx = 0; idx < numFiles; idx++) { - fs.linkSync(srcFile, path.join(__dirname, './out-fixtures/in/test' + idx + '.coffee')); + fs.writeFileSync(path.join(__dirname, './out-fixtures/in/test' + idx + '.coffee'), ''); } var srcStream = vfs.src(path.join(__dirname, './out-fixtures/in/*.coffee'), { buffer: false }); @@ -1516,4 +1057,147 @@ describe('dest stream', function() { }); }); + it('errors if we cannot mkdirp', function(done) { + var mkdirSpy = expect.spyOn(fs, 'mkdir').andCall(function() { + var callback = arguments[arguments.length - 1]; + callback(new Error('mocked error')); + }); + + var outputDir = path.join(__dirname, './out-fixtures/'); + var inputPath = path.join(__dirname, './fixtures/test.coffee'); + + var expectedFile = new File({ + base: __dirname, + cwd: __dirname, + path: inputPath, + contents: null, + }); + + var stream = vfs.dest(outputDir); + stream.write(expectedFile); + stream.on('error', function(err) { + expect(err).toExist(); + expect(mkdirSpy.calls.length).toEqual(1); + done(); + }); + }); + + it('errors if vinyl object is a directory and we cannot mkdirp', function(done) { + var ogMkdir = fs.mkdir; + + var mkdirSpy = expect.spyOn(fs, 'mkdir').andCall(function() { + if (mkdirSpy.calls.length > 1) { + var callback = arguments[arguments.length - 1]; + callback(new Error('mocked error')); + } else { + ogMkdir.apply(null, arguments); + } + }); + + var outputDir = path.join(__dirname, './out-fixtures/'); + var inputPath = path.join(__dirname, './other-dir/'); + + var expectedFile = new File({ + base: __dirname, + cwd: __dirname, + path: inputPath, + contents: null, + stat: { + isDirectory: function() { + return true; + }, + }, + }); + + var stream = vfs.dest(outputDir); + stream.write(expectedFile); + stream.on('error', function(err) { + expect(err).toExist(); + expect(mkdirSpy.calls.length).toEqual(2); + done(); + }); + }); + + // TODO: is this correct behavior? had to adjust it + it('does not error if vinyl object is a directory and we cannot open it', function(done) { + var outputDir = path.join(__dirname, './out-fixtures/'); + var inputPath = path.join(__dirname, './other-dir/'); + + var expectedFile = new File({ + base: __dirname, + cwd: __dirname, + path: inputPath, + contents: null, + stat: { + isDirectory: function() { + return true; + }, + mode: parseInt('000', 8), + }, + }); + + var stream = vfs.dest(outputDir); + stream.write(expectedFile); + stream.on('error', function(err) { + expect(err).toNotExist(); + done(err); + }); + stream.end(function() { + var exists = fs.existsSync(path.join(outputDir, './other-dir/')); + expect(exists).toEqual(true); + done(); + }); + }); + + it('errors if vinyl object is a directory and open errors', function(done) { + var openSpy = expect.spyOn(fs, 'open').andCall(function(writePath, flag, cb) { + cb(new Error('mocked error')); + }); + + var outputDir = path.join(__dirname, './out-fixtures/'); + var inputPath = path.join(__dirname, './other-dir/'); + + var expectedFile = new File({ + base: __dirname, + cwd: __dirname, + path: inputPath, + contents: null, + stat: { + isDirectory: function() { + return true; + }, + }, + }); + + var stream = vfs.dest(outputDir); + stream.write(expectedFile); + stream.on('error', function(err) { + expect(err).toExist(); + expect(openSpy.calls.length).toEqual(1); + done(); + }); + }); + + it('error if content stream errors', function(done) { + var inputPath = path.join(__dirname, './fixtures/test.coffee'); + var inputBase = path.join(__dirname, './fixtures/'); + + var contentStream = through.obj(); + var expectedFile = new File({ + base: inputBase, + cwd: __dirname, + path: inputPath, + contents: contentStream, + }); + + var stream = vfs.dest('./out-fixtures/', { cwd: __dirname }); + stream.write(expectedFile); + setTimeout(function() { + contentStream.emit('error', new Error('mocked error')); + }, 100); + stream.on('error', function(err) { + expect(err).toExist(); + done(); + }); + }); }); diff --git a/test/destModes.js b/test/destModes.js new file mode 100644 index 00000000..84a05256 --- /dev/null +++ b/test/destModes.js @@ -0,0 +1,405 @@ +'use strict'; + +var os = require('os'); +var path = require('path'); + +var fs = require('graceful-fs'); +var del = require('del'); +var File = require('vinyl'); +var expect = require('expect'); +var through = require('through2'); + +var vfs = require('../'); + +function wipeOut() { + this.timeout(20000); + + expect.restoreSpies(); + + // Async del to get sort-of-fix for https://github.com/isaacs/rimraf/issues/72 + return del(path.join(__dirname, './fixtures/highwatermark')) + .then(function() { + return del(path.join(__dirname, './out-fixtures/')); + }); +} + +var MASK_MODE = parseInt('777', 8); + +function masked(mode) { + return mode & MASK_MODE; +} + +var isWindows = (os.platform() === 'win32'); + +describe('.dest() with custom modes', function() { + beforeEach(wipeOut); + afterEach(wipeOut); + + it('should set the mode of a written buffer file if set on the vinyl object', function(done) { + if (isWindows) { + console.log('Changing the mode of a file is not supported by node.js in Windows.'); + console.log('Windows is treated as though it does not have permission to make this operation.'); + this.skip(); + return; + } + + var inputPath = path.join(__dirname, './fixtures/test.coffee'); + var inputBase = path.join(__dirname, './fixtures/'); + var expectedPath = path.join(__dirname, './out-fixtures/test.coffee'); + var expectedContents = fs.readFileSync(inputPath); + var expectedMode = parseInt('655', 8); + + var expectedFile = new File({ + base: inputBase, + cwd: __dirname, + path: inputPath, + contents: expectedContents, + stat: { + mode: expectedMode, + }, + }); + + var onEnd = function() { + expect(masked(fs.lstatSync(expectedPath).mode)).toEqual(expectedMode); + done(); + }; + + var stream = vfs.dest('./out-fixtures/', { cwd: __dirname }); + stream.on('end', onEnd); + stream.write(expectedFile); + stream.end(); + }); + + it('should set the mode of a written stream file if set on the vinyl object', function(done) { + if (isWindows) { + this.skip(); + return; + } + + var inputPath = path.join(__dirname, './fixtures/test.coffee'); + var inputBase = path.join(__dirname, './fixtures/'); + var expectedPath = path.join(__dirname, './out-fixtures/test.coffee'); + var expectedContents = fs.readFileSync(inputPath); + var expectedMode = parseInt('655', 8); + + var contentStream = through.obj(); + var expectedFile = new File({ + base: inputBase, + cwd: __dirname, + path: inputPath, + contents: contentStream, + stat: { + mode: expectedMode, + }, + }); + + var onEnd = function() { + expect(masked(fs.lstatSync(expectedPath).mode)).toEqual(expectedMode); + done(); + }; + + var stream = vfs.dest('./out-fixtures/', { cwd: __dirname }); + stream.on('end', onEnd); + stream.write(expectedFile); + setTimeout(function() { + contentStream.write(expectedContents); + contentStream.end(); + }, 100); + stream.end(); + }); + + it('should set the mode of a written directory if set on the vinyl object', function(done) { + if (isWindows) { + this.skip(); + return; + } + + var inputPath = path.join(__dirname, './fixtures/test'); + var inputBase = path.join(__dirname, './fixtures/'); + var expectedPath = path.join(__dirname, './out-fixtures/test'); + var expectedMode = parseInt('655', 8); + + var expectedFile = new File({ + base: inputBase, + cwd: __dirname, + path: inputPath, + contents: null, + stat: { + isDirectory: function() { + return true; + }, + mode: expectedMode, + }, + }); + + var onEnd = function() { + expect(masked(fs.lstatSync(expectedPath).mode)).toEqual(expectedMode); + done(); + }; + + var stream = vfs.dest('./out-fixtures/', { cwd: __dirname }); + stream.on('end', onEnd); + stream.write(expectedFile); + stream.end(); + }); + + it('should write new files with the mode specified in options', function(done) { + if (isWindows) { + this.skip(); + return; + } + + var inputPath = path.join(__dirname, './fixtures/test.coffee'); + var inputBase = path.join(__dirname, './fixtures/'); + var expectedPath = path.join(__dirname, './out-fixtures/test.coffee'); + var expectedContents = fs.readFileSync(inputPath); + var expectedMode = parseInt('744', 8); + + var expectedFile = new File({ + base: inputBase, + cwd: __dirname, + path: inputPath, + contents: expectedContents, + }); + + var onEnd = function() { + expect(masked(fs.lstatSync(expectedPath).mode)).toEqual(expectedMode); + done(); + }; + + var stream = vfs.dest('./out-fixtures/', { cwd: __dirname, mode: expectedMode }); + stream.on('end', onEnd); + stream.write(expectedFile); + stream.end(); + }); + + it('should update file mode to match the vinyl mode', function(done) { + if (isWindows) { + this.skip(); + return; + } + + var inputPath = path.join(__dirname, './fixtures/test.coffee'); + var inputBase = path.join(__dirname, './fixtures/'); + var expectedPath = path.join(__dirname, './out-fixtures/test.coffee'); + var expectedContents = fs.readFileSync(inputPath); + var expectedBase = path.join(__dirname, './out-fixtures'); + var startMode = parseInt('0655', 8); + var expectedMode = parseInt('0722', 8); + + var expectedFile = new File({ + base: inputBase, + cwd: __dirname, + path: inputPath, + contents: expectedContents, + stat: { + mode: expectedMode, + }, + }); + + var onEnd = function() { + expect(masked(fs.lstatSync(expectedPath).mode)).toEqual(expectedMode); + done(); + }; + + fs.mkdirSync(expectedBase); + fs.closeSync(fs.openSync(expectedPath, 'w')); + fs.chmodSync(expectedPath, startMode); + + var stream = vfs.dest('./out-fixtures/', { cwd: __dirname }); + stream.on('end', onEnd); + stream.write(expectedFile); + stream.end(); + }); + + it('should update directory mode to match the vinyl mode', function(done) { + if (isWindows) { + this.skip(); + return; + } + + var inputBase = path.join(__dirname, './fixtures/'); + var inputPath = path.join(__dirname, './fixtures/wow'); + var expectedPath = path.join(__dirname, './out-fixtures/wow'); + var expectedBase = path.join(__dirname, './out-fixtures'); + + var firstFile = new File({ + base: inputBase, + cwd: __dirname, + path: expectedPath, + stat: fs.statSync(inputPath), + }); + var startMode = firstFile.stat.mode; + var expectedMode = parseInt('727', 8); + + var expectedFile = new File(firstFile); + expectedFile.stat.mode = (startMode & ~parseInt('7777', 8)) | expectedMode; + + var onEnd = function() { + expect(masked(fs.lstatSync(expectedPath).mode)).toEqual(expectedMode); + done(); + }; + + fs.mkdirSync(expectedBase); + + var stream = vfs.dest('./out-fixtures/', { cwd: __dirname }); + stream.on('end', onEnd); + stream.write(firstFile); + stream.write(expectedFile); + stream.end(); + }); + + it('should use different modes for files and directories', function(done) { + if (isWindows) { + this.skip(); + return; + } + + var inputBase = path.join(__dirname, './fixtures'); + var inputPath = path.join(__dirname, './fixtures/wow/suchempty'); + var expectedBase = path.join(__dirname, './out-fixtures/wow'); + var expectedPath = path.join(__dirname, './out-fixtures/wow/suchempty'); + var expectedDirMode = parseInt('755', 8); + var expectedFileMode = parseInt('655', 8); + + var firstFile = new File({ + base: inputBase, + cwd: __dirname, + path: inputPath, + contents: fs.readFileSync(inputPath), + stat: fs.statSync(inputPath), + }); + + var onEnd = function() { + expect(masked(fs.lstatSync(expectedBase).mode)).toEqual(expectedDirMode); + expect(masked(fs.lstatSync(expectedPath).mode)).toEqual(expectedFileMode); + done(); + }; + + var stream = vfs.dest('./out-fixtures/', { + cwd: __dirname, + mode: expectedFileMode, + dirMode: expectedDirMode, + }); + stream.on('end', onEnd); + stream.write(firstFile); + stream.end(); + }); + + it('should not fchmod a matching file', function(done) { + if (isWindows) { + this.skip(); + return; + } + + var fchmodSpy = expect.spyOn(fs, 'fchmod').andCallThrough(); + + var inputPath = path.join(__dirname, './fixtures/test.coffee'); + var inputBase = path.join(__dirname, './fixtures/'); + var expectedPath = path.join(__dirname, './out-fixtures/test.coffee'); + var expectedContents = fs.readFileSync(inputPath); + var expectedMode = parseInt('711', 8); + + var expectedFile = new File({ + base: inputBase, + cwd: __dirname, + path: inputPath, + contents: expectedContents, + stat: { + mode: expectedMode, + }, + }); + + var onEnd = function() { + expect(fchmodSpy.calls.length).toEqual(0); + expect(masked(fs.lstatSync(expectedPath).mode)).toEqual(expectedMode); + done(); + }; + + var stream = vfs.dest('./out-fixtures/', { cwd: __dirname }); + stream.on('end', onEnd); + stream.write(expectedFile); + stream.end(); + }); + + it('should see a file with special chmod (setuid/setgid/sticky) as matching', function(done) { + if (isWindows) { + this.skip(); + return; + } + + var fchmodSpy = expect.spyOn(fs, 'fchmod').andCallThrough(); + + var inputPath = path.join(__dirname, './fixtures/test.coffee'); + var inputBase = path.join(__dirname, './fixtures/'); + var expectedPath = path.join(__dirname, './out-fixtures/test.coffee'); + var expectedContents = fs.readFileSync(inputPath); + var expectedBase = path.join(__dirname, './out-fixtures'); + var expectedMode = parseInt('3722', 8); + var normalMode = parseInt('722', 8); + + var expectedFile = new File({ + base: inputBase, + cwd: __dirname, + path: inputPath, + contents: expectedContents, + stat: { + mode: normalMode, + }, + }); + + var onEnd = function() { + expect(fchmodSpy.calls.length).toEqual(0); + done(); + }; + + fs.mkdirSync(expectedBase); + fs.closeSync(fs.openSync(expectedPath, 'w')); + fs.chmodSync(expectedPath, expectedMode); + + var stream = vfs.dest('./out-fixtures/', { cwd: __dirname }); + stream.on('end', onEnd); + stream.write(expectedFile); + stream.end(); + }); + + it('should report fchmod errors', function(done) { + if (isWindows) { + this.skip(); + return; + } + + var inputPath = path.join(__dirname, './fixtures/test.coffee'); + var inputBase = path.join(__dirname, './fixtures/'); + var expectedPath = path.join(__dirname, './out-fixtures/test.coffee'); + var expectedContents = fs.readFileSync(inputPath); + var expectedBase = path.join(__dirname, './out-fixtures'); + var expectedMode = parseInt('722', 8); + + var fchmodSpy = expect.spyOn(fs, 'fchmod').andCall(function() { + var callback = arguments[arguments.length - 1]; + callback(new Error('mocked error')); + }); + + var expectedFile = new File({ + base: inputBase, + cwd: __dirname, + path: inputPath, + contents: expectedContents, + stat: { + mode: expectedMode, + }, + }); + + fs.mkdirSync(expectedBase); + fs.closeSync(fs.openSync(expectedPath, 'w')); + + var stream = vfs.dest('./out-fixtures/', { cwd: __dirname }); + stream.on('error', function(err) { + expect(err).toExist(); + expect(fchmodSpy.calls.length).toEqual(1); + done(); + }); + stream.write(expectedFile); + }); +}); diff --git a/test/destTimes.js b/test/destTimes.js new file mode 100644 index 00000000..9d69100a --- /dev/null +++ b/test/destTimes.js @@ -0,0 +1,223 @@ +'use strict'; + +var os = require('os'); +var path = require('path'); + +var fs = require('graceful-fs'); +var del = require('del'); +var File = require('vinyl'); +var expect = require('expect'); + +var vfs = require('../'); + +function wipeOut() { + this.timeout(20000); + + expect.restoreSpies(); + + // Async del to get sort-of-fix for https://github.com/isaacs/rimraf/issues/72 + return del(path.join(__dirname, './out-fixtures/')); +} + +var isWindows = (os.platform() === 'win32'); + +describe('.dest() with custom times', function() { + beforeEach(wipeOut); + afterEach(wipeOut); + + it('should not call futimes when no mtime is provided on the vinyl stat', function(done) { + if (isWindows) { + console.log('Changing the time of a directory errors in Windows.'); + console.log('Windows is treated as though it does not have permission to make this operation.'); + this.skip(); + return; + } + + var inputPath = path.join(__dirname, './fixtures/test.coffee'); + var inputBase = path.join(__dirname, './fixtures/'); + var expectedPath = path.join(__dirname, './out-fixtures/test.coffee'); + var expectedContents = fs.readFileSync(inputPath); + var earlier = Date.now() - 1000; + + var futimesSpy = expect.spyOn(fs, 'futimes').andCallThrough(); + + var expectedFile = new File({ + base: inputBase, + cwd: __dirname, + path: inputPath, + contents: expectedContents, + stat: {}, + }); + + var onEnd = function() { + var stats = fs.lstatSync(expectedPath); + + expect(futimesSpy.calls.length).toEqual(0); + expect(stats.atime.getTime()).toBeGreaterThan(earlier); + expect(stats.mtime.getTime()).toBeGreaterThan(earlier); + done(); + }; + + var stream = vfs.dest('./out-fixtures/', { cwd: __dirname }); + stream.on('end', onEnd); + stream.write(expectedFile); + stream.end(); + }); + + it('should call futimes when an mtime is provided on the vinyl stat', function(done) { + if (isWindows) { + this.skip(); + return; + } + + var inputPath = path.join(__dirname, './fixtures/test.coffee'); + var inputBase = path.join(__dirname, './fixtures/'); + var expectedPath = path.join(__dirname, './out-fixtures/test.coffee'); + var expectedContents = fs.readFileSync(inputPath); + var expectedMtime = fs.lstatSync(inputPath).mtime; + + var futimesSpy = expect.spyOn(fs, 'futimes').andCallThrough(); + + var expectedFile = new File({ + base: inputBase, + cwd: __dirname, + path: inputPath, + contents: expectedContents, + stat: { + mtime: expectedMtime, + }, + }); + + var onEnd = function() { + var stats = fs.lstatSync(expectedPath); + + expect(futimesSpy.calls.length).toEqual(1); + expect(stats.mtime.getTime()).toEqual(expectedMtime.getTime()); + expect(expectedFile.stat.mtime).toEqual(expectedMtime); + done(); + }; + + var stream = vfs.dest('./out-fixtures/', { cwd: __dirname }); + stream.on('end', onEnd); + stream.write(expectedFile); + stream.end(); + }); + + it('should not call futimes when provided mtime on the vinyl stat is invalid', function(done) { + if (isWindows) { + this.skip(); + return; + } + + var inputPath = path.join(__dirname, './fixtures/test.coffee'); + var inputBase = path.join(__dirname, './fixtures/'); + var expectedPath = path.join(__dirname, './out-fixtures/test.coffee'); + var expectedContents = fs.readFileSync(inputPath); + var earlier = Date.now() - 1000; + + var futimesSpy = expect.spyOn(fs, 'futimes').andCallThrough(); + + var expectedFile = new File({ + base: inputBase, + cwd: __dirname, + path: inputPath, + contents: expectedContents, + stat: { + mtime: new Date(undefined), + }, + }); + + var onEnd = function() { + var stats = fs.lstatSync(expectedPath); + + expect(futimesSpy.calls.length).toEqual(0); + expect(stats.mtime.getTime()).toBeGreaterThan(earlier); + done(); + }; + + var stream = vfs.dest('./out-fixtures/', { cwd: __dirname }); + stream.on('end', onEnd); + stream.write(expectedFile); + stream.end(); + }); + + it('should call futimes when provided mtime on the vinyl stat is valid but provided atime is invalid', function(done) { + if (isWindows) { + this.skip(); + return; + } + + var inputPath = path.join(__dirname, './fixtures/test.coffee'); + var inputBase = path.join(__dirname, './fixtures/'); + var expectedPath = path.join(__dirname, './out-fixtures/test.coffee'); + var expectedContents = fs.readFileSync(inputPath); + var expectedMtime = fs.lstatSync(inputPath).mtime; + var invalidAtime = new Date(undefined); + + var futimesSpy = expect.spyOn(fs, 'futimes').andCallThrough(); + + var expectedFile = new File({ + base: inputBase, + cwd: __dirname, + path: inputPath, + contents: expectedContents, + stat: { + atime: invalidAtime, + mtime: expectedMtime, + }, + }); + + var onEnd = function() { + var stats = fs.lstatSync(expectedPath); + + expect(futimesSpy.calls.length).toEqual(1); + expect(stats.mtime.getTime()).toEqual(expectedMtime.getTime()); + done(); + }; + + var stream = vfs.dest('./out-fixtures/', { cwd: __dirname }); + stream.on('end', onEnd); + stream.write(expectedFile); + stream.end(); + }); + + it('should write file atime and mtime using the vinyl stat', function(done) { + if (isWindows) { + this.skip(); + return; + } + + var inputPath = path.join(__dirname, './fixtures/test.coffee'); + var inputBase = path.join(__dirname, './fixtures/'); + var expectedPath = path.join(__dirname, './out-fixtures/test.coffee'); + var expectedContents = fs.readFileSync(inputPath); + var expectedAtime = fs.lstatSync(inputPath).atime; + var expectedMtime = fs.lstatSync(inputPath).mtime; + + var expectedFile = new File({ + base: inputBase, + cwd: __dirname, + path: inputPath, + contents: expectedContents, + stat: { + atime: expectedAtime, + mtime: expectedMtime, + }, + }); + + var onEnd = function() { + var stats = fs.lstatSync(expectedPath); + + expect(stats.atime.getTime()).toEqual(expectedAtime.getTime()); + expect(stats.mtime.getTime()).toEqual(expectedMtime.getTime()); + expect(expectedFile.stat.mtime).toEqual(expectedMtime); + expect(expectedFile.stat.atime).toEqual(expectedAtime); + done(); + }; + + var stream = vfs.dest('./out-fixtures/', { cwd: __dirname }); + stream.on('end', onEnd); + stream.write(expectedFile); + stream.end(); + }); +}); diff --git a/test/fileOperations.js b/test/fileOperations.js new file mode 100644 index 00000000..454eaba9 --- /dev/null +++ b/test/fileOperations.js @@ -0,0 +1,868 @@ +'use strict'; + +var expect = require('expect'); + +var os = require('os'); +var fs = require('graceful-fs'); +var del = require('del'); +var path = require('path'); +var File = require('vinyl'); +var defaultResolution = require('default-resolution'); + +var fo = require('../lib/fileOperations'); + +var closeFd = fo.closeFd; +var isOwner = fo.isOwner; +var writeFile = fo.writeFile; +var getModeDiff = fo.getModeDiff; +var getTimesDiff = fo.getTimesDiff; +var updateMetadata = fo.updateMetadata; + +var resolution = defaultResolution(); + +var MASK_MODE = parseInt('777', 8); + +function masked(mode) { + return mode & MASK_MODE; +} + +function noop() {} + +var isWindows = (os.platform() === 'win32'); + +describe('isOwner', function() { + + var ownerStat = { + uid: 9001, + }; + + var nonOwnerStat = { + uid: 9002, + }; + + var getuidSpy; + var geteuidSpy; + + beforeEach(function(done) { + if (typeof process.geteuid !== 'function') { + process.geteuid = noop; + } + + // Windows :( + if (typeof process.getuid !== 'function') { + process.getuid = noop; + } + + getuidSpy = expect.spyOn(process, 'getuid').andReturn(ownerStat.uid); + geteuidSpy = expect.spyOn(process, 'geteuid').andReturn(ownerStat.uid); + + done(); + }); + + afterEach(function(done) { + expect.restoreSpies(); + + if (process.geteuid === noop) { + delete process.geteuid; + } + + // Windows :( + if (process.getuid === noop) { + delete process.getuid; + } + + done(); + }); + + // TODO: test for having neither + + it('uses process.geteuid() when available', function(done) { + + isOwner(ownerStat); + + expect(getuidSpy.calls.length).toEqual(0); + expect(geteuidSpy.calls.length).toEqual(1); + + done(); + }); + + it('uses process.getuid() when geteuid() is not available', function(done) { + delete process.geteuid; + + isOwner(ownerStat); + + expect(getuidSpy.calls.length).toEqual(1); + + done(); + }); + + it('returns false when non-root and non-owner', function(done) { + var result = isOwner(nonOwnerStat); + + expect(result).toEqual(false); + + done(); + }); + + it('returns true when owner and non-root', function(done) { + var result = isOwner(ownerStat); + + expect(result).toEqual(true); + + done(); + }); + + it('returns true when non-owner but root', function(done) { + expect.spyOn(process, 'geteuid').andReturn(0); // 0 is root uid + + var result = isOwner(nonOwnerStat); + + expect(result).toEqual(true); + + done(); + }); +}); + +describe('getModeDiff', function() { + + it('returns 0 if both modes are the same', function(done) { + var fsMode = parseInt('777', 8); + var vfsMode = parseInt('777', 8); + + var result = getModeDiff(fsMode, vfsMode); + + expect(result).toEqual(0); + + done(); + }); + + it('returns 0 if vinyl mode is not a number', function(done) { + var fsMode = parseInt('777', 8); + var vfsMode = undefined; + + var result = getModeDiff(fsMode, vfsMode); + + expect(result).toEqual(0); + + done(); + }); + + it('returns a value greater than 0 if modes are different', function(done) { + var fsMode = parseInt('777', 8); + var vfsMode = parseInt('744', 8); + + var result = getModeDiff(fsMode, vfsMode); + + expect(result).toEqual(27); + + done(); + }); + + it('does not matter the order of diffing', function(done) { + var fsMode = parseInt('655', 8); + var vfsMode = parseInt('777', 8); + + var result = getModeDiff(fsMode, vfsMode); + + expect(result).toEqual(82); + + done(); + }); + + it('ignores the sticky/setuid/setgid bits', function(done) { + var fsMode = parseInt('1777', 8); + var vfsMode = parseInt('4777', 8); + + var result = getModeDiff(fsMode, vfsMode); + + expect(result).toEqual(0); + + done(); + }); +}); + +describe('getTimesDiff', function() { + + it('returns undefined if vinyl mtime is not a valid date', function(done) { + var fsStat = { + mtime: new Date(), + }; + var vfsStat = { + mtime: new Date(undefined), + }; + + var result = getTimesDiff(fsStat, vfsStat); + + expect(result).toEqual(undefined); + + done(); + }); + + it('returns undefined if vinyl mtime & atime are both equal to counterparts', function(done) { + var now = Date.now(); + var fsStat = { + mtime: new Date(now), + atime: new Date(now), + }; + var vfsStat = { + mtime: new Date(now), + atime: new Date(now), + }; + + var result = getTimesDiff(fsStat, vfsStat); + + expect(result).toEqual(undefined); + + done(); + }); + + // TODO: is this proper/expected? + it('returns undefined if vinyl mtimes equals the counterpart and atimes are null', function(done) { + var now = Date.now(); + var fsStat = { + mtime: new Date(now), + atime: null, + }; + var vfsStat = { + mtime: new Date(now), + atime: null, + }; + + var result = getTimesDiff(fsStat, vfsStat); + + expect(result).toEqual(undefined); + + done(); + }); + + it('returns a diff object if mtimes do not match', function(done) { + var now = Date.now(); + var then = now - 1000; + var fsStat = { + mtime: new Date(now), + }; + var vfsStat = { + mtime: new Date(then), + }; + var expected = { + mtime: new Date(then), + atime: undefined, + }; + + var result = getTimesDiff(fsStat, vfsStat); + + expect(result).toEqual(expected); + + done(); + }); + + it('returns a diff object if atimes do not match', function(done) { + var now = Date.now(); + var then = now - 1000; + var fsStat = { + mtime: new Date(now), + atime: new Date(now), + }; + var vfsStat = { + mtime: new Date(now), + atime: new Date(then), + }; + var expected = { + mtime: new Date(now), + atime: new Date(then), + }; + + var result = getTimesDiff(fsStat, vfsStat); + + expect(result).toEqual(expected); + + done(); + }); + + it('returns the fs atime if the vinyl atime is invalid', function(done) { + var now = Date.now(); + var fsStat = { + mtime: new Date(now), + atime: new Date(now), + }; + var vfsStat = { + mtime: new Date(now), + atime: new Date(undefined), + }; + var expected = { + mtime: new Date(now), + atime: new Date(now), + }; + + var result = getTimesDiff(fsStat, vfsStat); + + expect(result).toEqual(expected); + + done(); + }); + + // TODO: is this proper/expected? + it('makes atime diff undefined if fs and vinyl atime are invalid', function(done) { + var now = Date.now(); + var fsStat = { + mtime: new Date(now), + atime: new Date(undefined), + }; + var vfsStat = { + mtime: new Date(now), + atime: new Date(undefined), + }; + var expected = { + mtime: new Date(now), + atime: undefined, + }; + + var result = getTimesDiff(fsStat, vfsStat); + + expect(result).toEqual(expected); + + done(); + }); +}); + +describe('closeFd', function() { + + it('calls the callback with propagated error if fd is not a number', function(done) { + var propagatedError = new Error(); + + closeFd(propagatedError, null, function(err) { + expect(err).toEqual(propagatedError); + + done(); + }); + }); + + it('calls the callback with close error if no error to propagate', function(done) { + closeFd(null, -1, function(err) { + expect(err).toExist(); + + done(); + }); + }); + + it('calls the callback with propagated error if close errors', function(done) { + var propagatedError = new Error(); + + closeFd(propagatedError, -1, function(err) { + expect(err).toEqual(propagatedError); + + done(); + }); + }); + + it('calls the callback with propagated error if close succeeds', function(done) { + var propagatedError = new Error(); + + var fd = fs.openSync(path.join(__dirname, './fixtures/test.coffee'), 'r'); + + var spy = expect.spyOn(fs, 'close').andCallThrough(); + + closeFd(propagatedError, fd, function(err) { + spy.restore(); + + expect(spy.calls.length).toEqual(1); + expect(err).toEqual(propagatedError); + + done(); + }); + }); + + it('calls the callback with no error if close succeeds & no propagated error', function(done) { + var fd = fs.openSync(path.join(__dirname, './fixtures/test.coffee'), 'r'); + + var spy = expect.spyOn(fs, 'close').andCallThrough(); + + closeFd(null, fd, function(err) { + spy.restore(); + + expect(spy.calls.length).toEqual(1); + expect(err).toEqual(undefined); + + done(); + }); + }); +}); + +describe('writeFile', function() { + + var filepath; + + beforeEach(function(done) { + filepath = path.join(__dirname, './fixtures/writeFile.txt'); + + done(); + }); + + afterEach(function() { + // Async del to get sort-of-fix for https://github.com/isaacs/rimraf/issues/72 + return del(filepath); + }); + + it('writes a file to the filesystem, does not close and returns the fd', function(done) { + var expected = 'test'; + var content = new Buffer(expected); + + writeFile(filepath, content, function(err, fd) { + expect(err).toNotExist(); + expect(typeof fd === 'number').toEqual(true); + + fs.close(fd, function() { + var written = fs.readFileSync(filepath, 'utf-8'); + + expect(written).toEqual(expected); + + done(); + }); + }); + }); + + it('defaults to writing files with 0666 mode', function(done) { + var expected = parseInt('0666', 8) & (~process.umask()); + var content = new Buffer('test'); + + writeFile(filepath, content, function(err, fd) { + expect(err).toNotExist(); + expect(typeof fd === 'number').toEqual(true); + + fs.close(fd, function() { + var stats = fs.lstatSync(filepath); + + expect(masked(stats.mode)).toEqual(expected); + + done(); + }); + }); + }); + + it('accepts a different mode in options', function(done) { + if (isWindows) { + console.log('Changing the mode of a file is not supported by node.js in Windows.'); + this.skip(); + return; + } + + var expected = parseInt('0777', 8) & (~process.umask()); + var content = new Buffer('test'); + var options = { + mode: parseInt('0777', 8), + }; + + writeFile(filepath, content, options, function(err, fd) { + expect(err).toNotExist(); + expect(typeof fd === 'number').toEqual(true); + + fs.close(fd, function() { + var stats = fs.lstatSync(filepath); + + expect(masked(stats.mode)).toEqual(expected); + + done(); + }); + }); + }); + + it('defaults to opening files with write flag', function(done) { + var content = new Buffer('test'); + + writeFile(filepath, content, function(err, fd) { + expect(err).toNotExist(); + expect(typeof fd === 'number').toEqual(true); + + fs.read(fd, new Buffer(4), 0, 4, 0, function(readErr) { + expect(readErr).toExist(); + + fs.close(fd, done); + }); + }); + }); + + it('accepts a different flag in options', function(done) { + var expected = 'test'; + var content = new Buffer(expected); + var options = { + flag: 'w+', + }; + + writeFile(filepath, content, options, function(err, fd) { + expect(err).toNotExist(); + expect(typeof fd === 'number').toEqual(true); + + fs.read(fd, new Buffer(4), 0, 4, 0, function(readErr, _, written) { + expect(readErr).toNotExist(); + + expect(written.toString()).toEqual(expected); + + fs.close(fd, done); + }); + }); + }); + + it('appends to a file if append flag is given', function(done) { + var initial = 'test'; + var toWrite = '-a-thing'; + + fs.writeFileSync(filepath, initial, 'utf-8'); + + var expected = initial + toWrite; + + var content = new Buffer(toWrite); + var options = { + flag: 'a', + }; + + writeFile(filepath, content, options, function(err, fd) { + expect(err).toNotExist(); + expect(typeof fd === 'number').toEqual(true); + + fs.close(fd, function() { + var written = fs.readFileSync(filepath, 'utf-8'); + + expect(written).toEqual(expected); + + done(); + }); + }); + }); + + it('does not pass a file descriptor if open call errors', function(done) { + filepath = path.join(__dirname, './not-exist-dir/writeFile.txt'); + var content = new Buffer('test'); + + writeFile(filepath, content, function(err, fd) { + expect(err).toExist(); + expect(typeof fd === 'number').toEqual(false); + + done(); + }); + }); + + it('passes a file descriptor if write call errors', function(done) { + var existsFilepath = path.join(__dirname, './fixtures/test.coffee'); // File must exist + var content = new Buffer('test'); + var options = { + flag: 'r', + }; + + writeFile(existsFilepath, content, options, function(err, fd) { + expect(err).toExist(); + expect(typeof fd === 'number').toEqual(true); + + fs.close(fd, done); + }); + }); + + it('passes an error if called without buffer for data', function(done) { + writeFile(filepath, 'test', function(err) { + expect(err).toExist(); + + done(); + }); + }); + + it('does not error if options is falsey', function(done) { + var content = new Buffer('test'); + writeFile(filepath, content, null, function(err, fd) { + expect(err).toNotExist(); + expect(typeof fd === 'number').toEqual(true); + + fs.close(fd, done); + }); + }); +}); + +describe('updateMetadata', function() { + + var inputPath = path.join(__dirname, './fixtures/stats.txt'); + var file; + + beforeEach(function(done) { + file = new File({ + base: __dirname, + cwd: __dirname, + path: inputPath, + contents: null, + stat: { + + }, + }); + + done(); + }); + + afterEach(function(done) { + expect.restoreSpies(); + + del.sync(inputPath); + + if (process.geteuid === noop) { + delete process.geteuid; + } + + done(); + }); + + it('passes the error and file descriptor if fstat fails', function(done) { + if (isWindows) { + console.log('Changing the time of a directory errors in Windows.'); + console.log('Changing the mode of a file is not supported by node.js in Windows.'); + console.log('Windows is treated as though it does not have permission to make these operations.'); + this.skip(); + return; + } + + var fd = 9001; + + updateMetadata(fd, file, function(err, fd2) { + expect(err).toExist(); + expect(typeof fd === 'number').toEqual(true); + expect(fd2).toEqual(fd); + + done(); + }); + }); + + it('updates the vinyl object with fs stats', function(done) { + if (isWindows) { + this.skip(); + return; + } + + var fd = fs.openSync(inputPath, 'w+'); + var stats = fs.fstatSync(fd); + + updateMetadata(fd, file, function(err, fd2) { + // Not sure why .toEqual doesn't match these + Object.keys(file.stat).forEach(function(key) { + expect(file.stat[key]).toEqual(stats[key]); + }); + + fs.close(fd2, done); + }); + }); + + it('does not touch the fs if nothing to update', function(done) { + if (isWindows) { + this.skip(); + return; + } + + var fchmodSpy = expect.spyOn(fs, 'fchmod').andCallThrough(); + var futimesSpy = expect.spyOn(fs, 'futimes').andCallThrough(); + + var fd = fs.openSync(inputPath, 'w+'); + + updateMetadata(fd, file, function(err, fd2) { + expect(fchmodSpy.calls.length).toEqual(0); + expect(futimesSpy.calls.length).toEqual(0); + + fs.close(fd2, done); + }); + }); + + it('does not touch the fs if process is not owner of the file', function(done) { + if (isWindows) { + this.skip(); + return; + } + + if (typeof process.geteuid !== 'function') { + process.geteuid = noop; + } + + expect.spyOn(process, 'geteuid').andReturn(9002); + var fchmodSpy = expect.spyOn(fs, 'fchmod').andCallThrough(); + var futimesSpy = expect.spyOn(fs, 'futimes').andCallThrough(); + + file.stat.mtime = new Date(Date.now() - 1000); + + var fd = fs.openSync(inputPath, 'w+'); + + updateMetadata(fd, file, function(err, fd2) { + expect(fchmodSpy.calls.length).toEqual(0); + expect(futimesSpy.calls.length).toEqual(0); + + fs.close(fd2, done); + }); + }); + + it('updates times on fs and vinyl object if there is a diff', function(done) { + if (isWindows) { + this.skip(); + return; + } + + var futimesSpy = expect.spyOn(fs, 'futimes').andCallThrough(); + + var now = Date.now(); + var then = now - 1000; + file.stat.mtime = new Date(then); + file.stat.atime = new Date(then); + + var fd = fs.openSync(inputPath, 'w+'); + + updateMetadata(fd, file, function(err, fd2) { + expect(futimesSpy.calls.length).toEqual(1); + var stats = fs.fstatSync(fd); + var mtimeMs = Date.parse(file.stat.mtime); + var mtime = resolution ? mtimeMs - (mtimeMs % resolution) : mtimeMs; + var atimeMs = Date.parse(file.stat.atime); + var atime = resolution ? atimeMs - (atimeMs % resolution) : atimeMs; + expect(file.stat.mtime).toEqual(new Date(then)); + expect(mtime).toEqual(Date.parse(stats.mtime)); + expect(file.stat.atime).toEqual(new Date(then)); + expect(atime).toEqual(Date.parse(stats.atime)); + + fs.close(fd2, done); + }); + }); + + it('forwards futimes error and descriptor upon error', function(done) { + if (isWindows) { + this.skip(); + return; + } + + var futimesSpy = expect.spyOn(fs, 'futimes').andCall(function(fd, atime, mtime, cb) { + cb(new Error('mocked error')); + }); + + var now = Date.now(); + var then = now - 1000; + file.stat.mtime = new Date(then); + file.stat.atime = new Date(then); + + var fd = fs.openSync(inputPath, 'w+'); + + updateMetadata(fd, file, function(err, fd2) { + expect(err).toExist(); + expect(futimesSpy.calls.length).toEqual(1); + expect(typeof fd2 === 'number').toEqual(true); + + fs.close(fd2, done); + }); + }); + + it('updates the mode on fs and vinyl object if there is a diff', function(done) { + if (isWindows) { + this.skip(); + return; + } + + var fchmodSpy = expect.spyOn(fs, 'fchmod').andCallThrough(); + + var mode = parseInt('777', 8); + file.stat.mode = mode; + + var fd = fs.openSync(inputPath, 'w+'); + + updateMetadata(fd, file, function(err, fd2) { + expect(fchmodSpy.calls.length).toEqual(1); + var stats = fs.fstatSync(fd); + expect(file.stat.mode).toEqual(stats.mode); + + fs.close(fd2, done); + }); + }); + + it('forwards fchmod error and descriptor upon error', function(done) { + if (isWindows) { + this.skip(); + return; + } + + var mode = parseInt('777', 8); + file.stat.mode = mode; + + var fd = fs.openSync(inputPath, 'w+'); + + var fchmodSpy = expect.spyOn(fs, 'fchmod').andCall(function(fd, mode, cb) { + cb(new Error('mocked error')); + }); + + updateMetadata(fd, file, function(err, fd2) { + expect(err).toExist(); + expect(fchmodSpy.calls.length).toEqual(1); + expect(typeof fd2 === 'number').toEqual(true); + + fs.close(fd2, done); + }); + }); + + it('updates the mode & times on fs and vinyl object if there is a diff', function(done) { + if (isWindows) { + this.skip(); + return; + } + + var fchmodSpy = expect.spyOn(fs, 'fchmod').andCallThrough(); + var futimesSpy = expect.spyOn(fs, 'futimes').andCallThrough(); + + var mode = parseInt('777', 8); + file.stat.mode = mode; + + var now = Date.now(); + var then = now - 1000; + file.stat.mtime = new Date(then); + file.stat.atime = new Date(then); + + var fd = fs.openSync(inputPath, 'w+'); + + updateMetadata(fd, file, function(err, fd2) { + expect(fchmodSpy.calls.length).toEqual(1); + expect(futimesSpy.calls.length).toEqual(1); + + var stats = fs.fstatSync(fd); + var mtimeMs = Date.parse(file.stat.mtime); + var mtime = resolution ? mtimeMs - (mtimeMs % resolution) : mtimeMs; + var atimeMs = Date.parse(file.stat.atime); + var atime = resolution ? atimeMs - (atimeMs % resolution) : atimeMs; + + expect(file.stat.mtime).toEqual(new Date(then)); + expect(mtime).toEqual(Date.parse(stats.mtime)); + expect(file.stat.atime).toEqual(new Date(then)); + expect(atime).toEqual(Date.parse(stats.atime)); + expect(file.stat.mode).toEqual(stats.mode); + + fs.close(fd2, done); + }); + }); + + it('forwards fchmod error and descriptor through futimes if there is a time diff', function(done) { + if (isWindows) { + this.skip(); + return; + } + + var expectedErr = new Error('mocked error'); + + var fchmodSpy = expect.spyOn(fs, 'fchmod').andCall(function(fd, mode, cb) { + cb(expectedErr); + }); + var futimesSpy = expect.spyOn(fs, 'futimes').andCallThrough(); + + var mode = parseInt('777', 8); + file.stat.mode = mode; + + var now = Date.now(); + var then = now - 1000; + file.stat.mtime = new Date(then); + file.stat.atime = new Date(then); + + var fd = fs.openSync(inputPath, 'w'); + + updateMetadata(fd, file, function(err, fd2) { + expect(err).toExist(); + expect(err).toEqual(expectedErr); + expect(fchmodSpy.calls.length).toEqual(1); + expect(futimesSpy.calls.length).toEqual(1); + expect(typeof fd2 === 'number').toEqual(true); + + fs.close(fd2, done); + }); + }); +}); diff --git a/test/fixtures/test-symlink b/test/fixtures/test-symlink deleted file mode 120000 index 3fcfe6c7..00000000 --- a/test/fixtures/test-symlink +++ /dev/null @@ -1 +0,0 @@ -test.coffee \ No newline at end of file diff --git a/test/fixtures/test-symlink-dir b/test/fixtures/test-symlink-dir deleted file mode 120000 index 9a17374b..00000000 --- a/test/fixtures/test-symlink-dir +++ /dev/null @@ -1 +0,0 @@ -wow \ No newline at end of file diff --git a/test/not-owned/not-owned.txt b/test/not-owned/not-owned.txt new file mode 100644 index 00000000..aeeb2943 --- /dev/null +++ b/test/not-owned/not-owned.txt @@ -0,0 +1 @@ +Something new diff --git a/test/notOwned.js b/test/notOwned.js new file mode 100644 index 00000000..5758e887 --- /dev/null +++ b/test/notOwned.js @@ -0,0 +1,75 @@ +'use strict'; + +var path = require('path'); + +var expect = require('expect'); + +var fs = require('graceful-fs'); +var File = require('vinyl'); + +var vfs = require('../'); + +describe('.dest() on not owned files', function() { + var outDir = path.join(__dirname, './not-owned/'); + var outPath = path.join(outDir, 'not-owned.txt'); + + var dirStats = fs.statSync(outDir); + var fileStats = fs.statSync(outPath); + + it('does not error if mtime is different', function(done) { + if (dirStats.uid !== 0 || fileStats.uid !== 0) { + console.log('Test files not owned by root. ' + + 'Please chown ' + outDir + ' and' + outPath + ' and try again.'); + this.skip(); + return; + } + + var expectedFile = new File({ + base: __dirname, + cwd: __dirname, + path: 'not-owned/not-owned.txt', + contents: new Buffer('Something new'), + stat: { + mtime: new Date(Date.now() - 1000), + }, + }); + + var stream = vfs.dest(outDir); + stream.write(expectedFile); + stream.on('error', function(err) { + expect(err).toNotExist(); + done(err); + }); + stream.on('end', done); + stream.end(); + }); + + it('does not error if mode is different', function(done) { + if (dirStats.uid !== 0 || fileStats.uid !== 0) { + console.log('Test files not owned by root. ' + + 'Please chown ' + outDir + ' and' + outPath + ' and try again.'); + this.skip(); + return; + } + + var expectedFile = new File({ + base: __dirname, + cwd: __dirname, + path: 'not-owned/not-owned.txt', + contents: new Buffer('Something new'), + stat: { + mode: parseInt('777', 8), + }, + }); + + var stream = vfs.dest(outDir); + stream.write(expectedFile); + stream.on('error', function(err) { + expect(err).toNotExist(); + done(err); + }); + stream.on('end', done); + stream.end(); + }); + +}); diff --git a/test/src.js b/test/src.js index 34a6f0e5..3fa2c655 100644 --- a/test/src.js +++ b/test/src.js @@ -396,38 +396,54 @@ describe('source stream', function() { stream1.pipe(stream2).pipe(bufferStream); }); - it('should follow file symlinks', function(done) { - var expectedPath = path.join(__dirname, './fixtures/test.coffee'); +}); + +describe('.src() symlinks', function() { + + + var dirPath = path.join(__dirname, './fixtures/wow'); + var dirSymlinkPath = path.join(__dirname, './fixtures/test-symlink-dir'); + + var filePath = path.join(__dirname, './fixtures/test.coffee'); + var fileSymlinkPath = path.join(__dirname, './fixtures/test-symlink'); + + beforeEach(function(done) { + fs.symlinkSync(dirPath, dirSymlinkPath); + fs.symlinkSync(filePath, fileSymlinkPath); + done(); + }); + + afterEach(function(done) { + fs.unlinkSync(dirSymlinkPath); + fs.unlinkSync(fileSymlinkPath); + done(); + }); - var stream = vfs.src('./fixtures/test-symlink', { cwd: __dirname }); + it('should follow file symlinks', function(done) { + var stream = vfs.src(fileSymlinkPath, { cwd: __dirname }); stream.on('data', function(file) { - file.path.should.equal(expectedPath); + file.path.should.equal(filePath); done(); }); }); it('should follow dir symlinks', function(done) { - var expectedPath = path.join(__dirname, './fixtures/wow'); - - var stream = vfs.src('./fixtures/test-symlink-dir', { cwd: __dirname }); + var stream = vfs.src(dirSymlinkPath, { cwd: __dirname }); stream.on('data', function(file) { - file.path.should.equal(expectedPath); + file.path.should.equal(dirPath); done(); }); }); it('should preserve file symlinks with followSymlinks option set to false', function(done) { - var sourcePath = path.join(__dirname, './fixtures/test-symlink'); - var expectedPath = sourcePath; - - fs.readlink(sourcePath, function(err, expectedRelativeSymlinkPath) { + fs.readlink(fileSymlinkPath, function(err, expectedRelativeSymlinkPath) { if (err) { throw err; } - var stream = vfs.src('./fixtures/test-symlink', { cwd: __dirname, followSymlinks: false }); + var stream = vfs.src(fileSymlinkPath, { cwd: __dirname, followSymlinks: false }); stream.on('data', function(file) { - file.path.should.equal(expectedPath); + file.path.should.equal(fileSymlinkPath); file.symlink.should.equal(expectedRelativeSymlinkPath); done(); }); @@ -435,21 +451,17 @@ describe('source stream', function() { }); it('should preserve dir symlinks with followSymlinks option set to false', function(done) { - var sourcePath = path.join(__dirname, './fixtures/test-symlink-dir'); - var expectedPath = sourcePath; - - fs.readlink(sourcePath, function(err, expectedRelativeSymlinkPath) { + fs.readlink(dirSymlinkPath, function(err, expectedRelativeSymlinkPath) { if (err) { throw err; } - var stream = vfs.src(sourcePath, { cwd: __dirname, followSymlinks: false }); + var stream = vfs.src(dirSymlinkPath, { cwd: __dirname, followSymlinks: false }); stream.on('data', function(file) { - file.path.should.equal(expectedPath); + file.path.should.equal(dirSymlinkPath); file.symlink.should.equal(expectedRelativeSymlinkPath); done(); }); }); }); - }); diff --git a/test/symlink.js b/test/symlink.js index d1b3df9c..b6a74764 100644 --- a/test/symlink.js +++ b/test/symlink.js @@ -6,9 +6,10 @@ var statSpy = spies.statSpy; var vfs = require('../'); +var os = require('os'); var path = require('path'); var fs = require('graceful-fs'); -var rimraf = require('rimraf'); +var del = require('del'); var bufEqual = require('buffer-equal'); var through = require('through2'); @@ -17,12 +18,13 @@ var File = require('vinyl'); var should = require('should'); require('mocha'); -var wipeOut = function(cb) { - rimraf(path.join(__dirname, './out-fixtures/'), cb); +function wipeOut() { spies.setError('false'); statSpy.reset(); chmodSpy.reset(); -}; + + return del(path.join(__dirname, './out-fixtures/')); +} var dataWrap = function(fn) { return function(data, enc, cb) { @@ -35,14 +37,16 @@ var realMode = function(n) { return n & parseInt('777', 8); }; +var isWindows = (os.platform() === 'win32'); + describe('symlink stream', function() { beforeEach(wipeOut); afterEach(wipeOut); - it('should explode on invalid folder', function(done) { + it.skip('should explode on invalid folder', function(done) { var stream; try { - stream = gulp.symlink(); + stream = vfs.symlink(); } catch (err) { should.exist(err); should.not.exist(stream); @@ -307,6 +311,12 @@ describe('symlink stream', function() { }); it('should use different modes for files and directories', function(done) { + if (isWindows) { + console.log('Changing the mode of a file is not supported by node.js in Windows.'); + this.skip(); + return; + } + var inputBase = path.join(__dirname, './fixtures'); var inputPath = path.join(__dirname, './fixtures/wow/suchempty'); var expectedBase = path.join(__dirname, './out-fixtures/wow'); @@ -372,20 +382,23 @@ describe('symlink stream', function() { }); it('should report IO errors', function(done) { + if (isWindows) { + console.log('Changing the mode of a file is not supported by node.js in Windows.'); + console.log('This test is skipped on Windows because we have to chmod the file to 0.'); + this.skip(); + return; + } + var inputPath = path.join(__dirname, './fixtures/test.coffee'); var inputBase = path.join(__dirname, './fixtures/'); var expectedContents = fs.readFileSync(inputPath); var expectedBase = path.join(__dirname, './out-fixtures'); - var expectedMode = parseInt('722', 8); var expectedFile = new File({ base: inputBase, cwd: __dirname, path: inputPath, contents: expectedContents, - stat: { - mode: expectedMode, - }, }); fs.mkdirSync(expectedBase);