From 26bfdf792ac2608563d6130153a570995df5f53e Mon Sep 17 00:00:00 2001 From: Ron S Date: Mon, 27 Jul 2020 00:27:24 -0400 Subject: [PATCH 01/31] Added mock.bypass() and mock.createDirectoryInfoFromPaths() --- lib/index.js | 90 ++++++++++++++++++++++ test/assets/dir/file2.txt | 1 + test/assets/dir/subdir/file3.txt | 1 + test/assets/file1.txt | 1 + test/lib/index.spec.js | 126 ++++++++++++++++++++++++++++++- 5 files changed, 218 insertions(+), 1 deletion(-) create mode 100755 test/assets/dir/file2.txt create mode 100755 test/assets/dir/subdir/file3.txt create mode 100755 test/assets/file1.txt diff --git a/lib/index.js b/lib/index.js index 04ac665a..4b8f5332 100644 --- a/lib/index.js +++ b/lib/index.js @@ -183,3 +183,93 @@ exports.directory = FileSystem.directory; * Create a symbolic link factory. */ exports.symlink = FileSystem.symlink; + +/** + * 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 = function(fn) { + if (typeof fn !== 'function') { + throw new Error(`Must provide a function to perform for mock.bypass()`); + } + + // Deactivate mocked bindings + const binding = process.binding('fs')._mockedBinding; + delete process.binding('fs')._mockedBinding; + + // Perform action + const res = fn(); + + // Reactivate mocked bindings + process.binding('fs')._mockedBinding = binding; + + return res; +}; + +/** + * Populate a DirectoryItems object to use with mock() from a series of paths to files/directories + */ +exports.createDirectoryInfoFromPaths = function(paths, options) { + return exports.bypass(() => { + /** type FileSystem.DirectoryItems */ + const res = {}; + + /* Get options or apply defaults */ + let recursive = options && options.recursive; + let lazyLoad = options && options.lazyLoad; + if (recursive === undefined) { + recursive = true; + } + if (lazyLoad === undefined) { + lazyLoad = true; + } + + if (Array.isArray(paths)) { + paths.forEach(p => scan(p, true)); + } else { + scan(paths, true); + } + + return res; + + function scan(p, isRoot) { + if (typeof p !== 'string') { + throw new Error( + `Must provide path or array of paths (as strings) to createDirectoryInfoFromPaths()` + ); + } + + const stats = fs.statSync(p); + if (stats.isFile()) { + addFile(p, stats); + } else if ((isRoot || recursive) && stats.isDirectory()) { + res[p] = {}; + fs.readdirSync(p).forEach(subPath => scan(path.join(p, subPath))); + } + } + + function addFile(p, stats) { + if (!lazyLoad) { + res[p] = fs.readFileSync(p); + } + + /* Lazy Load */ + res[p] = () => + Object.defineProperty( + exports.file({...stats, content: ''})(), + '_content', + { + get() { + const res = exports.bypass(() => fs.readFileSync(p)); + Object.defineProperty(this, '_content', {value: res}); + return res; + }, + configurable: true + } + ); + } + }); +}; 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..65cc7da9 100644 --- a/test/lib/index.spec.js +++ b/test/lib/index.spec.js @@ -5,6 +5,7 @@ const fs = require('fs'); const mock = require('../../lib/index'); const os = require('os'); const path = require('path'); +const File = require('../../lib/file'); const assert = helper.assert; @@ -180,7 +181,130 @@ describe('The API', function() { }); }); - xdescribe('mock.fs()', function() { + describe(`mock.bypass()`, () => { + afterEach(mock.restore); + + it('bypasses mock FS', () => { + mock({'/path/to/file': 'content'}); + + assert.equal(fs.readFileSync('/path/to/file', 'utf8'), 'content'); + assert.throws(() => fs.readFileSync(__filename)); + assert.doesNotThrow(() => mock.bypass(() => fs.readFileSync(__filename))); + }); + }); + + describe(`mock.createDirectoryInfoFromPaths()`, () => { + const assetsPath = path.resolve(__dirname, '../assets'); + + it('adds from multiple paths', () => { + const expectedFile1 = path.join(assetsPath, 'file1.txt'); + const expectedFile2 = path.join(assetsPath, 'dir/subdir/file3.txt'); + const paths = mock.createDirectoryInfoFromPaths([ + expectedFile1, + path.dirname(expectedFile2) + ]); + + assert.instanceOf(paths[expectedFile1](), File); + assert.instanceOf(paths[expectedFile2](), File); + assert.deepEqual(paths[path.dirname(expectedFile2)], {}); + }); + + it('adds from single path', () => { + const expectedFile = path.join(assetsPath, 'dir/subdir/file3.txt'); + const paths = mock.createDirectoryInfoFromPaths( + path.dirname(expectedFile) + ); + + assert.instanceOf(paths[expectedFile](), File); + assert.deepEqual(paths[path.dirname(expectedFile)], {}); + }); + + it('recursive=false does not go deep', () => { + const expectedFile = path.join(assetsPath, 'file1.txt'); + const paths = mock.createDirectoryInfoFromPaths(assetsPath, { + recursive: false + }); + + const keys = Object.keys(paths); + assert.lengthOf(keys, 2); + assert.instanceOf(paths[expectedFile](), File); + assert.deepEqual(paths[path.dirname(expectedFile)], {}); + }); + + it('recursive=true loads all files and directories', () => { + const paths = mock.createDirectoryInfoFromPaths(assetsPath); + + const keys = Object.keys(paths); + assert.lengthOf(keys, 7); + }); + + describe('lazyLoad=true', () => { + let paths; + const triggeredGetters = []; + + before(() => { + paths = mock.createDirectoryInfoFromPaths(assetsPath, {lazyLoad: true}); + + for (const p of Object.keys(paths)) { + if (typeof paths[p] === 'function') { + const file = paths[p](); + // Ensure getter was set + assert( + Object.getOwnPropertyDescriptor(file, '_content').hasOwnProperty( + 'get' + ) + ); + + // Wrap factory & getter so we know when it is fired + const originalGetter = Object.getOwnPropertyDescriptor( + file, + '_content' + ).get; + + paths[p] = () => + Object.defineProperty(file, '_content', { + get() { + triggeredGetters.push(p); + return originalGetter.call(this); + } + }); + } + } + + mock(paths); + }); + after(() => { + mock.restore(); + triggeredGetters.splice(0, triggeredGetters.length); + }); + + it('waits to load files', () => assert.lengthOf(triggeredGetters, 0)); + it('loads proper data', () => { + const expectedFile1 = path.join(assetsPath, 'file1.txt'); + const expectedFile2 = path.join(assetsPath, 'dir/file2.txt'); + const expectedFile3 = path.join(assetsPath, 'dir/subdir/file3.txt'); + + const res1 = fs.readFileSync(expectedFile1, 'utf8'); + const res2 = fs.readFileSync(expectedFile2, 'utf8'); + const res3 = fs.readFileSync(expectedFile3, 'utf8'); + // Triggering a duplicate read to determine getter was replaced. + // If it wasn't, triggeredGetters array will have an extra expectedFile2 + fs.readFileSync(expectedFile2, 'utf8'); + + assert.equal(res1, 'data1'); + assert.equal(res2, 'data2'); + assert.equal(res3, 'data3'); + assert.deepEqual(triggeredGetters, [ + expectedFile1, + expectedFile2, + expectedFile3 + ]); + assert.lengthOf(triggeredGetters, 3); + }); + }); + }); + + describe('mock.fs()', function() { it('generates a mock fs module with a mock file system', function(done) { const mockFs = mock.fs({ 'path/to/file.txt': 'file content' From be7bf436b09ffc573ef4dac82fbf443c4521acaa Mon Sep 17 00:00:00 2001 From: Ron S Date: Mon, 27 Jul 2020 00:31:04 -0400 Subject: [PATCH 02/31] Fix broken `xdescribe` --- test/lib/index.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/lib/index.spec.js b/test/lib/index.spec.js index 65cc7da9..1ba94163 100644 --- a/test/lib/index.spec.js +++ b/test/lib/index.spec.js @@ -304,7 +304,7 @@ describe('The API', function() { }); }); - describe('mock.fs()', function() { + xdescribe('mock.fs()', function() { it('generates a mock fs module with a mock file system', function(done) { const mockFs = mock.fs({ 'path/to/file.txt': 'file content' From a39712e12c4a425cefc7d786c807e20a2fa31ba9 Mon Sep 17 00:00:00 2001 From: Ron S Date: Mon, 27 Jul 2020 00:52:18 -0400 Subject: [PATCH 03/31] Node 6 support --- lib/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/index.js b/lib/index.js index 4b8f5332..48c5271f 100644 --- a/lib/index.js +++ b/lib/index.js @@ -259,7 +259,7 @@ exports.createDirectoryInfoFromPaths = function(paths, options) { /* Lazy Load */ res[p] = () => Object.defineProperty( - exports.file({...stats, content: ''})(), + exports.file(Object.assign({}, stats, {content: ''}))(), '_content', { get() { From 266569b69e0cd2aff551aad9af64d9446d5128b4 Mon Sep 17 00:00:00 2001 From: Ron S Date: Mon, 27 Jul 2020 00:52:48 -0400 Subject: [PATCH 04/31] Investigate missing entry (squash) --- test/lib/index.spec.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/lib/index.spec.js b/test/lib/index.spec.js index 1ba94163..320536e2 100644 --- a/test/lib/index.spec.js +++ b/test/lib/index.spec.js @@ -235,6 +235,7 @@ describe('The API', function() { const paths = mock.createDirectoryInfoFromPaths(assetsPath); const keys = Object.keys(paths); + console.log(keys); assert.lengthOf(keys, 7); }); From 95d3ff2a4af4c4153d373af19f59c43dc6e71593 Mon Sep 17 00:00:00 2001 From: Ron S Date: Mon, 27 Jul 2020 01:20:40 -0400 Subject: [PATCH 05/31] Fixed + improved test --- test/lib/index.spec.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/test/lib/index.spec.js b/test/lib/index.spec.js index 320536e2..4c0cbf1d 100644 --- a/test/lib/index.spec.js +++ b/test/lib/index.spec.js @@ -233,10 +233,18 @@ describe('The API', function() { it('recursive=true loads all files and directories', () => { const paths = mock.createDirectoryInfoFromPaths(assetsPath); + const expectedPaths = [ + '', + 'file1.txt', + 'dir', + 'dir/file2.txt', + 'dir/subdir', + 'dir/subdir/file3.txt' + ].map(p => path.join(assetsPath, p)); const keys = Object.keys(paths); - console.log(keys); - assert.lengthOf(keys, 7); + assert.lengthOf(keys, 6); + assert.deepEqual(expectedPaths.slice().sort(), keys.slice().sort()); }); describe('lazyLoad=true', () => { From 34a450339b2d7b6ef53a8dacd49a499826ac684e Mon Sep 17 00:00:00 2001 From: Ron S Date: Mon, 27 Jul 2020 01:20:51 -0400 Subject: [PATCH 06/31] Added normalize path --- lib/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/index.js b/lib/index.js index 48c5271f..9e6198d4 100644 --- a/lib/index.js +++ b/lib/index.js @@ -242,6 +242,8 @@ exports.createDirectoryInfoFromPaths = function(paths, options) { ); } + p = path.normalize(p); + const stats = fs.statSync(p); if (stats.isFile()) { addFile(p, stats); From 91ab56a803163d1d8d3f6ea94a534a5bfd7e4afc Mon Sep 17 00:00:00 2001 From: Ron S Date: Mon, 27 Jul 2020 01:36:53 -0400 Subject: [PATCH 07/31] Added coverage for non-string error --- test/lib/index.spec.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/lib/index.spec.js b/test/lib/index.spec.js index 4c0cbf1d..3b32d783 100644 --- a/test/lib/index.spec.js +++ b/test/lib/index.spec.js @@ -196,6 +196,12 @@ describe('The API', function() { describe(`mock.createDirectoryInfoFromPaths()`, () => { const assetsPath = path.resolve(__dirname, '../assets'); + it('throws with non-string paths', () => { + assert.throws(() => mock.createDirectoryInfoFromPaths(null)); + assert.throws(() => mock.createDirectoryInfoFromPaths(['a', null])); + }); + + it('adds from multiple paths', () => { const expectedFile1 = path.join(assetsPath, 'file1.txt'); const expectedFile2 = path.join(assetsPath, 'dir/subdir/file3.txt'); From fdeed028e9bd9b40b4c2e485ac9ccdd5bf895b2c Mon Sep 17 00:00:00 2001 From: Ron S Date: Mon, 27 Jul 2020 01:49:22 -0400 Subject: [PATCH 08/31] Remove artifact --- lib/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/index.js b/lib/index.js index 9e6198d4..47213405 100644 --- a/lib/index.js +++ b/lib/index.js @@ -214,7 +214,6 @@ exports.bypass = function(fn) { */ exports.createDirectoryInfoFromPaths = function(paths, options) { return exports.bypass(() => { - /** type FileSystem.DirectoryItems */ const res = {}; /* Get options or apply defaults */ From 0ad98579ed9dae39bab23152951ad774eab4753d Mon Sep 17 00:00:00 2001 From: Ron S Date: Mon, 27 Jul 2020 01:51:37 -0400 Subject: [PATCH 09/31] prettier --- test/lib/index.spec.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/lib/index.spec.js b/test/lib/index.spec.js index 3b32d783..3a145c32 100644 --- a/test/lib/index.spec.js +++ b/test/lib/index.spec.js @@ -201,7 +201,6 @@ describe('The API', function() { assert.throws(() => mock.createDirectoryInfoFromPaths(['a', null])); }); - it('adds from multiple paths', () => { const expectedFile1 = path.join(assetsPath, 'file1.txt'); const expectedFile2 = path.join(assetsPath, 'dir/subdir/file3.txt'); From c3eddcbf56089fb0faf3eb6111189017d34ce9b1 Mon Sep 17 00:00:00 2001 From: Ron S Date: Mon, 27 Jul 2020 02:41:13 -0400 Subject: [PATCH 10/31] LazyLoad Fixes - Lazy-loaded files were not able to be written to if they hadn't been read yet - Descriptors defaulted to writable: false after setting value --- lib/index.js | 33 ++++++++++++++++++++------------- test/lib/index.spec.js | 9 +++++++-- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/lib/index.js b/lib/index.js index 47213405..0fe27ff0 100644 --- a/lib/index.js +++ b/lib/index.js @@ -258,19 +258,26 @@ exports.createDirectoryInfoFromPaths = function(paths, options) { } /* Lazy Load */ - res[p] = () => - Object.defineProperty( - exports.file(Object.assign({}, stats, {content: ''}))(), - '_content', - { - get() { - const res = exports.bypass(() => fs.readFileSync(p)); - Object.defineProperty(this, '_content', {value: res}); - return res; - }, - configurable: true - } - ); + res[p] = () => { + const file = exports.file(Object.assign({}, stats, {content: ''}))(); + return Object.defineProperty(file, '_content', { + get() { + const res = exports.bypass(() => fs.readFileSync(p)); + Object.defineProperty(file, '_content', { + value: res, + writable: true + }); + return res; + }, + set(data) { + Object.defineProperty(file, '_content', { + value: data, + writable: true + }); + }, + configurable: true + }); + }; } }); }; diff --git a/test/lib/index.spec.js b/test/lib/index.spec.js index 3a145c32..8c3aac98 100644 --- a/test/lib/index.spec.js +++ b/test/lib/index.spec.js @@ -256,7 +256,7 @@ describe('The API', function() { let paths; const triggeredGetters = []; - before(() => { + beforeEach(() => { paths = mock.createDirectoryInfoFromPaths(assetsPath, {lazyLoad: true}); for (const p of Object.keys(paths)) { @@ -287,11 +287,16 @@ describe('The API', function() { mock(paths); }); - after(() => { + afterEach(() => { mock.restore(); triggeredGetters.splice(0, triggeredGetters.length); }); + it("can write to lazy-loaded file before it's read", () => { + const filePath = path.join(assetsPath, 'file1.txt'); + fs.writeFileSync(filePath, 'new data'); + assert.equal(fs.readFileSync(filePath, 'utf8'), 'new data'); + }); it('waits to load files', () => assert.lengthOf(triggeredGetters, 0)); it('loads proper data', () => { const expectedFile1 = path.join(assetsPath, 'file1.txt'); From decb5de8632353d8e083a0cf9646db502d78154e Mon Sep 17 00:00:00 2001 From: Ron S Date: Mon, 27 Jul 2020 15:31:18 -0400 Subject: [PATCH 11/31] Updated readme - Included mock.createDirectoryInfoFromPaths() - Included mock.bypass() --- readme.md | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 61dca207..bfcd82df 100644 --- a/readme.md +++ b/readme.md @@ -58,7 +58,51 @@ 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. -### Creating files +### Automatically Creating Files & Directories + +You can create files and directories automatically by providing paths to `mock.createDirectoryInfoFromPaths()`. This will +read the filesystem for the paths you provide and automatically create files and directories for them. + +### `mock.createDirectoryInfoFromPaths(paths, options)` + +#### `paths` - `string` or `string[]` + +#### `options` + +The second (optional) argument may include the properties below. + + * `lazyLoad` - `boolean` File content does not get loaded until explicitly read. This is `true` by default. + * `recursive` - `boolean` Load all files and directories recursively. This is `true` by default. + +#### Examples + +Given the following directory structure +``` +- /root/ + - subdir/ + - file2.txt + - file1.txt +- /lib/ + - library.js + - extra.js +``` +```js +// Creates files and dirs for all in `/root` and creates the directory `/lib` and the file `/lib/extra.js` +// Notes: +// - /lib/library.js is not included +// - Files are lazy-loaded +mock(mock.createDirectoryInfoFromPaths([ '/root', '/lib/extra.js' ])); + +// ------------------------------------------------------------------------------------- + +// Creates `/root` directory and `/root/file1.txt` +// Notes: +// - subdir and its contents are not loaded (due to recursive=false) +// - Files content is loaded into memory immediately (due to lazyLoad=false) +mock(mock.createDirectoryInfoFromPaths([ '/root' ], { recursive: false, lazyLoad: false })); +``` + +### Manually 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). ```js @@ -95,7 +139,7 @@ mock({ Note that if you want to create a file with the default properties, you can provide a `string` or `Buffer` directly instead of calling `mock.file()`. -### Creating directories +### Manually Creating directories When `config` property values are an `Object`, a directory is created. The structure of the object is the same as the `config` object itself. So an empty directory can be created with a simple object literal (`{}`). The following configuration creates a directory containing two files (in addition to the two default directories): ```js @@ -187,6 +231,18 @@ beforeEach(function() { afterEach(mock.restore); ``` +### Bypassing the mock file system + +Sometimes you will want to execute calls against the actual file-system. In order to do that, we've provided a helper. + +### `mock.bypass(fn)` + +```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')); +``` + ## Install Using `npm`: From 135ec12dd388e62294d4396e1e9dbe9c834a4aa8 Mon Sep 17 00:00:00 2001 From: Ron S Date: Mon, 27 Jul 2020 16:56:24 -0400 Subject: [PATCH 12/31] Make automatically created directories inherit stats (permissions, dates, etc) --- lib/filesystem.js | 2 ++ lib/index.js | 23 +++++++++++++++++- lib/item.js | 4 +-- test/lib/index.spec.js | 55 ++++++++++++++++++++++++------------------ 4 files changed, 58 insertions(+), 26 deletions(-) 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 0fe27ff0..433354e7 100644 --- a/lib/index.js +++ b/lib/index.js @@ -17,6 +17,15 @@ const realCreateWriteStream = fs.createWriteStream; const realStats = realBinding.Stats; const realStatWatcher = realBinding.StatWatcher; +// File Permissions +const S_IRUSR = 0o400; +const S_IRGRP = 0o40; +const S_IROTH = 0o4; + +const S_IXUSR = 0o100; +const S_IXGRP = 0o10; +const S_IXOTH = 0o1; + /** * Pre-patch fs binding. * This allows mock-fs to work properly under nodejs v10+ readFile @@ -247,7 +256,19 @@ exports.createDirectoryInfoFromPaths = function(paths, options) { if (stats.isFile()) { addFile(p, stats); } else if ((isRoot || recursive) && stats.isDirectory()) { - res[p] = {}; + const dirStats = Object.assign({}, stats); + // 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 + if (process.platform === 'win32') { + // prettier-ignore + dirStats.mode |= + ((dirStats.mode & S_IRUSR) && S_IXUSR) | + ((dirStats.mode & S_IRGRP) && S_IXGRP) | + ((dirStats.mode & S_IROTH) && S_IXOTH); + } + res[p] = exports.directory(dirStats); fs.readdirSync(p).forEach(subPath => scan(path.join(p, subPath))); } } diff --git a/lib/item.js b/lib/item.js index 019c0529..b8a01acb 100644 --- a/lib/item.js +++ b/lib/item.js @@ -140,8 +140,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/test/lib/index.spec.js b/test/lib/index.spec.js index 8c3aac98..dbeb9d8f 100644 --- a/test/lib/index.spec.js +++ b/test/lib/index.spec.js @@ -6,6 +6,7 @@ const mock = require('../../lib/index'); const os = require('os'); const path = require('path'); const File = require('../../lib/file'); +const Directory = require('../../lib/directory'); const assert = helper.assert; @@ -211,7 +212,7 @@ describe('The API', function() { assert.instanceOf(paths[expectedFile1](), File); assert.instanceOf(paths[expectedFile2](), File); - assert.deepEqual(paths[path.dirname(expectedFile2)], {}); + assert.instanceOf(paths[path.dirname(expectedFile2)](), Directory); }); it('adds from single path', () => { @@ -221,7 +222,7 @@ describe('The API', function() { ); assert.instanceOf(paths[expectedFile](), File); - assert.deepEqual(paths[path.dirname(expectedFile)], {}); + assert.instanceOf(paths[path.dirname(expectedFile)](), Directory); }); it('recursive=false does not go deep', () => { @@ -233,7 +234,7 @@ describe('The API', function() { const keys = Object.keys(paths); assert.lengthOf(keys, 2); assert.instanceOf(paths[expectedFile](), File); - assert.deepEqual(paths[path.dirname(expectedFile)], {}); + assert.instanceOf(paths[path.dirname(expectedFile)](), Directory); }); it('recursive=true loads all files and directories', () => { @@ -250,6 +251,10 @@ describe('The API', function() { const keys = Object.keys(paths); assert.lengthOf(keys, 6); assert.deepEqual(expectedPaths.slice().sort(), keys.slice().sort()); + + expectedPaths.forEach(p => + assert.instanceOf(paths[p](), /\.\w+$/.test(p) ? File : Directory) + ); }); describe('lazyLoad=true', () => { @@ -262,26 +267,30 @@ describe('The API', function() { for (const p of Object.keys(paths)) { if (typeof paths[p] === 'function') { const file = paths[p](); - // Ensure getter was set - assert( - Object.getOwnPropertyDescriptor(file, '_content').hasOwnProperty( - 'get' - ) - ); - - // Wrap factory & getter so we know when it is fired - const originalGetter = Object.getOwnPropertyDescriptor( - file, - '_content' - ).get; - - paths[p] = () => - Object.defineProperty(file, '_content', { - get() { - triggeredGetters.push(p); - return originalGetter.call(this); - } - }); + + if (file instanceof File) { + // Ensure getter was set + assert( + Object.getOwnPropertyDescriptor( + file, + '_content' + ).hasOwnProperty('get') + ); + + // Wrap factory & getter so we know when it is fired + const originalGetter = Object.getOwnPropertyDescriptor( + file, + '_content' + ).get; + + paths[p] = () => + Object.defineProperty(file, '_content', { + get() { + triggeredGetters.push(p); + return originalGetter.call(this); + } + }); + } } } From 61b8ac1af9ff321584bef9b8f0fe835df0679a68 Mon Sep 17 00:00:00 2001 From: Ron S Date: Mon, 27 Jul 2020 17:15:06 -0400 Subject: [PATCH 13/31] Move badge to top of readme --- readme.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/readme.md b/readme.md index bfcd82df..c8e77250 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. @@ -278,6 +280,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 From b195d8e1679a17b6d4d0e30bc7986dd31da5e1e4 Mon Sep 17 00:00:00 2001 From: Ron S Date: Mon, 27 Jul 2020 17:30:05 -0400 Subject: [PATCH 14/31] Fix: Make non-lazy loaded files retain stats --- lib/index.js | 50 +++++++++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/lib/index.js b/lib/index.js index 433354e7..e224fca0 100644 --- a/lib/index.js +++ b/lib/index.js @@ -274,30 +274,34 @@ exports.createDirectoryInfoFromPaths = function(paths, options) { } function addFile(p, stats) { - if (!lazyLoad) { - res[p] = fs.readFileSync(p); - } - - /* Lazy Load */ res[p] = () => { - const file = exports.file(Object.assign({}, stats, {content: ''}))(); - return Object.defineProperty(file, '_content', { - get() { - const res = exports.bypass(() => fs.readFileSync(p)); - Object.defineProperty(file, '_content', { - value: res, - writable: true - }); - return res; - }, - set(data) { - Object.defineProperty(file, '_content', { - value: data, - writable: true - }); - }, - configurable: true - }); + const content = lazyLoad + ? exports.bypass(() => fs.readFileSync(p)) + : ''; + + const file = exports.file(Object.assign({}, stats, {content}))(); + + if (lazyLoad) { + Object.defineProperty(file, '_content', { + get() { + const res = exports.bypass(() => fs.readFileSync(p)); + Object.defineProperty(file, '_content', { + value: res, + writable: true + }); + return res; + }, + set(data) { + Object.defineProperty(file, '_content', { + value: data, + writable: true + }); + }, + configurable: true + }); + } + + return file; }; } }); From 3b58f7993103b421f49f0435b473b618849c0c44 Mon Sep 17 00:00:00 2001 From: Ron S Date: Mon, 27 Jul 2020 19:08:30 -0400 Subject: [PATCH 15/31] Prefer existing permissions const --- lib/index.js | 16 ++++------------ lib/item.js | 2 ++ 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/lib/index.js b/lib/index.js index e224fca0..b54cf5dd 100644 --- a/lib/index.js +++ b/lib/index.js @@ -6,6 +6,7 @@ const FileSystem = require('./filesystem'); const realBinding = process.binding('fs'); const path = require('path'); const fs = require('fs'); +const {permissions} = require('./item'); const toNamespacedPath = FileSystem.toNamespacedPath; @@ -17,15 +18,6 @@ const realCreateWriteStream = fs.createWriteStream; const realStats = realBinding.Stats; const realStatWatcher = realBinding.StatWatcher; -// File Permissions -const S_IRUSR = 0o400; -const S_IRGRP = 0o40; -const S_IROTH = 0o4; - -const S_IXUSR = 0o100; -const S_IXGRP = 0o10; -const S_IXOTH = 0o1; - /** * Pre-patch fs binding. * This allows mock-fs to work properly under nodejs v10+ readFile @@ -264,9 +256,9 @@ exports.createDirectoryInfoFromPaths = function(paths, options) { if (process.platform === 'win32') { // prettier-ignore dirStats.mode |= - ((dirStats.mode & S_IRUSR) && S_IXUSR) | - ((dirStats.mode & S_IRGRP) && S_IXGRP) | - ((dirStats.mode & S_IROTH) && S_IXOTH); + ((dirStats.mode & permissions.USER_READ) && permissions.USER_EXEC) | + ((dirStats.mode & permissions.GROUP_READ) && permissions.GROUP_EXEC) | + ((dirStats.mode & permissions.OTHER_READ) && permissions.OTHER_EXEC); } res[p] = exports.directory(dirStats); fs.readdirSync(p).forEach(subPath => scan(path.join(p, subPath))); diff --git a/lib/item.js b/lib/item.js index b8a01acb..e9c034cf 100644 --- a/lib/item.js +++ b/lib/item.js @@ -88,6 +88,8 @@ function Item() { this.links = 0; } +Item.permissions = permissions; + /** * Determine if the current user has read permission. * @return {boolean} The current user can read. From 6950d394e40ab3f20124d96c444db4e1ef6e4c8f Mon Sep 17 00:00:00 2001 From: Ron S Date: Tue, 28 Jul 2020 00:05:56 -0400 Subject: [PATCH 16/31] Re-engineered API - Added mock.mapFile(), mock.mapDir(), and mock.mapPaths() - Created improved tests for the new methods (comparing stats with real files, etc) --- lib/index.js | 227 ++++++++++++++++++++---------- lib/item.js | 11 +- test/lib/index.spec.js | 312 +++++++++++++++++++++++++---------------- 3 files changed, 355 insertions(+), 195 deletions(-) diff --git a/lib/index.js b/lib/index.js index b54cf5dd..0546f320 100644 --- a/lib/index.js +++ b/lib/index.js @@ -6,7 +6,7 @@ const FileSystem = require('./filesystem'); const realBinding = process.binding('fs'); const path = require('path'); const fs = require('fs'); -const {permissions} = require('./item'); +const {fixWin32Permissions} = require('./item'); const toNamespacedPath = FileSystem.toNamespacedPath; @@ -210,91 +210,174 @@ exports.bypass = function(fn) { return res; }; +/* ****************************************************************************************************************** * + * Mapping + * ****************************************************************************************************************** */ + +const mapping = {}; + +mapping.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 + ); + +mapping.addFile = function(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.win32.basename(target); + + output[outputPropKey] = () => { + const content = !lazyLoad ? fs.readFileSync(target) : ''; + const file = exports.file(Object.assign({}, stats, {content}))(); + + if (lazyLoad) { + Object.defineProperty(file, '_content', { + get() { + const res = exports.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]; +}; + +mapping.addDir = function(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.win32.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] = exports.directory( + Object.assign(stats, {items: directoryItems}) + ); + + fs.readdirSync(target).forEach(p => { + const absPath = path.join(target, p); + const stats = fs.statSync(absPath); + const newContext = mapping.createContext(context, { + target: absPath, + output: directoryItems + }); + + if (recursive && stats.isDirectory()) { + mapping.addDir(newContext, stats); + } else if (stats.isFile()) { + mapping.addFile(newContext, stats); + } + }); + + return output[outputPropKey]; +}; + +mapping.fixupPath = p => { + if (typeof p !== 'string') { + throw new TypeError(`Invalid path. All paths must be strings`); + } + return path.resolve(p); +}; + +/* ********************************************************* * + * Exported Methods + * ********************************************************* */ + /** - * Populate a DirectoryItems object to use with mock() from a series of paths to files/directories + * Automatically maps specified paths (for use with `mock()`) */ -exports.createDirectoryInfoFromPaths = function(paths, options) { +exports.mapPaths = function(paths, options) { return exports.bypass(() => { const res = {}; + const context = mapping.createContext({output: res, options}); - /* Get options or apply defaults */ - let recursive = options && options.recursive; - let lazyLoad = options && options.lazyLoad; - if (recursive === undefined) { - recursive = true; - } - if (lazyLoad === undefined) { - lazyLoad = true; - } + const addPath = p => { + const absPath = mapping.fixupPath(p); + const stats = fs.statSync(absPath); + const newContext = mapping.createContext(context, {target: absPath}); + + if (stats.isDirectory()) { + mapping.addDir(newContext, stats, true); + } else if (stats.isFile()) { + mapping.addFile(newContext, stats, true); + } + }; if (Array.isArray(paths)) { - paths.forEach(p => scan(p, true)); + paths.forEach(addPath); } else { - scan(paths, true); + addPath(paths); } return res; + }); +}; - function scan(p, isRoot) { - if (typeof p !== 'string') { - throw new Error( - `Must provide path or array of paths (as strings) to createDirectoryInfoFromPaths()` - ); - } - - p = path.normalize(p); - - const stats = fs.statSync(p); - if (stats.isFile()) { - addFile(p, stats); - } else if ((isRoot || recursive) && stats.isDirectory()) { - const dirStats = Object.assign({}, stats); - // 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 - if (process.platform === 'win32') { - // prettier-ignore - dirStats.mode |= - ((dirStats.mode & permissions.USER_READ) && permissions.USER_EXEC) | - ((dirStats.mode & permissions.GROUP_READ) && permissions.GROUP_EXEC) | - ((dirStats.mode & permissions.OTHER_READ) && permissions.OTHER_EXEC); - } - res[p] = exports.directory(dirStats); - fs.readdirSync(p).forEach(subPath => scan(path.join(p, subPath))); - } - } - - function addFile(p, stats) { - res[p] = () => { - const content = lazyLoad - ? exports.bypass(() => fs.readFileSync(p)) - : ''; +/** + * Maps specific directory (for use with `mock()`) + */ +exports.mapDir = function(dir, options) { + return exports.bypass(() => { + dir = mapping.fixupPath(dir); - const file = exports.file(Object.assign({}, stats, {content}))(); + return mapping.addDir( + mapping.createContext({output: {}, options, target: dir}), + fs.statSync(dir), + true + ); + }); +}; - if (lazyLoad) { - Object.defineProperty(file, '_content', { - get() { - const res = exports.bypass(() => fs.readFileSync(p)); - Object.defineProperty(file, '_content', { - value: res, - writable: true - }); - return res; - }, - set(data) { - Object.defineProperty(file, '_content', { - value: data, - writable: true - }); - }, - configurable: true - }); - } +/** + * Maps specific file (for use with `mock()`) + */ +exports.mapFile = function(file, options) { + return exports.bypass(() => { + file = mapping.fixupPath(file); - return file; - }; - } + return mapping.addFile( + mapping.createContext({output: {}, options, target: file}), + fs.statSync(file), + true + ); }); }; diff --git a/lib/item.js b/lib/item.js index e9c034cf..252f5c32 100644 --- a/lib/item.js +++ b/lib/item.js @@ -88,7 +88,16 @@ function Item() { this.links = 0; } -Item.permissions = permissions; +/** + * Add execute if read allowed + * See notes in index.js -> mapping#addDir + */ +Item.fixWin32Permissions = mode => + // prettier-ignore + 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. diff --git a/test/lib/index.spec.js b/test/lib/index.spec.js index dbeb9d8f..2598d4a1 100644 --- a/test/lib/index.spec.js +++ b/test/lib/index.spec.js @@ -6,9 +6,11 @@ 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 assert = helper.assert; +const assetsPath = path.resolve(__dirname, '../assets'); describe('The API', function() { describe('mock()', function() { @@ -194,140 +196,206 @@ describe('The API', function() { }); }); - describe(`mock.createDirectoryInfoFromPaths()`, () => { - const assetsPath = path.resolve(__dirname, '../assets'); - - it('throws with non-string paths', () => { - assert.throws(() => mock.createDirectoryInfoFromPaths(null)); - assert.throws(() => mock.createDirectoryInfoFromPaths(['a', null])); - }); - - it('adds from multiple paths', () => { - const expectedFile1 = path.join(assetsPath, 'file1.txt'); - const expectedFile2 = path.join(assetsPath, 'dir/subdir/file3.txt'); - const paths = mock.createDirectoryInfoFromPaths([ - expectedFile1, - path.dirname(expectedFile2) - ]); - - assert.instanceOf(paths[expectedFile1](), File); - assert.instanceOf(paths[expectedFile2](), File); - assert.instanceOf(paths[path.dirname(expectedFile2)](), Directory); - }); + describe(`Mapping functions`, () => { + 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(`mock.mapFile()`, () => { + const filePath = path.join(assetsPath, 'file1.txt'); + + it('throws with non-string path', () => + assert.throws(() => mock.mapFile(null))); + it('throws with directory', () => + assert.throws(() => mock.mapFile(path.join(assetsPath)))); + it('creates a File factory with correct attributes', () => { + const file = mock.mapFile(filePath)(); + const stats = fs.statSync(filePath); + + assert.instanceOf(file, File); + assert.deepEqual(filterStats(file), filterStats(stats)); + }); + describe('lazyLoad=true', () => { + let file; + beforeEach(() => (file = mock.mapFile(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('adds from single path', () => { - const expectedFile = path.join(assetsPath, 'dir/subdir/file3.txt'); - const paths = mock.createDirectoryInfoFromPaths( - path.dirname(expectedFile) - ); + it('lazyLoad=false loads file content', () => { + const file = mock.mapFile(path.join(assetsPath, 'file1.txt'), { + lazyLoad: false + })(); - assert.instanceOf(paths[expectedFile](), File); - assert.instanceOf(paths[path.dirname(expectedFile)](), Directory); + assert.equal( + Object.getOwnPropertyDescriptor(file, '_content').value.toString(), + 'data1' + ); + }); }); - it('recursive=false does not go deep', () => { - const expectedFile = path.join(assetsPath, 'file1.txt'); - const paths = mock.createDirectoryInfoFromPaths(assetsPath, { - recursive: false + describe(`mock.mapDir()`, () => { + it('throws with non-string path', () => + assert.throws(() => mock.mapDir(null))); + it('throws with file', () => + assert.throws(() => mock.mapDir(path.join(assetsPath, 'file1.txt')))); + it('creates a Directory factory with correct attributes', () => { + const dir = mock.mapDir(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.mapDir(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.mapDir(assetsPath, {recursive: true, lazyLoad: true})(); + assert.typeOf( + Object.getOwnPropertyDescriptor(getFile(), '_content').get, + 'function' + ); + + dir = mock.mapDir(assetsPath, {recursive: true, lazyLoad: false})(); + assert.instanceOf( + Object.getOwnPropertyDescriptor(getFile(), '_content').value, + Buffer + ); + }); }); - const keys = Object.keys(paths); - assert.lengthOf(keys, 2); - assert.instanceOf(paths[expectedFile](), File); - assert.instanceOf(paths[path.dirname(expectedFile)](), Directory); - }); - - it('recursive=true loads all files and directories', () => { - const paths = mock.createDirectoryInfoFromPaths(assetsPath); - const expectedPaths = [ - '', - 'file1.txt', - 'dir', - 'dir/file2.txt', - 'dir/subdir', - 'dir/subdir/file3.txt' - ].map(p => path.join(assetsPath, p)); - - const keys = Object.keys(paths); - assert.lengthOf(keys, 6); - assert.deepEqual(expectedPaths.slice().sort(), keys.slice().sort()); - - expectedPaths.forEach(p => - assert.instanceOf(paths[p](), /\.\w+$/.test(p) ? File : Directory) - ); + it('recursive=false creates files & does not recurse', () => { + const base = mock.mapDir(assetsPath, {recursive: false})(); + assert.instanceOf(base, Directory); + assert.instanceOf(base._items['file1.txt'], File); + assert.isNotOk(base._items.dir); + }); }); - describe('lazyLoad=true', () => { - let paths; - const triggeredGetters = []; - - beforeEach(() => { - paths = mock.createDirectoryInfoFromPaths(assetsPath, {lazyLoad: true}); - - for (const p of Object.keys(paths)) { - if (typeof paths[p] === 'function') { - const file = paths[p](); - - if (file instanceof File) { - // Ensure getter was set - assert( - Object.getOwnPropertyDescriptor( - file, - '_content' - ).hasOwnProperty('get') - ); - - // Wrap factory & getter so we know when it is fired - const originalGetter = Object.getOwnPropertyDescriptor( - file, - '_content' - ).get; - - paths[p] = () => - Object.defineProperty(file, '_content', { - get() { - triggeredGetters.push(p); - return originalGetter.call(this); - } - }); - } - } - } - - mock(paths); + describe(`mock.mapPaths()`, () => { + it('throws with non-string path', () => { + assert.throws(() => mock.mapDir(null)); + assert.throws(() => mock.mapDir([null])); }); - afterEach(() => { - mock.restore(); - triggeredGetters.splice(0, triggeredGetters.length); + it('maps multiple paths', () => { + const filePath1 = path.join(assetsPath, 'file1.txt'); + const filePath2 = path.join(assetsPath, '/dir/file2.txt'); + const res = mock.mapPaths([filePath1, filePath2]); + assert.instanceOf(res[filePath1](), File); + assert.instanceOf(res[filePath2](), File); }); - - it("can write to lazy-loaded file before it's read", () => { + it('maps single path', () => { + const filePath1 = path.join(assetsPath, 'file1.txt'); + const res = mock.mapPaths(filePath1); + assert.instanceOf(res[filePath1](), File); + }); + it('respects lazyLoad setting', () => { + let res; const filePath = path.join(assetsPath, 'file1.txt'); - fs.writeFileSync(filePath, 'new data'); - assert.equal(fs.readFileSync(filePath, 'utf8'), 'new data'); + + res = mock.mapPaths(filePath, {lazyLoad: true}); + assert.typeOf( + Object.getOwnPropertyDescriptor(res[filePath](), '_content').get, + 'function' + ); + + res = mock.mapPaths(filePath, {lazyLoad: false}); + assert.instanceOf( + Object.getOwnPropertyDescriptor(res[filePath](), '_content').value, + Buffer + ); }); - it('waits to load files', () => assert.lengthOf(triggeredGetters, 0)); - it('loads proper data', () => { - const expectedFile1 = path.join(assetsPath, 'file1.txt'); - const expectedFile2 = path.join(assetsPath, 'dir/file2.txt'); - const expectedFile3 = path.join(assetsPath, 'dir/subdir/file3.txt'); - - const res1 = fs.readFileSync(expectedFile1, 'utf8'); - const res2 = fs.readFileSync(expectedFile2, 'utf8'); - const res3 = fs.readFileSync(expectedFile3, 'utf8'); - // Triggering a duplicate read to determine getter was replaced. - // If it wasn't, triggeredGetters array will have an extra expectedFile2 - fs.readFileSync(expectedFile2, 'utf8'); - - assert.equal(res1, 'data1'); - assert.equal(res2, 'data2'); - assert.equal(res3, 'data3'); - assert.deepEqual(triggeredGetters, [ - expectedFile1, - expectedFile2, - expectedFile3 - ]); - assert.lengthOf(triggeredGetters, 3); + it('recursive=true loads recursively', () => { + const dirPath = path.join(assetsPath, 'dir'); + const filePath = path.join(assetsPath, 'file1.txt'); + const res = mock.mapPaths([dirPath, filePath], {recursive: true}); + + const dir = res[dirPath](); + const dirSubdir = dir._items.subdir; + + assert.instanceOf(res[filePath](), File); + assert.instanceOf(dir, Directory); + assert.instanceOf(dir._items['file2.txt'], File); + assert.instanceOf(dirSubdir, Directory); + assert.instanceOf(dirSubdir._items['file3.txt'], File); }); }); }); From 9f284af9fda65842b410d5cd4a08e66839e840d1 Mon Sep 17 00:00:00 2001 From: Ron S Date: Tue, 28 Jul 2020 01:03:05 -0400 Subject: [PATCH 17/31] Update readme --- readme.md | 88 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 54 insertions(+), 34 deletions(-) diff --git a/readme.md b/readme.md index c8e77250..5465e8c2 100644 --- a/readme.md +++ b/readme.md @@ -60,51 +60,71 @@ 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. -### Automatically Creating Files & Directories +### Mapping real files & directories -You can create files and directories automatically by providing paths to `mock.createDirectoryInfoFromPaths()`. This will -read the filesystem for the paths you provide and automatically create files and directories for them. +If you want to add files from your real filesystem to the mocked FS, there are several mapping methods to help. -### `mock.createDirectoryInfoFromPaths(paths, options)` +#### Notes -#### `paths` - `string` or `string[]` +- Each mapper function duplicates all stat information (dates, permissions, etc) +- By default, all files are lazy-loaded, unless you specify the `{ lazyLoad: false }` option, which makes it reasonable +to expose large areas of the filesystem to your mocked FS. -#### `options` +#### options -The second (optional) argument may include the properties below. - - * `lazyLoad` - `boolean` File content does not get loaded until explicitly read. This is `true` by default. - * `recursive` - `boolean` Load all files and directories recursively. This is `true` by default. +| Option | Type | Default | Description | +| --------- | ------- | ------- | ------------ +| lazyLoad | boolean | true | File content isn't loaded until explicitly read +| recursive | boolean | true | Load all files and directories recursively (applies to `mock.mapDir()` & `mock.mapPaths()`) -#### Examples +#### `mock.mapPaths(path, options)` + +Quickly map one or more paths. + +_Note: Mapping a path makes its absolute path available in the Mock Filesystem. It is a good way to 'mount' sections of your +real filesystem, without allowing changes to affect the real files._ + +```js +// Mock FS, with single dir +mock(mock.mapPaths('/path/to/dir')); -Given the following directory structure +// Mock FS, with two dirs and a file (do not recurse and pre-load all content) +mock(mock.mapPaths([ '/path/to/dir', '/path/to/anotherdir', '/srv/some-file.txt' ], { recursive: false, lazyLoad: false })); ``` -- /root/ - - subdir/ - - file2.txt - - file1.txt -- /lib/ - - library.js - - extra.js + +#### `mock.mapDir(dirPath, options)` + +Create a `Directory` for a real dir. + +```js +mock({ + // Recursively loads all node_modules + 'node_modules': mock.mapDir(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.mapDir('/tmp/special_tmp_files', { recursive: false, lazyLoad:false }), + + 'fakefile.txt': 'content here' +}); ``` + +#### `mock.mapFile(filePath, options)` + +Create a `File` for a real file. + ```js -// Creates files and dirs for all in `/root` and creates the directory `/lib` and the file `/lib/extra.js` -// Notes: -// - /lib/library.js is not included -// - Files are lazy-loaded -mock(mock.createDirectoryInfoFromPaths([ '/root', '/lib/extra.js' ])); - -// ------------------------------------------------------------------------------------- - -// Creates `/root` directory and `/root/file1.txt` -// Notes: -// - subdir and its contents are not loaded (due to recursive=false) -// - Files content is loaded into memory immediately (due to lazyLoad=false) -mock(mock.createDirectoryInfoFromPaths([ '/root' ], { recursive: false, lazyLoad: false })); +mock({ + // Lazy-load file + 'my-file.txt': mock.mapDir(path.resolve(__dirname, 'assets/special-file.txt')), + + // Pre-load js file + 'ready.js': mock.mapDir(path.resolve(__dirname, 'scripts/ready.js'), { lazyLoad: false }), + + 'fakefile.txt': 'content here' +}); ``` -### Manually Creating files +### 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). ```js @@ -141,7 +161,7 @@ mock({ Note that if you want to create a file with the default properties, you can provide a `string` or `Buffer` directly instead of calling `mock.file()`. -### Manually Creating directories +### Creating directories When `config` property values are an `Object`, a directory is created. The structure of the object is the same as the `config` object itself. So an empty directory can be created with a simple object literal (`{}`). The following configuration creates a directory containing two files (in addition to the two default directories): ```js From 562c6aedc789cfcf6cc397792cc78b54a73ef1fb Mon Sep 17 00:00:00 2001 From: Ron S Date: Tue, 28 Jul 2020 01:18:23 -0400 Subject: [PATCH 18/31] Added tests for integration with mock() & fs.readFileSync Just to be safe --- test/lib/index.spec.js | 39 +++++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/test/lib/index.spec.js b/test/lib/index.spec.js index 2598d4a1..e1dc71d6 100644 --- a/test/lib/index.spec.js +++ b/test/lib/index.spec.js @@ -226,10 +226,12 @@ describe('The API', function() { describe(`mock.mapFile()`, () => { const filePath = path.join(assetsPath, 'file1.txt'); - it('throws with non-string path', () => - assert.throws(() => mock.mapFile(null))); - it('throws with directory', () => - assert.throws(() => mock.mapFile(path.join(assetsPath)))); + it('throws with non-string path', () => { + assert.throws(() => mock.mapFile(null)); + }); + it('throws with directory', () => { + assert.throws(() => mock.mapFile(path.join(assetsPath))); + }); it('creates a File factory with correct attributes', () => { const file = mock.mapFile(filePath)(); const stats = fs.statSync(filePath); @@ -296,13 +298,21 @@ describe('The API', function() { 'data1' ); }); + + it('can read file from mocked FS', () => { + mock({'/file': mock.mapFile(filePath)}); + assert.equal(fs.readFileSync('/file'), 'data1'); + mock.restore(); + }); }); describe(`mock.mapDir()`, () => { - it('throws with non-string path', () => - assert.throws(() => mock.mapDir(null))); - it('throws with file', () => - assert.throws(() => mock.mapDir(path.join(assetsPath, 'file1.txt')))); + it('throws with non-string path', () => { + assert.throws(() => mock.mapDir(null)); + }); + it('throws with file', () => { + assert.throws(() => mock.mapDir(path.join(assetsPath, 'file1.txt'))); + }); it('creates a Directory factory with correct attributes', () => { const dir = mock.mapDir(assetsPath)(); const stats = fs.statSync(assetsPath); @@ -348,6 +358,12 @@ describe('The API', function() { assert.instanceOf(base._items['file1.txt'], File); assert.isNotOk(base._items.dir); }); + + it('can read file from mocked FS', () => { + mock({'/dir': mock.mapDir(assetsPath, {recursive: true})}); + assert.equal(fs.readFileSync('/dir/file1.txt'), 'data1'); + mock.restore(); + }); }); describe(`mock.mapPaths()`, () => { @@ -398,6 +414,13 @@ describe('The API', function() { assert.instanceOf(dirSubdir._items['file3.txt'], File); }); }); + it('can read file from mocked FS', () => { + const filePath1 = path.join(assetsPath, 'file1.txt'); + const filePath2 = path.join(assetsPath, '/dir/file2.txt'); + mock(mock.mapPaths([filePath1, filePath2])); + assert.equal(fs.readFileSync(filePath2), 'data2'); + mock.restore(); + }); }); xdescribe('mock.fs()', function() { From 2ca7d0e925af9fa2de43bdd20f0ebb5dfbeb9b07 Mon Sep 17 00:00:00 2001 From: Ron S Date: Tue, 28 Jul 2020 01:26:12 -0400 Subject: [PATCH 19/31] Correct typo --- readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 5465e8c2..dfdaddca 100644 --- a/readme.md +++ b/readme.md @@ -115,10 +115,10 @@ Create a `File` for a real file. ```js mock({ // Lazy-load file - 'my-file.txt': mock.mapDir(path.resolve(__dirname, 'assets/special-file.txt')), + 'my-file.txt': mock.mapFile(path.resolve(__dirname, 'assets/special-file.txt')), // Pre-load js file - 'ready.js': mock.mapDir(path.resolve(__dirname, 'scripts/ready.js'), { lazyLoad: false }), + 'ready.js': mock.mapFile(path.resolve(__dirname, 'scripts/ready.js'), { lazyLoad: false }), 'fakefile.txt': 'content here' }); From 10f61daf0b01686502603e039e3877b65ebfa4ab Mon Sep 17 00:00:00 2001 From: Ron S Date: Tue, 28 Jul 2020 01:30:52 -0400 Subject: [PATCH 20/31] Fix: fixWin32Permissions missing platform check --- lib/item.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/item.js b/lib/item.js index 252f5c32..1ec3e75d 100644 --- a/lib/item.js +++ b/lib/item.js @@ -92,12 +92,14 @@ function Item() { * Add execute if read allowed * See notes in index.js -> mapping#addDir */ +// prettier-ignore Item.fixWin32Permissions = mode => - // prettier-ignore - mode | - ((mode & permissions.USER_READ) && permissions.USER_EXEC) | - ((mode & permissions.GROUP_READ) && permissions.GROUP_EXEC) | - ((mode & permissions.OTHER_READ) && permissions.OTHER_EXEC); + (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. From 92ca9c80858bf57a9e3bac73ce3d17413d27c0ea Mon Sep 17 00:00:00 2001 From: Ron S Date: Tue, 28 Jul 2020 13:09:22 -0400 Subject: [PATCH 21/31] Address 3cp's comments --- lib/index.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/index.js b/lib/index.js index 0546f320..7d85c953 100644 --- a/lib/index.js +++ b/lib/index.js @@ -198,14 +198,17 @@ exports.bypass = function(fn) { } // Deactivate mocked bindings - const binding = process.binding('fs')._mockedBinding; - delete process.binding('fs')._mockedBinding; - - // Perform action - const res = fn(); + const binding = realBinding._mockedBinding; + delete realBinding._mockedBinding; - // Reactivate mocked bindings - process.binding('fs')._mockedBinding = binding; + let res; + try { + // Perform action + res = fn(); + } finally { + // Reactivate mocked bindings + realBinding._mockedBinding = binding; + } return res; }; From 172e698012ab3ec5f9f753647c1d4b7efaf85535 Mon Sep 17 00:00:00 2001 From: Ron S Date: Tue, 28 Jul 2020 22:39:33 -0400 Subject: [PATCH 22/31] Clarified bypassing methods & added restoration test --- lib/index.js | 35 ++++++++++++++++++++++++++++++----- readme.md | 24 ++++++++++++++++++++++-- test/lib/index.spec.js | 8 ++++++++ 3 files changed, 60 insertions(+), 7 deletions(-) diff --git a/lib/index.js b/lib/index.js index 7d85c953..d249e70c 100644 --- a/lib/index.js +++ b/lib/index.js @@ -185,6 +185,26 @@ exports.directory = FileSystem.directory; */ exports.symlink = FileSystem.symlink; +let storedBinding; + +/** + * Temporarily disable Mocked FS + */ +exports.disable = () => { + storedBinding = realBinding._mockedBinding; + delete realBinding._mockedBinding; +}; + +/** + * Enables Mocked FS after being disabled by mock.disable() + */ +exports.enable = () => { + if (storedBinding) { + realBinding._mockedBinding = storedBinding; + storedBinding = undefined; + } +}; + /** * Perform action, bypassing mock FS * @example @@ -198,16 +218,21 @@ exports.bypass = function(fn) { } // Deactivate mocked bindings - const binding = realBinding._mockedBinding; + exports.disable(); delete realBinding._mockedBinding; let res; try { - // Perform action - res = fn(); + res = fn(); // Perform action } finally { - // Reactivate mocked bindings - realBinding._mockedBinding = binding; + exports.enable(); + } + + if (res.then) { + // eslint-disable-next-line no-console + console.warn( + `Async functions are not supported with exports.bypass(). See https://github.com/tschaub/mock-fs/#advancedbypass` + ); } return res; diff --git a/readme.md b/readme.md index dfdaddca..30aff21b 100644 --- a/readme.md +++ b/readme.md @@ -255,9 +255,9 @@ afterEach(mock.restore); ### Bypassing the mock file system -Sometimes you will want to execute calls against the actual file-system. In order to do that, we've provided a helper. +#### `mock.bypass(fn)` -### `mock.bypass(fn)` +Execute _synchronous calls_ to the real filesystem with mock.bypass() ```js // This file exists only on the real FS, not on the mocked FS @@ -265,6 +265,26 @@ const realFilePath = '/path/to/real/file.txt'; const myData = mock.bypass(() => fs.readFileSync(realFilePath, 'utf-8')); ``` +#### Advanced Bypassing + +Asynchronous calls are not recommended as they could produce unintended consequences if anything else tries to access the +mocked filesystem before they've completed. + +However, if you know what you're doing, you can selectively disable and re-enable the mock filesystem. + +```js +async function getFileInfo(fileName) { + mock.disable(); + try { + const stats = await fs.promises.stat(fileName); + const data = await fs.promises.readFile(fileName); + return { stats, data }; + } finally { + mock.enable(); + } +} +``` + ## Install Using `npm`: diff --git a/test/lib/index.spec.js b/test/lib/index.spec.js index e1dc71d6..5045ab51 100644 --- a/test/lib/index.spec.js +++ b/test/lib/index.spec.js @@ -194,6 +194,14 @@ describe('The API', function() { assert.throws(() => fs.readFileSync(__filename)); assert.doesNotThrow(() => mock.bypass(() => fs.readFileSync(__filename))); }); + + it('restores mock FS after bypass', () => { + mock({'/path/to/file': 'content'}); + + assert.throws(() => fs.readFileSync(__filename)); + assert.doesNotThrow(() => mock.bypass(() => fs.readFileSync(__filename))); + assert.throws(() => fs.readFileSync(__filename)); + }); }); describe(`Mapping functions`, () => { From 9087580e6cf971819bff9f9904c68c94c3451e02 Mon Sep 17 00:00:00 2001 From: Ron S Date: Tue, 28 Jul 2020 22:50:24 -0400 Subject: [PATCH 23/31] Update mock.bypass test Addresses https://github.com/tschaub/mock-fs/pull/304/commits/172e698012ab3ec5f9f753647c1d4b7efaf85535#r461961591 --- test/lib/index.spec.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/lib/index.spec.js b/test/lib/index.spec.js index 5045ab51..ed8323e3 100644 --- a/test/lib/index.spec.js +++ b/test/lib/index.spec.js @@ -191,16 +191,16 @@ describe('The API', function() { mock({'/path/to/file': 'content'}); assert.equal(fs.readFileSync('/path/to/file', 'utf8'), 'content'); - assert.throws(() => fs.readFileSync(__filename)); - assert.doesNotThrow(() => mock.bypass(() => fs.readFileSync(__filename))); + assert.isNotOk(fs.existsSync(__filename)); + assert.isOk(mock.bypass(() => fs.existsSync(__filename))); }); it('restores mock FS after bypass', () => { - mock({'/path/to/file': 'content'}); + mock({}); - assert.throws(() => fs.readFileSync(__filename)); - assert.doesNotThrow(() => mock.bypass(() => fs.readFileSync(__filename))); - assert.throws(() => fs.readFileSync(__filename)); + assert.isNotOk(fs.existsSync(__filename)); + assert.isOk(mock.bypass(() => fs.existsSync(__filename))); + assert.isNotOk(fs.existsSync(__filename)); }); }); From 0c07e577e2846e51c6267f69499cdd118f3384c4 Mon Sep 17 00:00:00 2001 From: Ron S Date: Tue, 28 Jul 2020 23:16:08 -0400 Subject: [PATCH 24/31] Moved mapping helpers to outside module Addresses https://github.com/tschaub/mock-fs/pull/304#discussion_r461959380 --- lib/index.js | 114 +------------------------------------------------ lib/mapping.js | 105 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 113 deletions(-) create mode 100755 lib/mapping.js diff --git a/lib/index.js b/lib/index.js index d249e70c..e950f841 100644 --- a/lib/index.js +++ b/lib/index.js @@ -6,7 +6,7 @@ const FileSystem = require('./filesystem'); const realBinding = process.binding('fs'); const path = require('path'); const fs = require('fs'); -const {fixWin32Permissions} = require('./item'); +const mapping = require('./mapping'); const toNamespacedPath = FileSystem.toNamespacedPath; @@ -238,118 +238,6 @@ exports.bypass = function(fn) { return res; }; -/* ****************************************************************************************************************** * - * Mapping - * ****************************************************************************************************************** */ - -const mapping = {}; - -mapping.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 - ); - -mapping.addFile = function(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.win32.basename(target); - - output[outputPropKey] = () => { - const content = !lazyLoad ? fs.readFileSync(target) : ''; - const file = exports.file(Object.assign({}, stats, {content}))(); - - if (lazyLoad) { - Object.defineProperty(file, '_content', { - get() { - const res = exports.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]; -}; - -mapping.addDir = function(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.win32.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] = exports.directory( - Object.assign(stats, {items: directoryItems}) - ); - - fs.readdirSync(target).forEach(p => { - const absPath = path.join(target, p); - const stats = fs.statSync(absPath); - const newContext = mapping.createContext(context, { - target: absPath, - output: directoryItems - }); - - if (recursive && stats.isDirectory()) { - mapping.addDir(newContext, stats); - } else if (stats.isFile()) { - mapping.addFile(newContext, stats); - } - }); - - return output[outputPropKey]; -}; - -mapping.fixupPath = p => { - if (typeof p !== 'string') { - throw new TypeError(`Invalid path. All paths must be strings`); - } - return path.resolve(p); -}; - -/* ********************************************************* * - * Exported Methods - * ********************************************************* */ - /** * Automatically maps specified paths (for use with `mock()`) */ diff --git a/lib/mapping.js b/lib/mapping.js new file mode 100755 index 00000000..98650e1c --- /dev/null +++ b/lib/mapping.js @@ -0,0 +1,105 @@ +const {fixWin32Permissions} = require('./item'); +const path = require('path'); +const fs = require('fs'); + +exports.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 + ); + +exports.addFile = function(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.win32.basename(target); + + output[outputPropKey] = () => { + const content = !lazyLoad ? fs.readFileSync(target) : ''; + const file = exports.file(Object.assign({}, stats, {content}))(); + + if (lazyLoad) { + Object.defineProperty(file, '_content', { + get() { + const res = exports.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]; +}; + +exports.addDir = function(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.win32.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] = exports.directory( + Object.assign(stats, {items: directoryItems}) + ); + + fs.readdirSync(target).forEach(p => { + const absPath = path.join(target, p); + const stats = fs.statSync(absPath); + const newContext = exports.createContext(context, { + target: absPath, + output: directoryItems + }); + + if (recursive && stats.isDirectory()) { + exports.addDir(newContext, stats); + } else if (stats.isFile()) { + exports.addFile(newContext, stats); + } + }); + + return output[outputPropKey]; +}; + +exports.fixupPath = p => { + if (typeof p !== 'string') { + throw new TypeError(`Invalid path. All paths must be strings`); + } + return path.resolve(p); +}; From 8e11bec7ec6ed73c91e69f0f978d9e520a1e04ba Mon Sep 17 00:00:00 2001 From: Ron S Date: Tue, 28 Jul 2020 23:28:56 -0400 Subject: [PATCH 25/31] Fix circular issue --- lib/index.js | 3 ++- lib/mapping.js | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/index.js b/lib/index.js index e950f841..23567722 100644 --- a/lib/index.js +++ b/lib/index.js @@ -6,7 +6,6 @@ const FileSystem = require('./filesystem'); const realBinding = process.binding('fs'); const path = require('path'); const fs = require('fs'); -const mapping = require('./mapping'); const toNamespacedPath = FileSystem.toNamespacedPath; @@ -238,6 +237,8 @@ exports.bypass = function(fn) { return res; }; +const mapping = require('./mapping'); + /** * Automatically maps specified paths (for use with `mock()`) */ diff --git a/lib/mapping.js b/lib/mapping.js index 98650e1c..3f390e68 100755 --- a/lib/mapping.js +++ b/lib/mapping.js @@ -1,6 +1,7 @@ const {fixWin32Permissions} = require('./item'); const path = require('path'); const fs = require('fs'); +const mock = require('./index'); exports.createContext = ({output, options = {}, target}, newContext) => Object.assign( @@ -28,12 +29,12 @@ exports.addFile = function(context, stats, isRoot) { output[outputPropKey] = () => { const content = !lazyLoad ? fs.readFileSync(target) : ''; - const file = exports.file(Object.assign({}, stats, {content}))(); + const file = mock.file(Object.assign({}, stats, {content}))(); if (lazyLoad) { Object.defineProperty(file, '_content', { get() { - const res = exports.bypass(() => fs.readFileSync(target)); + const res = mock.bypass(() => fs.readFileSync(target)); Object.defineProperty(file, '_content', { value: res, writable: true @@ -75,7 +76,7 @@ exports.addDir = function(context, stats, isRoot) { // Create directory factory const directoryItems = {}; - output[outputPropKey] = exports.directory( + output[outputPropKey] = mock.directory( Object.assign(stats, {items: directoryItems}) ); From 4b579c0373ee1a8d5b3b478465015fcc099be877 Mon Sep 17 00:00:00 2001 From: Ron S Date: Tue, 28 Jul 2020 23:50:54 -0400 Subject: [PATCH 26/31] Made modules according to repo convention Addresses https://github.com/tschaub/mock-fs/pull/304#pullrequestreview-457106373 --- lib/bypass.js | 53 ++++++++++++++++++++++++ lib/index.js | 109 +++++++------------------------------------------ lib/mapping.js | 87 +++++++++++++++++++++++++++++++++------ 3 files changed, 142 insertions(+), 107 deletions(-) create mode 100755 lib/bypass.js diff --git a/lib/bypass.js b/lib/bypass.js new file mode 100755 index 00000000..cb6c60ce --- /dev/null +++ b/lib/bypass.js @@ -0,0 +1,53 @@ +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()`); + } + + // Deactivate mocked bindings + exports.disable(); + delete realBinding._mockedBinding; + + let res; + try { + res = fn(); // Perform action + } finally { + exports.enable(); + } + + if (res.then) { + // eslint-disable-next-line no-console + console.warn( + `Async functions are not supported with exports.bypass(). See https://github.com/tschaub/mock-fs/#advancedbypass` + ); + } + + return res; +}; + +/** + * Temporarily disable Mocked FS + */ +exports.disable = () => { + 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/index.js b/lib/index.js index 23567722..974941df 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 mapping = require('./mapping'); +const bypass = require('./bypass'); const fs = require('fs'); const toNamespacedPath = FileSystem.toNamespacedPath; @@ -184,25 +186,20 @@ exports.directory = FileSystem.directory; */ exports.symlink = FileSystem.symlink; -let storedBinding; +/** + * Automatically maps specified paths (for use with `mock()`) + */ +exports.mapPaths = mapping.mapPaths; /** - * Temporarily disable Mocked FS + * Maps specific directory (for use with `mock()`) */ -exports.disable = () => { - storedBinding = realBinding._mockedBinding; - delete realBinding._mockedBinding; -}; +exports.mapDir = mapping.mapDir; /** - * Enables Mocked FS after being disabled by mock.disable() + * Maps specific file (for use with `mock()`) */ -exports.enable = () => { - if (storedBinding) { - realBinding._mockedBinding = storedBinding; - storedBinding = undefined; - } -}; +exports.mapFile = mapping.mapFile; /** * Perform action, bypassing mock FS @@ -211,90 +208,14 @@ exports.enable = () => { * const filePath = '/path/file.json'; * const data = mock.bypass(() => fs.readFileSync(filePath, 'utf-8')); */ -exports.bypass = function(fn) { - if (typeof fn !== 'function') { - throw new Error(`Must provide a function to perform for mock.bypass()`); - } - - // Deactivate mocked bindings - exports.disable(); - delete realBinding._mockedBinding; - - let res; - try { - res = fn(); // Perform action - } finally { - exports.enable(); - } - - if (res.then) { - // eslint-disable-next-line no-console - console.warn( - `Async functions are not supported with exports.bypass(). See https://github.com/tschaub/mock-fs/#advancedbypass` - ); - } - - return res; -}; - -const mapping = require('./mapping'); +exports.bypass = bypass; /** - * Automatically maps specified paths (for use with `mock()`) - */ -exports.mapPaths = function(paths, options) { - return exports.bypass(() => { - const res = {}; - const context = mapping.createContext({output: res, options}); - - const addPath = p => { - const absPath = mapping.fixupPath(p); - const stats = fs.statSync(absPath); - const newContext = mapping.createContext(context, {target: absPath}); - - if (stats.isDirectory()) { - mapping.addDir(newContext, stats, true); - } else if (stats.isFile()) { - mapping.addFile(newContext, stats, true); - } - }; - - if (Array.isArray(paths)) { - paths.forEach(addPath); - } else { - addPath(paths); - } - - return res; - }); -}; - -/** - * Maps specific directory (for use with `mock()`) + * Temporarily disable Mocked FS */ -exports.mapDir = function(dir, options) { - return exports.bypass(() => { - dir = mapping.fixupPath(dir); - - return mapping.addDir( - mapping.createContext({output: {}, options, target: dir}), - fs.statSync(dir), - true - ); - }); -}; +exports.disable = bypass.disable; /** - * Maps specific file (for use with `mock()`) + * Enables Mocked FS after being disabled by mock.disable() */ -exports.mapFile = function(file, options) { - return exports.bypass(() => { - file = mapping.fixupPath(file); - - return mapping.addFile( - mapping.createContext({output: {}, options, target: file}), - fs.statSync(file), - true - ); - }); -}; +exports.enable = bypass.enable; diff --git a/lib/mapping.js b/lib/mapping.js index 3f390e68..918e2d51 100755 --- a/lib/mapping.js +++ b/lib/mapping.js @@ -1,9 +1,10 @@ const {fixWin32Permissions} = require('./item'); const path = require('path'); +const FileSystem = require('./filesystem'); const fs = require('fs'); -const mock = require('./index'); +const bypass = require('./bypass'); -exports.createContext = ({output, options = {}, target}, newContext) => +const createContext = ({output, options = {}, target}, newContext) => Object.assign( { // Assign options and set defaults if needed @@ -17,7 +18,7 @@ exports.createContext = ({output, options = {}, target}, newContext) => newContext ); -exports.addFile = function(context, stats, isRoot) { +function addFile(context, stats, isRoot) { const {output, target} = context; const {lazyLoad} = context.options; @@ -29,12 +30,12 @@ exports.addFile = function(context, stats, isRoot) { output[outputPropKey] = () => { const content = !lazyLoad ? fs.readFileSync(target) : ''; - const file = mock.file(Object.assign({}, stats, {content}))(); + const file = FileSystem.file(Object.assign({}, stats, {content}))(); if (lazyLoad) { Object.defineProperty(file, '_content', { get() { - const res = mock.bypass(() => fs.readFileSync(target)); + const res = bypass(() => fs.readFileSync(target)); Object.defineProperty(file, '_content', { value: res, writable: true @@ -55,9 +56,9 @@ exports.addFile = function(context, stats, isRoot) { }; return output[outputPropKey]; -}; +} -exports.addDir = function(context, stats, isRoot) { +function addDir(context, stats, isRoot) { const {target, output} = context; const {recursive} = context.options; @@ -76,31 +77,91 @@ exports.addDir = function(context, stats, isRoot) { // Create directory factory const directoryItems = {}; - output[outputPropKey] = mock.directory( + 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 = exports.createContext(context, { + const newContext = createContext(context, { target: absPath, output: directoryItems }); if (recursive && stats.isDirectory()) { - exports.addDir(newContext, stats); + addDir(newContext, stats); } else if (stats.isFile()) { - exports.addFile(newContext, stats); + addFile(newContext, stats); } }); return output[outputPropKey]; -}; +} -exports.fixupPath = p => { +const fixupPath = p => { if (typeof p !== 'string') { throw new TypeError(`Invalid path. All paths must be strings`); } return path.resolve(p); }; + +/** + * Automatically maps specified paths (for use with `mock()`) + */ +exports.mapPaths = function(paths, options) { + return bypass(() => { + const res = {}; + const context = createContext({output: res, options}); + + const addPath = p => { + const absPath = fixupPath(p); + const stats = fs.statSync(absPath); + const newContext = createContext(context, {target: absPath}); + + if (stats.isDirectory()) { + addDir(newContext, stats, true); + } else if (stats.isFile()) { + addFile(newContext, stats, true); + } + }; + + if (Array.isArray(paths)) { + paths.forEach(addPath); + } else { + addPath(paths); + } + + return res; + }); +}; + +/** + * Maps specific directory (for use with `mock()`) + */ +exports.mapDir = function(dir, options) { + return bypass(() => { + dir = fixupPath(dir); + + return addDir( + createContext({output: {}, options, target: dir}), + fs.statSync(dir), + true + ); + }); +}; + +/** + * Maps specific file (for use with `mock()`) + */ +exports.mapFile = function(file, options) { + return bypass(() => { + file = fixupPath(file); + + return addFile( + createContext({output: {}, options, target: file}), + fs.statSync(file), + true + ); + }); +}; From 8dfdc859d1f9919761e58fdac98dd234bd89d195 Mon Sep 17 00:00:00 2001 From: Ron S Date: Mon, 3 Aug 2020 23:09:05 -0400 Subject: [PATCH 27/31] Applied review fixes - Remove `enable`, `disable` from exports - Add promise support to `mock.bypass()` - Merge `mapDir()` and `mapFile()` functions to `load()`` - Remove `mapPaths()` - Use `path.basename` (May affect cygwin, but Linux allows backslashes in filename, which takes precedence) - Remove custom error message for non-string parameter to `load()` - Updated tests to accommodate changes - Updated readme according to suggestions --- lib/bypass.js | 34 ++++++----- lib/index.js | 24 +------- lib/{mapping.js => loader.js} | 73 ++++------------------ readme.md | 73 ++++++---------------- test/lib/index.spec.js | 112 ++++++++-------------------------- 5 files changed, 79 insertions(+), 237 deletions(-) rename lib/{mapping.js => loader.js} (64%) diff --git a/lib/bypass.js b/lib/bypass.js index cb6c60ce..6a836b28 100755 --- a/lib/bypass.js +++ b/lib/bypass.js @@ -13,33 +13,35 @@ exports = module.exports = function bypass(fn) { throw new Error(`Must provide a function to perform for mock.bypass()`); } - // Deactivate mocked bindings exports.disable(); - delete realBinding._mockedBinding; - let res; try { - res = fn(); // Perform action - } finally { - exports.enable(); - } + // Perform action + const res = fn(); - if (res.then) { - // eslint-disable-next-line no-console - console.warn( - `Async functions are not supported with exports.bypass(). See https://github.com/tschaub/mock-fs/#advancedbypass` - ); - } + // Handle promise return + if (res.then) { + res.then(exports.enable); + res.catch(exports.enable); + } else { + exports.enable(); + } - return res; + return res; + } catch (e) { + exports.enable(); + throw e; + } }; /** * Temporarily disable Mocked FS */ exports.disable = () => { - storedBinding = realBinding._mockedBinding; - delete realBinding._mockedBinding; + if (realBinding._mockedBinding) { + storedBinding = realBinding._mockedBinding; + delete realBinding._mockedBinding; + } }; /** diff --git a/lib/index.js b/lib/index.js index 974941df..0f78d734 100644 --- a/lib/index.js +++ b/lib/index.js @@ -5,7 +5,7 @@ const FSError = require('./error'); const FileSystem = require('./filesystem'); const realBinding = process.binding('fs'); const path = require('path'); -const mapping = require('./mapping'); +const loader = require('./loader'); const bypass = require('./bypass'); const fs = require('fs'); @@ -189,17 +189,7 @@ exports.symlink = FileSystem.symlink; /** * Automatically maps specified paths (for use with `mock()`) */ -exports.mapPaths = mapping.mapPaths; - -/** - * Maps specific directory (for use with `mock()`) - */ -exports.mapDir = mapping.mapDir; - -/** - * Maps specific file (for use with `mock()`) - */ -exports.mapFile = mapping.mapFile; +exports.load = loader.load; /** * Perform action, bypassing mock FS @@ -209,13 +199,3 @@ exports.mapFile = mapping.mapFile; * const data = mock.bypass(() => fs.readFileSync(filePath, 'utf-8')); */ exports.bypass = bypass; - -/** - * Temporarily disable Mocked FS - */ -exports.disable = bypass.disable; - -/** - * Enables Mocked FS after being disabled by mock.disable() - */ -exports.enable = bypass.enable; diff --git a/lib/mapping.js b/lib/loader.js similarity index 64% rename from lib/mapping.js rename to lib/loader.js index 918e2d51..f2d549dc 100755 --- a/lib/mapping.js +++ b/lib/loader.js @@ -26,7 +26,7 @@ function addFile(context, stats, isRoot) { throw new Error(`${target} is not a valid file!`); } - const outputPropKey = isRoot ? target : path.win32.basename(target); + const outputPropKey = isRoot ? target : path.basename(target); output[outputPropKey] = () => { const content = !lazyLoad ? fs.readFileSync(target) : ''; @@ -67,7 +67,7 @@ function addDir(context, stats, isRoot) { } stats = Object.assign({}, stats); - const outputPropKey = isRoot ? target : path.win32.basename(target); + 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 @@ -99,69 +99,20 @@ function addDir(context, stats, isRoot) { return output[outputPropKey]; } -const fixupPath = p => { - if (typeof p !== 'string') { - throw new TypeError(`Invalid path. All paths must be strings`); - } - return path.resolve(p); -}; - /** - * Automatically maps specified paths (for use with `mock()`) + * Load directory or file from real FS */ -exports.mapPaths = function(paths, options) { +exports.load = function(p, options) { return bypass(() => { - const res = {}; - const context = createContext({output: res, options}); - - const addPath = p => { - const absPath = fixupPath(p); - const stats = fs.statSync(absPath); - const newContext = createContext(context, {target: absPath}); - - if (stats.isDirectory()) { - addDir(newContext, stats, true); - } else if (stats.isFile()) { - addFile(newContext, stats, true); - } - }; - - if (Array.isArray(paths)) { - paths.forEach(addPath); - } else { - addPath(paths); - } - - return res; - }); -}; + p = path.resolve(p); -/** - * Maps specific directory (for use with `mock()`) - */ -exports.mapDir = function(dir, options) { - return bypass(() => { - dir = fixupPath(dir); + const stats = fs.statSync(p); + const context = createContext({output: {}, options, target: p}); - return addDir( - createContext({output: {}, options, target: dir}), - fs.statSync(dir), - true - ); - }); -}; - -/** - * Maps specific file (for use with `mock()`) - */ -exports.mapFile = function(file, options) { - return bypass(() => { - file = fixupPath(file); - - return addFile( - createContext({output: {}, options, target: file}), - fs.statSync(file), - true - ); + 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 30aff21b..15901b21 100644 --- a/readme.md +++ b/readme.md @@ -60,65 +60,37 @@ 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. -### Mapping real files & directories +### Loading real files & directories -If you want to add files from your real filesystem to the mocked FS, there are several mapping methods to help. +You can load real files and directories into the mock system using `mock.load()` #### Notes -- Each mapper function duplicates all stat information (dates, permissions, etc) -- By default, all files are lazy-loaded, unless you specify the `{ lazyLoad: false }` option, which makes it reasonable -to expose large areas of the filesystem to your mocked FS. +- 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 (applies to `mock.mapDir()` & `mock.mapPaths()`) +| recursive | boolean | true | Load all files and directories recursively -#### `mock.mapPaths(path, options)` - -Quickly map one or more paths. - -_Note: Mapping a path makes its absolute path available in the Mock Filesystem. It is a good way to 'mount' sections of your -real filesystem, without allowing changes to affect the real files._ - -```js -// Mock FS, with single dir -mock(mock.mapPaths('/path/to/dir')); - -// Mock FS, with two dirs and a file (do not recurse and pre-load all content) -mock(mock.mapPaths([ '/path/to/dir', '/path/to/anotherdir', '/srv/some-file.txt' ], { recursive: false, lazyLoad: false })); -``` - -#### `mock.mapDir(dirPath, options)` - -Create a `Directory` for a real dir. - -```js -mock({ - // Recursively loads all node_modules - 'node_modules': mock.mapDir(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.mapDir('/tmp/special_tmp_files', { recursive: false, lazyLoad:false }), - - 'fakefile.txt': 'content here' -}); -``` - -#### `mock.mapFile(filePath, options)` - -Create a `File` for a real file. +#### `mock.load(path, options)` ```js mock({ // Lazy-load file - 'my-file.txt': mock.mapFile(path.resolve(__dirname, 'assets/special-file.txt')), + 'my-file.txt': mock.load(path.resolve(__dirname, 'assets/special-file.txt')), // Pre-load js file - 'ready.js': mock.mapFile(path.resolve(__dirname, 'scripts/ready.js'), { lazyLoad: false }), + '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' }); @@ -257,7 +229,7 @@ afterEach(mock.restore); #### `mock.bypass(fn)` -Execute _synchronous calls_ to the real filesystem with mock.bypass() +Execute calls to the real filesystem with mock.bypass() ```js // This file exists only on the real FS, not on the mocked FS @@ -265,23 +237,18 @@ const realFilePath = '/path/to/real/file.txt'; const myData = mock.bypass(() => fs.readFileSync(realFilePath, 'utf-8')); ``` -#### Advanced Bypassing +#### Async Warning -Asynchronous calls are not recommended as they could produce unintended consequences if anything else tries to access the -mocked filesystem before they've completed. - -However, if you know what you're doing, you can selectively disable and re-enable the mock filesystem. +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) { - mock.disable(); - try { + return await mock.bypass(async () => { const stats = await fs.promises.stat(fileName); const data = await fs.promises.readFile(fileName); return { stats, data }; - } finally { - mock.enable(); - } + }); } ``` diff --git a/test/lib/index.spec.js b/test/lib/index.spec.js index ed8323e3..40d570fc 100644 --- a/test/lib/index.spec.js +++ b/test/lib/index.spec.js @@ -187,24 +187,34 @@ describe('The API', function() { describe(`mock.bypass()`, () => { afterEach(mock.restore); - it('bypasses mock FS', () => { + 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)); }); - it('restores mock FS after bypass', () => { - mock({}); + it('(async) bypasses mock FS & restores after', async () => { + 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))); + + await mock.bypass(async () => { + let stat = await fs.promises.stat(__filename); + assert.isTrue(stat.isFile()); + stat = await fs.promises.stat(__filename); + assert.isTrue(stat.isFile()); + }); + assert.isNotOk(fs.existsSync(__filename)); }); }); - describe(`Mapping functions`, () => { + describe(`mock.load()`, () => { const statsCompareKeys = [ 'birthtime', 'ctime', @@ -231,17 +241,11 @@ describe('The API', function() { return res; }; - describe(`mock.mapFile()`, () => { + describe(`File`, () => { const filePath = path.join(assetsPath, 'file1.txt'); - it('throws with non-string path', () => { - assert.throws(() => mock.mapFile(null)); - }); - it('throws with directory', () => { - assert.throws(() => mock.mapFile(path.join(assetsPath))); - }); it('creates a File factory with correct attributes', () => { - const file = mock.mapFile(filePath)(); + const file = mock.load(filePath)(); const stats = fs.statSync(filePath); assert.instanceOf(file, File); @@ -249,7 +253,7 @@ describe('The API', function() { }); describe('lazyLoad=true', () => { let file; - beforeEach(() => (file = mock.mapFile(filePath)())); + beforeEach(() => (file = mock.load(filePath)())); it('creates accessors', () => { assert.typeOf( @@ -297,7 +301,7 @@ describe('The API', function() { }); it('lazyLoad=false loads file content', () => { - const file = mock.mapFile(path.join(assetsPath, 'file1.txt'), { + const file = mock.load(path.join(assetsPath, 'file1.txt'), { lazyLoad: false })(); @@ -308,21 +312,15 @@ describe('The API', function() { }); it('can read file from mocked FS', () => { - mock({'/file': mock.mapFile(filePath)}); + mock({'/file': mock.load(filePath)}); assert.equal(fs.readFileSync('/file'), 'data1'); mock.restore(); }); }); - describe(`mock.mapDir()`, () => { - it('throws with non-string path', () => { - assert.throws(() => mock.mapDir(null)); - }); - it('throws with file', () => { - assert.throws(() => mock.mapDir(path.join(assetsPath, 'file1.txt'))); - }); + describe(`Dir`, () => { it('creates a Directory factory with correct attributes', () => { - const dir = mock.mapDir(assetsPath)(); + const dir = mock.load(assetsPath)(); const stats = fs.statSync(assetsPath); assert.instanceOf(dir, Directory); @@ -330,7 +328,7 @@ describe('The API', function() { }); describe('recursive=true', () => { it('creates all files & dirs', () => { - const base = mock.mapDir(assetsPath, {recursive: true})(); + const base = mock.load(assetsPath, {recursive: true})(); const baseDir = base._items.dir; const baseDirSubdir = baseDir._items.subdir; @@ -346,13 +344,13 @@ describe('The API', function() { const getFile = () => dir._items.dir._items.subdir._items['file3.txt']; - dir = mock.mapDir(assetsPath, {recursive: true, lazyLoad: true})(); + dir = mock.load(assetsPath, {recursive: true, lazyLoad: true})(); assert.typeOf( Object.getOwnPropertyDescriptor(getFile(), '_content').get, 'function' ); - dir = mock.mapDir(assetsPath, {recursive: true, lazyLoad: false})(); + dir = mock.load(assetsPath, {recursive: true, lazyLoad: false})(); assert.instanceOf( Object.getOwnPropertyDescriptor(getFile(), '_content').value, Buffer @@ -361,74 +359,18 @@ describe('The API', function() { }); it('recursive=false creates files & does not recurse', () => { - const base = mock.mapDir(assetsPath, {recursive: false})(); + 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.mapDir(assetsPath, {recursive: true})}); + mock({'/dir': mock.load(assetsPath, {recursive: true})}); assert.equal(fs.readFileSync('/dir/file1.txt'), 'data1'); mock.restore(); }); }); - - describe(`mock.mapPaths()`, () => { - it('throws with non-string path', () => { - assert.throws(() => mock.mapDir(null)); - assert.throws(() => mock.mapDir([null])); - }); - it('maps multiple paths', () => { - const filePath1 = path.join(assetsPath, 'file1.txt'); - const filePath2 = path.join(assetsPath, '/dir/file2.txt'); - const res = mock.mapPaths([filePath1, filePath2]); - assert.instanceOf(res[filePath1](), File); - assert.instanceOf(res[filePath2](), File); - }); - it('maps single path', () => { - const filePath1 = path.join(assetsPath, 'file1.txt'); - const res = mock.mapPaths(filePath1); - assert.instanceOf(res[filePath1](), File); - }); - it('respects lazyLoad setting', () => { - let res; - const filePath = path.join(assetsPath, 'file1.txt'); - - res = mock.mapPaths(filePath, {lazyLoad: true}); - assert.typeOf( - Object.getOwnPropertyDescriptor(res[filePath](), '_content').get, - 'function' - ); - - res = mock.mapPaths(filePath, {lazyLoad: false}); - assert.instanceOf( - Object.getOwnPropertyDescriptor(res[filePath](), '_content').value, - Buffer - ); - }); - it('recursive=true loads recursively', () => { - const dirPath = path.join(assetsPath, 'dir'); - const filePath = path.join(assetsPath, 'file1.txt'); - const res = mock.mapPaths([dirPath, filePath], {recursive: true}); - - const dir = res[dirPath](); - const dirSubdir = dir._items.subdir; - - assert.instanceOf(res[filePath](), File); - assert.instanceOf(dir, Directory); - assert.instanceOf(dir._items['file2.txt'], File); - assert.instanceOf(dirSubdir, Directory); - assert.instanceOf(dirSubdir._items['file3.txt'], File); - }); - }); - it('can read file from mocked FS', () => { - const filePath1 = path.join(assetsPath, 'file1.txt'); - const filePath2 = path.join(assetsPath, '/dir/file2.txt'); - mock(mock.mapPaths([filePath1, filePath2])); - assert.equal(fs.readFileSync(filePath2), 'data2'); - mock.restore(); - }); }); xdescribe('mock.fs()', function() { From 2cc92f3922bd2cbb4347935628f13342fe138ea9 Mon Sep 17 00:00:00 2001 From: Ron S Date: Mon, 3 Aug 2020 23:36:34 -0400 Subject: [PATCH 28/31] Remove async await in test (Node 6/8 support) --- test/lib/index.spec.js | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/test/lib/index.spec.js b/test/lib/index.spec.js index 40d570fc..76a32279 100644 --- a/test/lib/index.spec.js +++ b/test/lib/index.spec.js @@ -197,20 +197,27 @@ describe('The API', function() { assert.isNotOk(fs.existsSync(__filename)); }); - it('(async) bypasses mock FS & restores after', async () => { + 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)); - await mock.bypass(async () => { - let stat = await fs.promises.stat(__filename); - assert.isTrue(stat.isFile()); - stat = await fs.promises.stat(__filename); - assert.isTrue(stat.isFile()); - }); - - 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)) + ); }); }); From 0c7bbbb6101951b483f36f18e4fa26f609eed7b6 Mon Sep 17 00:00:00 2001 From: Ron S Date: Mon, 3 Aug 2020 23:40:16 -0400 Subject: [PATCH 29/31] Prettier correction --- test/lib/index.spec.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/lib/index.spec.js b/test/lib/index.spec.js index 76a32279..72d21c04 100644 --- a/test/lib/index.spec.js +++ b/test/lib/index.spec.js @@ -197,14 +197,15 @@ describe('The API', function() { assert.isNotOk(fs.existsSync(__filename)); }); - it('(async) bypasses mock FS & restores after', (done) => { + 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) + fs.promises + .stat(__filename) .then(stat => { assert.isTrue(stat.isFile()); return fs.promises.stat(__filename); From f431af2746dfe177995def8b180b918237b148ab Mon Sep 17 00:00:00 2001 From: Ron S Date: Mon, 3 Aug 2020 23:43:44 -0400 Subject: [PATCH 30/31] Add withPromise to test --- test/lib/index.spec.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/lib/index.spec.js b/test/lib/index.spec.js index 72d21c04..017dcb11 100644 --- a/test/lib/index.spec.js +++ b/test/lib/index.spec.js @@ -8,6 +8,7 @@ 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'); @@ -197,7 +198,7 @@ describe('The API', function() { assert.isNotOk(fs.existsSync(__filename)); }); - it('(async) bypasses mock FS & restores after', done => { + withPromise.it('(async) bypasses mock FS & restores after', done => { mock({'/path/to/file': 'content'}); assert.equal(fs.readFileSync('/path/to/file', 'utf8'), 'content'); From 768224fa947c473dc990167137636a9293ab63b1 Mon Sep 17 00:00:00 2001 From: Ron S Date: Tue, 4 Aug 2020 00:42:29 -0400 Subject: [PATCH 31/31] Changed to typeof function check --- lib/bypass.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/bypass.js b/lib/bypass.js index 6a836b28..770e81bd 100755 --- a/lib/bypass.js +++ b/lib/bypass.js @@ -20,7 +20,7 @@ exports = module.exports = function bypass(fn) { const res = fn(); // Handle promise return - if (res.then) { + if (typeof res.then === 'function') { res.then(exports.enable); res.catch(exports.enable); } else {