Skip to content

Commit

Permalink
Support for fs.mkdtemp and fs.mkdtempSync
Browse files Browse the repository at this point in the history
  • Loading branch information
tschaub committed Apr 30, 2017
1 parent d7532ca commit 663ef1d
Show file tree
Hide file tree
Showing 7 changed files with 308 additions and 10 deletions.
59 changes: 59 additions & 0 deletions lib/binding.js
Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -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"
}
}
8 changes: 0 additions & 8 deletions readme.md
Expand Up @@ -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)
File renamed without changes.
8 changes: 8 additions & 0 deletions test/helper.js
Expand Up @@ -2,6 +2,7 @@

var chai = require('chai');
var constants = require('constants');
var semver = require('semver');


/** @type {boolean} */
Expand All @@ -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.
Expand Down
29 changes: 29 additions & 0 deletions test/lib/binding.spec.js
Expand Up @@ -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() {
Expand Down
211 changes: 210 additions & 1 deletion 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() {
Expand Down Expand Up @@ -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() {
Expand Down

0 comments on commit 663ef1d

Please sign in to comment.