diff --git a/lib/binding.js b/lib/binding.js index 101af11a..b07f5cb6 100644 --- a/lib/binding.js +++ b/lib/binding.js @@ -711,6 +711,65 @@ Binding.prototype.rmdir = function(pathname, callback) { }; +var PATH_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + +var MAX_ATTEMPTS = 62 * 62 * 62; + +/** + * Create a directory based on a template. + * See http://web.mit.edu/freebsd/head/lib/libc/stdio/mktemp.c + * @param {string} template Path template (trailing Xs will be replaced). + * @param {string} encoding The encoding ('utf-8' or 'buffer'). + * @param {function(Error, string)} callback Optional callback. + */ +Binding.prototype.mkdtemp = function(prefix, encoding, callback) { + if (encoding && typeof encoding !== 'string') { + callback = encoding; + encoding = 'utf-8'; + } + return maybeCallback(callback, this, function() { + prefix = prefix.replace(/X{0,6}$/, 'XXXXXX'); + var parentPath = path.dirname(prefix); + var parent = this._system.getItem(parentPath); + if (!parent) { + throw new FSError('ENOENT', prefix); + } + if (!(parent instanceof Directory)) { + throw new FSError('ENOTDIR', prefix); + } + this.access(parentPath, parseInt('0002', 8)); + var template = path.basename(prefix); + var unique = false; + var count = 0; + var name; + while (!unique && count < MAX_ATTEMPTS) { + var position = template.length - 1; + var replacement = ''; + while (template.charAt(position) === 'X') { + replacement += PATH_CHARS.charAt(Math.floor(PATH_CHARS.length * Math.random())); + position -= 1; + } + var candidate = template.slice(0, position + 1) + replacement; + if (!parent.getItem(candidate)) { + name = candidate; + unique = true; + } + count += 1; + } + if (!name) { + throw new FSError('EEXIST', prefix); + } + var dir = new Directory(); + parent.addItem(name, dir); + var uniquePath = path.join(parentPath, name); + if (encoding === 'buffer') { + uniquePath = new Buffer(uniquePath); + } + return uniquePath; + }); +}; + + /** * Truncate a file. * @param {number} fd File descriptor. diff --git a/package.json b/package.json index be4d33e1..86a2a2c9 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "eslint": "^3.11.1", "eslint-config-tschaub": "^6.0.0", "mocha": "3.1.2", - "rimraf": "2.5.4" + "rimraf": "2.5.4", + "semver": "^5.3.0" } } diff --git a/readme.md b/readme.md index 096e3d36..0faf1574 100644 --- a/readme.md +++ b/readme.md @@ -197,22 +197,14 @@ npm install mock-fs --save-dev ## Caveats -### Using with other modules that modify `fs` - When you require `mock-fs`, Node's own `fs` module is patched to allow the binding to the underlying file system to be swapped out. If you require `mock-fs` *before* any other modules that modify `fs` (e.g. `graceful-fs`), the mock should behave as expected. **Note** `mock-fs` is not compatible with `graceful-fs@3.x` but works with `graceful-fs@4.x`. -### `fs` overrides - -The following [`fs` functions](http://nodejs.org/api/fs.html) are overridden: `fs.ReadStream`, `fs.Stats`, `fs.WriteStream`, `fs.access`, `fs.accessSync`, `fs.appendFile`, `fs.appendFileSync`, `fs.chmod`, `fs.chmodSync`, `fs.chown`, `fs.chownSync`, `fs.close`, `fs.closeSync`, `fs.createReadStream`, `fs.createWriteStream`, `fs.exists`, `fs.existsSync`, `fs.fchmod`, `fs.fchmodSync`, `fs.fchown`, `fs.fchownSync`, `fs.fdatasync`, `fs.fdatasyncSync`, `fs.fstat`, `fs.fstatSync`, `fs.fsync`, `fs.fsyncSync`, `fs.ftruncate`, `fs.ftruncateSync`, `fs.futimes`, `fs.futimesSync`, `fs.lchmod`, `fs.lchmodSync`, `fs.lchown`, `fs.lchownSync`, `fs.link`, `fs.linkSync`, `fs.lstatSync`, `fs.lstat`, `fs.mkdir`, `fs.mkdirSync`, `fs.open`, `fs.openSync`, `fs.read`, `fs.readSync`, `fs.readFile`, `fs.readFileSync`, `fs.readdir`, `fs.readdirSync`, `fs.readlink`, `fs.readlinkSync`, `fs.realpath`, `fs.realpathSync`, `fs.rename`, `fs.renameSync`, `fs.rmdir`, `fs.rmdirSync`, `fs.stat`, `fs.statSync`, `fs.symlink`, `fs.symlinkSync`, `fs.truncate`, `fs.truncateSync`, `fs.unlink`, `fs.unlinkSync`, `fs.utimes`, `fs.utimesSync`, `fs.write`, `fs.writeSync`, `fs.writeFile`, and `fs.writeFileSync`. - Mock `fs.Stats` objects have the following properties: `dev`, `ino`, `nlink`, `mode`, `size`, `rdev`, `blksize`, `blocks`, `atime`, `ctime`, `mtime`, `birthtime`, `uid`, and `gid`. In addition, all of the `is*()` method are provided (e.g. `isDirectory()`, `isFile()`, et al.). Mock file access is controlled based on file mode where `process.getuid()` and `process.getgid()` are available (POSIX systems). On other systems (e.g. Windows) the file mode has no effect. -The following `fs` functions are *not* currently mocked (if your tests use these, they will work against the real file system): `fs.FSWatcher`, `fs.unwatchFile`, `fs.watch`, and `fs.watchFile`. Pull requests welcome. - Tested on Linux, OSX, and Windows using Node 0.10 through 6.x. Check the tickets for a list of [known issues](https://github.com/tschaub/mock-fs/issues). [![Current Status](https://secure.travis-ci.org/tschaub/mock-fs.png?branch=master)](https://travis-ci.org/tschaub/mock-fs) diff --git a/test/lib/.eslintrc b/test/.eslintrc similarity index 100% rename from test/lib/.eslintrc rename to test/.eslintrc diff --git a/test/helper.js b/test/helper.js index 4ce27f82..2dba5dba 100644 --- a/test/helper.js +++ b/test/helper.js @@ -2,6 +2,7 @@ var chai = require('chai'); var constants = require('constants'); +var semver = require('semver'); /** @type {boolean} */ @@ -14,6 +15,13 @@ chai.config.includeStack = true; */ exports.assert = chai.assert; +exports.inVersion = function(range) { + if (semver.satisfies(process.version, range)) { + return {it: it, describe: describe}; + } else { + return {it: xit, describe: xdescribe}; + } +}; /** * Convert a string to flags for fs.open. diff --git a/test/lib/binding.spec.js b/test/lib/binding.spec.js index 4719e34a..71685829 100644 --- a/test/lib/binding.spec.js +++ b/test/lib/binding.spec.js @@ -1101,6 +1101,35 @@ describe('Binding', function() { }); + describe('#mkdtemp()', function() { + + it('creates a new directory', function() { + var binding = new Binding(system); + var template = path.join('mock-dir', 'fooXXXXXX'); + var dirPath = binding.mkdtemp(template); + assert.notEqual(template, dirPath); + var dir = system.getItem(dirPath); + assert.instanceOf(dir, Directory); + }); + + it('fails if parent does not exist', function() { + var binding = new Binding(system); + var dirPath = path.join('bogus', 'pathXXXXXX'); + assert.throws(function() { + binding.mkdtemp(dirPath); + }); + }); + + it('fails if file exists', function() { + var binding = new Binding(system); + var dirPath = path.join('mock-dir', 'one.txt', 'XXXXXX'); + assert.throws(function() { + binding.mkdtemp(dirPath); + }); + }); + + }); + describe('#rmdir()', function() { it('removes an empty directory', function() { diff --git a/test/lib/index.spec.js b/test/lib/index.spec.js index a3ddfdf5..567ab46a 100644 --- a/test/lib/index.spec.js +++ b/test/lib/index.spec.js @@ -1,12 +1,15 @@ 'use strict'; var Writable = require('stream').Writable; -var assert = require('../helper').assert; +var helper = require('../helper'); var fs = require('fs'); var mock = require('../../lib/index'); var os = require('os'); var path = require('path'); +var assert = helper.assert; +var inVersion = helper.inVersion; + var testParentPerms = (fs.access && fs.accessSync && process.getuid && process.getgid); describe('The API', function() { @@ -2120,6 +2123,212 @@ describe('Mocking the file system', function() { } }); + if (fs.mkdtemp) { + describe('fs.mkdtemp(prefix[, options], callback)', function() { + + beforeEach(function() { + mock({ + 'parent': {}, + 'file': 'contents', + 'unwriteable': mock.directory({mode: parseInt('0555', 8)}) + }); + }); + afterEach(mock.restore); + + it('creates a new directory', function(done) { + fs.mkdtemp('parent/dir', function(err, dirPath) { + if (err) { + return done(err); + } + var parentPath = path.dirname(dirPath); + assert.equal(parentPath, 'parent'); + var stats = fs.statSync(dirPath); + assert.isTrue(stats.isDirectory()); + done(); + }); + }); + + inVersion('>=6').it('accepts a "utf8" encoding argument', function(done) { + fs.mkdtemp('parent/dir', 'utf8', function(err, dirPath) { + if (err) { + return done(err); + } + assert.isString(dirPath); + var parentPath = path.dirname(dirPath); + assert.equal(parentPath, 'parent'); + var stats = fs.statSync(dirPath); + assert.isTrue(stats.isDirectory()); + done(); + }); + }); + + inVersion('>=6').it('accepts a "buffer" encoding argument', function(done) { + fs.mkdtemp('parent/dir', 'buffer', function(err, buffer) { + if (err) { + return done(err); + } + assert.instanceOf(buffer, Buffer); + var dirPath = buffer.toString(); + var parentPath = path.dirname(dirPath); + assert.equal(parentPath, 'parent'); + var stats = fs.statSync(dirPath); + assert.isTrue(stats.isDirectory()); + done(); + }); + }); + + inVersion('>=6').it('accepts an options argument with "utf8" encoding', function(done) { + fs.mkdtemp('parent/dir', {encoding: 'utf8'}, function(err, dirPath) { + if (err) { + return done(err); + } + assert.isString(dirPath); + var parentPath = path.dirname(dirPath); + assert.equal(parentPath, 'parent'); + var stats = fs.statSync(dirPath); + assert.isTrue(stats.isDirectory()); + done(); + }); + }); + + inVersion('>=6').it('accepts an options argument with "buffer" encoding', function(done) { + fs.mkdtemp('parent/dir', {encoding: 'buffer'}, function(err, buffer) { + if (err) { + return done(err); + } + assert.instanceOf(buffer, Buffer); + var dirPath = buffer.toString(); + var parentPath = path.dirname(dirPath); + assert.equal(parentPath, 'parent'); + var stats = fs.statSync(dirPath); + assert.isTrue(stats.isDirectory()); + done(); + }); + }); + + it('fails if parent does not exist', function(done) { + fs.mkdtemp('unknown/child', function(err, dirPath) { + if (!err || dirPath) { + done(new Error('Expected failure')); + } else { + assert.isTrue(!dirPath); + assert.instanceOf(err, Error); + assert.equal(err.code, 'ENOENT'); + done(); + } + }); + }); + + it('fails if parent is a file', function(done) { + fs.mkdtemp('file/child', function(err, dirPath) { + if (!err || dirPath) { + done(new Error('Expected failure')); + } else { + assert.isTrue(!dirPath); + assert.instanceOf(err, Error); + assert.equal(err.code, 'ENOTDIR'); + done(); + } + }); + }); + + if (testParentPerms) { + it('fails if parent is not writeable', function(done) { + fs.mkdtemp('unwriteable/child', function(err, dirPath) { + if (!err || dirPath) { + done(new Error('Expected failure')); + } else { + assert.isTrue(!dirPath); + assert.instanceOf(err, Error); + assert.equal(err.code, 'EACCES'); + done(); + } + }); + }); + } + }); + } + + if (fs.mkdtempSync) { + describe('fs.mkdtempSync(prefix[, options])', function() { + + beforeEach(function() { + mock({ + 'parent': {}, + 'file': 'contents', + 'unwriteable': mock.directory({mode: parseInt('0555', 8)}) + }); + }); + afterEach(mock.restore); + + it('creates a new directory', function() { + var dirPath = fs.mkdtempSync('parent/dir'); + var parentPath = path.dirname(dirPath); + assert.equal(parentPath, 'parent'); + var stats = fs.statSync(dirPath); + assert.isTrue(stats.isDirectory()); + }); + + inVersion('>=6').it('accepts a "utf8" encoding argument', function() { + var dirPath = fs.mkdtempSync('parent/dir', 'utf8'); + assert.isString(dirPath); + var parentPath = path.dirname(dirPath); + assert.equal(parentPath, 'parent'); + var stats = fs.statSync(dirPath); + assert.isTrue(stats.isDirectory()); + }); + + inVersion('>=6').it('accepts a "buffer" encoding argument', function() { + var buffer = fs.mkdtempSync('parent/dir', 'buffer'); + assert.instanceOf(buffer, Buffer); + var dirPath = buffer.toString(); + var parentPath = path.dirname(dirPath); + assert.equal(parentPath, 'parent'); + var stats = fs.statSync(dirPath); + assert.isTrue(stats.isDirectory()); + }); + + inVersion('>=6').it('accepts an options argument with "utf8" encoding', function() { + var dirPath = fs.mkdtempSync('parent/dir', {encoding: 'utf8'}); + assert.isString(dirPath); + var parentPath = path.dirname(dirPath); + assert.equal(parentPath, 'parent'); + var stats = fs.statSync(dirPath); + assert.isTrue(stats.isDirectory()); + }); + + inVersion('>=6').it('accepts an options argument with "buffer" encoding', function() { + var buffer = fs.mkdtempSync('parent/dir', {encoding: 'buffer'}); + assert.instanceOf(buffer, Buffer); + var dirPath = buffer.toString(); + var parentPath = path.dirname(dirPath); + assert.equal(parentPath, 'parent'); + var stats = fs.statSync(dirPath); + assert.isTrue(stats.isDirectory()); + }); + + it('fails if parent does not exist', function() { + assert.throws(function() { + fs.mkdtempSync('unknown/child'); + }); + }); + + it('fails if parent is a file', function() { + assert.throws(function() { + fs.mkdtempSync('file/child'); + }); + }); + + if (testParentPerms) { + it('fails if parent is not writeable', function() { + assert.throws(function() { + fs.mkdtempSync('unwriteable/child'); + }); + }); + } + }); + } + describe('fs.rmdir(path, callback)', function() { beforeEach(function() {