Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for fs.mkdtemp and fs.mkdtempSync #207

Merged
merged 1 commit into from
Apr 30, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions lib/binding.js
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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 `[email protected]` but works with `[email protected]`.

### `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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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