diff --git a/lib/bypass.js b/lib/bypass.js
new file mode 100755
index 00000000..770e81bd
--- /dev/null
+++ b/lib/bypass.js
@@ -0,0 +1,55 @@
+const realBinding = process.binding('fs');
+let storedBinding;
+
+/**
+ * Perform action, bypassing mock FS
+ * @example
+ * // This file exists on the real FS, not on the mocked FS
+ * const filePath = '/path/file.json';
+ * const data = mock.bypass(() => fs.readFileSync(filePath, 'utf-8'));
+ */
+exports = module.exports = function bypass(fn) {
+ if (typeof fn !== 'function') {
+ throw new Error(`Must provide a function to perform for mock.bypass()`);
+ }
+
+ exports.disable();
+
+ try {
+ // Perform action
+ const res = fn();
+
+ // Handle promise return
+ if (typeof res.then === 'function') {
+ res.then(exports.enable);
+ res.catch(exports.enable);
+ } else {
+ exports.enable();
+ }
+
+ return res;
+ } catch (e) {
+ exports.enable();
+ throw e;
+ }
+};
+
+/**
+ * Temporarily disable Mocked FS
+ */
+exports.disable = () => {
+ if (realBinding._mockedBinding) {
+ storedBinding = realBinding._mockedBinding;
+ delete realBinding._mockedBinding;
+ }
+};
+
+/**
+ * Enables Mocked FS after being disabled by mock.disable()
+ */
+exports.enable = () => {
+ if (storedBinding) {
+ realBinding._mockedBinding = storedBinding;
+ storedBinding = undefined;
+ }
+};
diff --git a/lib/filesystem.js b/lib/filesystem.js
index a860a7f8..268a8b2e 100644
--- a/lib/filesystem.js
+++ b/lib/filesystem.js
@@ -112,6 +112,8 @@ FileSystem.prototype.getItem = function(filepath) {
if (item) {
if (item instanceof Directory && name !== currentParts[i]) {
// make sure traversal is allowed
+ // This fails for Windows directories which do not have execute permission, by default. It may be a good idea
+ // to change this logic to windows-friendly. See notes in mock.createDirectoryInfoFromPaths()
if (!item.canExecute()) {
throw new FSError('EACCES', filepath);
}
diff --git a/lib/index.js b/lib/index.js
index 04ac665a..0f78d734 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -5,6 +5,8 @@ const FSError = require('./error');
const FileSystem = require('./filesystem');
const realBinding = process.binding('fs');
const path = require('path');
+const loader = require('./loader');
+const bypass = require('./bypass');
const fs = require('fs');
const toNamespacedPath = FileSystem.toNamespacedPath;
@@ -183,3 +185,17 @@ exports.directory = FileSystem.directory;
* Create a symbolic link factory.
*/
exports.symlink = FileSystem.symlink;
+
+/**
+ * Automatically maps specified paths (for use with `mock()`)
+ */
+exports.load = loader.load;
+
+/**
+ * Perform action, bypassing mock FS
+ * @example
+ * // This file exists on the real FS, not on the mocked FS
+ * const filePath = '/path/file.json';
+ * const data = mock.bypass(() => fs.readFileSync(filePath, 'utf-8'));
+ */
+exports.bypass = bypass;
diff --git a/lib/item.js b/lib/item.js
index 019c0529..1ec3e75d 100644
--- a/lib/item.js
+++ b/lib/item.js
@@ -88,6 +88,19 @@ function Item() {
this.links = 0;
}
+/**
+ * Add execute if read allowed
+ * See notes in index.js -> mapping#addDir
+ */
+// prettier-ignore
+Item.fixWin32Permissions = mode =>
+ (process.platform !== 'win32')
+ ? mode
+ : mode |
+ ((mode & permissions.USER_READ) && permissions.USER_EXEC) |
+ ((mode & permissions.GROUP_READ) && permissions.GROUP_EXEC) |
+ ((mode & permissions.OTHER_READ) && permissions.OTHER_EXEC);
+
/**
* Determine if the current user has read permission.
* @return {boolean} The current user can read.
@@ -140,8 +153,8 @@ Item.prototype.canExecute = function() {
let can = false;
if (uid === 0) {
can = true;
- } else if (uid === this._uid || uid !== uid) {
- // (uid !== uid) means uid is NaN, only for windows
+ } else if (uid === this._uid || isNaN(uid)) {
+ // NaN occurs on windows
can = (permissions.USER_EXEC & this._mode) === permissions.USER_EXEC;
} else if (gid === this._gid) {
can = (permissions.GROUP_EXEC & this._mode) === permissions.GROUP_EXEC;
diff --git a/lib/loader.js b/lib/loader.js
new file mode 100755
index 00000000..f2d549dc
--- /dev/null
+++ b/lib/loader.js
@@ -0,0 +1,118 @@
+const {fixWin32Permissions} = require('./item');
+const path = require('path');
+const FileSystem = require('./filesystem');
+const fs = require('fs');
+const bypass = require('./bypass');
+
+const createContext = ({output, options = {}, target}, newContext) =>
+ Object.assign(
+ {
+ // Assign options and set defaults if needed
+ options: {
+ recursive: options.recursive !== false,
+ lazyLoad: options.lazyLoad !== false
+ },
+ output,
+ target
+ },
+ newContext
+ );
+
+function addFile(context, stats, isRoot) {
+ const {output, target} = context;
+ const {lazyLoad} = context.options;
+
+ if (!stats.isFile()) {
+ throw new Error(`${target} is not a valid file!`);
+ }
+
+ const outputPropKey = isRoot ? target : path.basename(target);
+
+ output[outputPropKey] = () => {
+ const content = !lazyLoad ? fs.readFileSync(target) : '';
+ const file = FileSystem.file(Object.assign({}, stats, {content}))();
+
+ if (lazyLoad) {
+ Object.defineProperty(file, '_content', {
+ get() {
+ const res = bypass(() => fs.readFileSync(target));
+ Object.defineProperty(file, '_content', {
+ value: res,
+ writable: true
+ });
+ return res;
+ },
+ set(data) {
+ Object.defineProperty(file, '_content', {
+ value: data,
+ writable: true
+ });
+ },
+ configurable: true
+ });
+ }
+
+ return file;
+ };
+
+ return output[outputPropKey];
+}
+
+function addDir(context, stats, isRoot) {
+ const {target, output} = context;
+ const {recursive} = context.options;
+
+ if (!stats.isDirectory()) {
+ throw new Error(`${target} is not a valid directory!`);
+ }
+
+ stats = Object.assign({}, stats);
+ const outputPropKey = isRoot ? target : path.basename(target);
+
+ // On windows platforms, directories do not have the executable flag, which causes FileSystem.prototype.getItem
+ // to think that the directory cannot be traversed. This is a workaround, however, a better solution may be to
+ // re-think the logic in FileSystem.prototype.getItem
+ // This workaround adds executable privileges if read privileges are found
+ stats.mode = fixWin32Permissions(stats.mode);
+
+ // Create directory factory
+ const directoryItems = {};
+ output[outputPropKey] = FileSystem.directory(
+ Object.assign(stats, {items: directoryItems})
+ );
+
+ fs.readdirSync(target).forEach(p => {
+ const absPath = path.join(target, p);
+ const stats = fs.statSync(absPath);
+ const newContext = createContext(context, {
+ target: absPath,
+ output: directoryItems
+ });
+
+ if (recursive && stats.isDirectory()) {
+ addDir(newContext, stats);
+ } else if (stats.isFile()) {
+ addFile(newContext, stats);
+ }
+ });
+
+ return output[outputPropKey];
+}
+
+/**
+ * Load directory or file from real FS
+ */
+exports.load = function(p, options) {
+ return bypass(() => {
+ p = path.resolve(p);
+
+ const stats = fs.statSync(p);
+ const context = createContext({output: {}, options, target: p});
+
+ if (stats.isDirectory()) {
+ return addDir(context, stats, true);
+ } else if (stats.isFile()) {
+ return addFile(context, stats, true);
+ }
+ });
+};
diff --git a/readme.md b/readme.md
index 61dca207..15901b21 100644
--- a/readme.md
+++ b/readme.md
@@ -1,3 +1,5 @@
+[![Build Status](https://github.com/tschaub/mock-fs/workflows/Test/badge.svg)](https://github.com/tschaub/mock-fs/actions?workflow=Test)
+
# `mock-fs`
The `mock-fs` module allows Node's built-in [`fs` module](http://nodejs.org/api/fs.html) to be backed temporarily by an in-memory, mock file system. This lets you run tests against a set of mock files and directories instead of lugging around a bunch of test fixtures.
@@ -58,6 +60,42 @@ The second (optional) argument may include the properties below.
* `createCwd` - `boolean` Create a directory for `process.cwd()`. This is `true` by default.
* `createTmp` - `boolean` Create a directory for `os.tmpdir()`. This is `true` by default.
+### Loading real files & directories
+
+You can load real files and directories into the mock system using `mock.load()`
+
+#### Notes
+
+- All stat information is duplicated (dates, permissions, etc)
+- By default, all files are lazy-loaded, unless you specify the `{ lazyLoad: false }` option
+
+#### options
+
+| Option | Type | Default | Description |
+| --------- | ------- | ------- | ------------
+| lazyLoad | boolean | true | File content isn't loaded until explicitly read
+| recursive | boolean | true | Load all files and directories recursively
+
+#### `mock.load(path, options)`
+
+```js
+mock({
+ // Lazy-load file
+ 'my-file.txt': mock.load(path.resolve(__dirname, 'assets/special-file.txt')),
+
+ // Pre-load js file
+ 'ready.js': mock.load(path.resolve(__dirname, 'scripts/ready.js'), { lazyLoad: false }),
+
+ // Recursively loads all node_modules
+ 'node_modules': mock.load(path.resolve(__dirname, '../node_modules')),
+
+ // Creates a directory named /tmp with only the files in /tmp/special_tmp_files (no subdirectories), pre-loading all content
+ '/tmp': mock.load('/tmp/special_tmp_files', { recursive: false, lazyLoad:false }),
+
+ 'fakefile.txt': 'content here'
+});
+```
+
### Creating files
When `config` property values are a `string` or `Buffer`, a file is created with the provided content. For example, the following configuration creates a single file with string content (in addition to the two default directories).
@@ -187,6 +225,33 @@ beforeEach(function() {
afterEach(mock.restore);
```
+### Bypassing the mock file system
+
+#### `mock.bypass(fn)`
+
+Execute calls to the real filesystem with mock.bypass()
+
+```js
+// This file exists only on the real FS, not on the mocked FS
+const realFilePath = '/path/to/real/file.txt';
+const myData = mock.bypass(() => fs.readFileSync(realFilePath, 'utf-8'));
+```
+
+#### Async Warning
+
+Asynchronous calls are supported, however, they are not recommended as they could produce unintended consequences if
+anything else tries to access the mocked filesystem before they've completed.
+
+```js
+async function getFileInfo(fileName) {
+ return await mock.bypass(async () => {
+ const stats = await fs.promises.stat(fileName);
+ const data = await fs.promises.readFile(fileName);
+ return { stats, data };
+ });
+}
+```
+
## Install
Using `npm`:
@@ -222,6 +287,4 @@ expect(actual).toMatchSnapshot()
```
Note: it's safe to call `mock.restore` multiple times, so it can still be called in `afterEach` and then manually
-in test cases which use snapshot testing.
-
-[![Build Status](https://github.com/tschaub/mock-fs/workflows/Test/badge.svg)](https://github.com/tschaub/mock-fs/actions?workflow=Test)
+in test cases which use snapshot testing.
\ No newline at end of file
diff --git a/test/assets/dir/file2.txt b/test/assets/dir/file2.txt
new file mode 100755
index 00000000..fee93d14
--- /dev/null
+++ b/test/assets/dir/file2.txt
@@ -0,0 +1 @@
+data2
\ No newline at end of file
diff --git a/test/assets/dir/subdir/file3.txt b/test/assets/dir/subdir/file3.txt
new file mode 100755
index 00000000..cdca2c1e
--- /dev/null
+++ b/test/assets/dir/subdir/file3.txt
@@ -0,0 +1 @@
+data3
\ No newline at end of file
diff --git a/test/assets/file1.txt b/test/assets/file1.txt
new file mode 100755
index 00000000..0abc8f19
--- /dev/null
+++ b/test/assets/file1.txt
@@ -0,0 +1 @@
+data1
\ No newline at end of file
diff --git a/test/lib/index.spec.js b/test/lib/index.spec.js
index 42a95b08..017dcb11 100644
--- a/test/lib/index.spec.js
+++ b/test/lib/index.spec.js
@@ -5,8 +5,13 @@ const fs = require('fs');
const mock = require('../../lib/index');
const os = require('os');
const path = require('path');
+const File = require('../../lib/file');
+const {fixWin32Permissions} = require('../../lib/item');
+const Directory = require('../../lib/directory');
+const withPromise = helper.withPromise;
const assert = helper.assert;
+const assetsPath = path.resolve(__dirname, '../assets');
describe('The API', function() {
describe('mock()', function() {
@@ -180,6 +185,203 @@ describe('The API', function() {
});
});
+ describe(`mock.bypass()`, () => {
+ afterEach(mock.restore);
+
+ it('(synchronous) bypasses mock FS & restores after', () => {
+ mock({'/path/to/file': 'content'});
+
+ assert.equal(fs.readFileSync('/path/to/file', 'utf8'), 'content');
+ assert.isNotOk(fs.existsSync(__filename));
+ assert.isOk(mock.bypass(() => fs.existsSync(__filename)));
+
+ assert.isNotOk(fs.existsSync(__filename));
+ });
+
+ withPromise.it('(async) bypasses mock FS & restores after', done => {
+ mock({'/path/to/file': 'content'});
+
+ assert.equal(fs.readFileSync('/path/to/file', 'utf8'), 'content');
+ assert.isNotOk(fs.existsSync(__filename));
+
+ mock.bypass(() =>
+ fs.promises
+ .stat(__filename)
+ .then(stat => {
+ assert.isTrue(stat.isFile());
+ return fs.promises.stat(__filename);
+ })
+ .then(stat => assert.isTrue(stat.isFile()))
+ .then(() => {
+ setTimeout(() => {
+ assert.isNotOk(fs.existsSync(__filename));
+ done();
+ }, 0);
+ })
+ .catch(err => done(err))
+ );
+ });
+ });
+
+ describe(`mock.load()`, () => {
+ const statsCompareKeys = [
+ 'birthtime',
+ 'ctime',
+ 'mtime',
+ 'gid',
+ 'uid',
+ 'mtime',
+ 'mode'
+ ];
+ const filterStats = stats => {
+ const res = {};
+ for (const key of statsCompareKeys) {
+ const k =
+ (stats.hasOwnProperty(key) && key) ||
+ (stats.hasOwnProperty(`_${key}`) && `_${key}`);
+
+ if (k) {
+ res[key] =
+ k === 'mode' && stats.isDirectory()
+ ? fixWin32Permissions(stats[k])
+ : stats[k];
+ }
+ }
+ return res;
+ };
+
+ describe(`File`, () => {
+ const filePath = path.join(assetsPath, 'file1.txt');
+
+ it('creates a File factory with correct attributes', () => {
+ const file = mock.load(filePath)();
+ const stats = fs.statSync(filePath);
+
+ assert.instanceOf(file, File);
+ assert.deepEqual(filterStats(file), filterStats(stats));
+ });
+ describe('lazyLoad=true', () => {
+ let file;
+ beforeEach(() => (file = mock.load(filePath)()));
+
+ it('creates accessors', () => {
+ assert.typeOf(
+ Object.getOwnPropertyDescriptor(file, '_content').get,
+ 'function'
+ );
+ assert.typeOf(
+ Object.getOwnPropertyDescriptor(file, '_content').set,
+ 'function'
+ );
+ });
+ it('read file loads data and replaces accessors', () => {
+ assert.equal(file._content.toString(), 'data1');
+
+ assert.instanceOf(
+ Object.getOwnPropertyDescriptor(file, '_content').value,
+ Buffer
+ );
+ assert.isNotOk(
+ Object.getOwnPropertyDescriptor(file, '_content').get,
+ 'function'
+ );
+ assert.isNotOk(
+ Object.getOwnPropertyDescriptor(file, '_content').set,
+ 'function'
+ );
+ });
+ it('write file updates content and replaces accessors', () => {
+ file._content = Buffer.from('new data');
+
+ assert.equal(file._content.toString(), 'new data');
+ assert.instanceOf(
+ Object.getOwnPropertyDescriptor(file, '_content').value,
+ Buffer
+ );
+ assert.isNotOk(
+ Object.getOwnPropertyDescriptor(file, '_content').get,
+ 'function'
+ );
+ assert.isNotOk(
+ Object.getOwnPropertyDescriptor(file, '_content').set,
+ 'function'
+ );
+ });
+ });
+
+ it('lazyLoad=false loads file content', () => {
+ const file = mock.load(path.join(assetsPath, 'file1.txt'), {
+ lazyLoad: false
+ })();
+
+ assert.equal(
+ Object.getOwnPropertyDescriptor(file, '_content').value.toString(),
+ 'data1'
+ );
+ });
+
+ it('can read file from mocked FS', () => {
+ mock({'/file': mock.load(filePath)});
+ assert.equal(fs.readFileSync('/file'), 'data1');
+ mock.restore();
+ });
+ });
+
+ describe(`Dir`, () => {
+ it('creates a Directory factory with correct attributes', () => {
+ const dir = mock.load(assetsPath)();
+ const stats = fs.statSync(assetsPath);
+
+ assert.instanceOf(dir, Directory);
+ assert.deepEqual(filterStats(dir), filterStats(stats));
+ });
+ describe('recursive=true', () => {
+ it('creates all files & dirs', () => {
+ const base = mock.load(assetsPath, {recursive: true})();
+ const baseDir = base._items.dir;
+ const baseDirSubdir = baseDir._items.subdir;
+
+ assert.instanceOf(base, Directory);
+ assert.instanceOf(base._items['file1.txt'], File);
+ assert.instanceOf(baseDir, Directory);
+ assert.instanceOf(baseDir._items['file2.txt'], File);
+ assert.instanceOf(baseDirSubdir, Directory);
+ assert.instanceOf(baseDirSubdir._items['file3.txt'], File);
+ });
+ it('respects lazyLoad setting', () => {
+ let dir;
+ const getFile = () =>
+ dir._items.dir._items.subdir._items['file3.txt'];
+
+ dir = mock.load(assetsPath, {recursive: true, lazyLoad: true})();
+ assert.typeOf(
+ Object.getOwnPropertyDescriptor(getFile(), '_content').get,
+ 'function'
+ );
+
+ dir = mock.load(assetsPath, {recursive: true, lazyLoad: false})();
+ assert.instanceOf(
+ Object.getOwnPropertyDescriptor(getFile(), '_content').value,
+ Buffer
+ );
+ });
+ });
+
+ it('recursive=false creates files & does not recurse', () => {
+ const base = mock.load(assetsPath, {recursive: false})();
+ assert.instanceOf(base, Directory);
+ assert.instanceOf(base._items['file1.txt'], File);
+ assert.isNotOk(base._items.dir);
+ });
+
+ it('can read file from mocked FS', () => {
+ mock({'/dir': mock.load(assetsPath, {recursive: true})});
+ assert.equal(fs.readFileSync('/dir/file1.txt'), 'data1');
+ mock.restore();
+ });
+ });
+ });
+
xdescribe('mock.fs()', function() {
it('generates a mock fs module with a mock file system', function(done) {
const mockFs = mock.fs({