diff --git a/local-cli/__mocks__/fs.js b/local-cli/__mocks__/fs.js index bd4d5c1000..0ac81ced62 100644 --- a/local-cli/__mocks__/fs.js +++ b/local-cli/__mocks__/fs.js @@ -9,428 +9,35 @@ 'use strict'; -const asyncify = require('async/asyncify'); -const {EventEmitter} = require('events'); -const {dirname} = require.requireActual('path'); -const fs = jest.genMockFromModule('fs'); -const invariant = require('fbjs/lib/invariant'); -const path = require('path'); -const stream = require.requireActual('stream'); +const fs = new (require('metro-memory-fs'))(); -const noop = () => {}; - -function asyncCallback(cb) { - return function() { - setImmediate(() => cb.apply(this, arguments)); - }; +function setMockFilesystem(object) { + fs.reset(); + const root = process.platform === 'win32' ? 'c:\\' : '/'; + mockDir(root, {...object}); } -const mtime = { - getTime: () => Math.ceil(Math.random() * 10000000), -}; - -fs.realpath.mockImplementation((filepath, callback) => { - callback = asyncCallback(callback); - let node; - try { - node = getToNode(filepath); - } catch (e) { - return callback(e); - } - if (node && typeof node === 'object' && node.SYMLINK != null) { - return callback(null, node.SYMLINK); - } - return callback(null, filepath); -}); - -fs.readdirSync.mockImplementation(filepath => Object.keys(getToNode(filepath))); - -fs.readdir.mockImplementation((filepath, callback) => { - callback = asyncCallback(callback); - let node; - try { - node = getToNode(filepath); - if (node && typeof node === 'object' && node.SYMLINK != null) { - node = getToNode(node.SYMLINK); +function mockDir(dirPath, desc) { + for (const entName in desc) { + const ent = desc[entName]; + const entPath = require('path').join(dirPath, entName); + if (typeof ent === 'string' || ent instanceof Buffer) { + fs.writeFileSync(entPath, ent); + continue; } - } catch (e) { - return callback(e); - } - - if (!(node && typeof node === 'object' && node.SYMLINK == null)) { - return callback(new Error(filepath + ' is not a directory.')); - } - - return callback(null, Object.keys(node)); -}); - -fs.readFile.mockImplementation(asyncify(fs.readFileSync)); - -fs.readFileSync.mockImplementation(function(filepath, encoding) { - filepath = path.normalize(filepath); - const node = getToNode(filepath); - if (isDirNode(node)) { - throw new Error('Error readFileSync a dir: ' + filepath); - } - if (Buffer.isBuffer(node) && typeof encoding !== 'undefined') { - return node.toString(); - } - return node; -}); - -fs.writeFile.mockImplementation(asyncify(fs.writeFileSync)); - -fs.writeFileSync.mockImplementation((filePath, content, options) => { - filePath = path.normalize(filePath); - if (options == null || typeof options === 'string') { - options = {encoding: options}; - } - invariant( - options.encoding == null || options.encoding === 'utf8', - '`options` argument supports only `null` or `"utf8"`', - ); - const dirPath = path.dirname(filePath); - const node = getToNode(dirPath); - if (!isDirNode(node)) { - throw fsError('ENOTDIR', 'not a directory: ' + dirPath); - } - node[path.basename(filePath)] = content; -}); - -const openFds = new Map(); -let nextFd = 3; - -fs.openSync.mockImplementation((filePath, flags) => { - const dirPath = path.dirname(filePath); - const node = getToNode(dirPath); - if (!isDirNode(node)) { - throw fsError('ENOTDIR', 'not a directory: ' + dirPath); - } - node[path.basename(filePath)] = ''; - openFds.set(nextFd, {filePath, flags, node}); - return nextFd++; -}); - -fs.writeSync.mockImplementation((fd, str) => { - invariant(typeof str === 'string', 'only strings supported'); - const data = openFds.get(fd); - if (data == null || data.flags !== 'w') { - throw fsError('EBADF', 'bad file descriptor, write'); - } - data.node[path.basename(data.filePath)] += str; -}); - -fs.closeSync.mockImplementation(fd => { - openFds.delete(fd); -}); - -fs.mkdir.mockImplementation(asyncify(fs.mkdirSync)); - -fs.mkdirSync.mockImplementation((dirPath, mode) => { - const parentPath = path.dirname(dirPath); - const node = getToNode(parentPath); - if (!isDirNode(node)) { - throw fsError('ENOTDIR', 'not a directory: ' + parentPath); - } - if (node[path.basename(dirPath)] == null) { - node[path.basename(dirPath)] = {}; - } -}); - -function fsError(code, message) { - const error = new Error(code + ': ' + message); - error.code = code; - return error; -} - -function isDirNode(node) { - return ( - node && - typeof node === 'object' && - node.SYMLINK == null && - Buffer.isBuffer(node) === false - ); -} - -function readlinkSync(filepath) { - const node = getToNode(filepath); - if (node !== null && typeof node === 'object' && !!node.SYMLINK) { - return node.SYMLINK; - } else { - throw new Error(`EINVAL: invalid argument, readlink '${filepath}'`); - } -} - -fs.readlink.mockImplementation((filepath, callback) => { - callback = asyncCallback(callback); - let result; - try { - result = readlinkSync(filepath); - } catch (e) { - callback(e); - return; - } - callback(null, result); -}); - -fs.readlinkSync.mockImplementation(readlinkSync); - -function existsSync(filepath) { - try { - const node = getToNode(filepath); - return node !== null; - } catch (e) { - return false; - } -} - -fs.exists.mockImplementation((filepath, callback) => { - callback = asyncCallback(callback); - let result; - try { - result = existsSync(filepath); - } catch (e) { - callback(e); - return; - } - callback(null, result); -}); - -fs.existsSync.mockImplementation(existsSync); - -function makeStatResult(node) { - const isSymlink = node != null && node.SYMLINK != null; - return { - isBlockDevice: () => false, - isCharacterDevice: () => false, - isDirectory: () => node != null && typeof node === 'object' && !isSymlink, - isFIFO: () => false, - isFile: () => node != null && typeof node === 'string', - isSocket: () => false, - isSymbolicLink: () => isSymlink, - mtime, - }; -} - -function statSync(filepath) { - const node = getToNode(filepath); - if (node != null && node.SYMLINK) { - return statSync(node.SYMLINK); - } - return makeStatResult(node); -} - -fs.stat.mockImplementation((filepath, callback) => { - callback = asyncCallback(callback); - let result; - try { - result = statSync(filepath); - } catch (e) { - callback(e); - return; - } - callback(null, result); -}); - -fs.statSync.mockImplementation(statSync); - -function lstatSync(filepath) { - const node = getToNode(filepath); - return makeStatResult(node); -} - -fs.lstat.mockImplementation((filepath, callback) => { - callback = asyncCallback(callback); - let result; - try { - result = lstatSync(filepath); - } catch (e) { - callback(e); - return; - } - callback(null, result); -}); - -fs.lstatSync.mockImplementation(lstatSync); - -fs.open.mockImplementation(function(filepath) { - const callback = arguments[arguments.length - 1] || noop; - let data, error, fd; - try { - data = getToNode(filepath); - } catch (e) { - error = e; - } - - if (error || data == null) { - error = Error(`ENOENT: no such file or directory: \`${filepath}\``); - } - if (data != null) { - /* global Buffer: true */ - fd = {buffer: new Buffer(data, 'utf8'), position: 0}; - } - - callback(error, fd); -}); - -fs.read.mockImplementation( - (fd, buffer, writeOffset, length, position, callback = noop) => { - let bytesWritten; - try { - if (position == null || position < 0) { - ({position} = fd); - } - bytesWritten = fd.buffer.copy( - buffer, - writeOffset, - position, - position + length, - ); - fd.position = position + bytesWritten; - } catch (e) { - callback(Error('invalid argument')); - return; + if (typeof ent !== 'object') { + throw new Error(require('util').format('invalid entity:', ent)); } - callback(null, bytesWritten, buffer); - }, -); - -fs.close.mockImplementation((fd, callback = noop) => { - try { - fd.buffer = fs.position = undefined; - } catch (e) { - callback(Error('invalid argument')); - return; - } - callback(null); -}); - -let filesystem = {}; - -fs.mock = { - clear() { - filesystem = {}; - }, -}; - -fs.createReadStream.mockImplementation(filepath => { - if (!filepath.startsWith('/')) { - throw Error('Cannot open file ' + filepath); - } - - const parts = filepath.split('/').slice(1); - let file = filesystem; - - for (const part of parts) { - file = file[part]; - if (!file) { - break; + if (ent.SYMLINK != null) { + fs.symlinkSync(ent.SYMLINK, entPath); + continue; } + fs.mkdirSync(entPath); + mockDir(entPath, ent); } - - if (typeof file !== 'string') { - throw Error('Cannot open file ' + filepath); - } - - return new stream.Readable({ - read() { - this.push(file, 'utf8'); - this.push(null); - }, - }); -}); - -fs.createWriteStream.mockImplementation(filePath => { - let node; - const writeStream = new stream.Writable({ - write(chunk, encoding, callback) { - this.__chunks.push(chunk); - node[path.basename(filePath)] = this.__chunks.join(''); - callback(); - }, - }); - writeStream.__file = filePath; - writeStream.__chunks = []; - writeStream.end = jest.fn(writeStream.end); - fs.createWriteStream.mock.returned.push(writeStream); - try { - const dirPath = dirname(filePath); - node = getToNode(dirPath); - if (!isDirNode(node)) { - throw fsError('ENOTDIR', 'not a directory: ' + dirPath); - } - // Truncate the file on opening. - node[path.basename(filePath)] = ''; - } catch (error) { - process.nextTick(() => writeStream.emit('error', error)); - } - return writeStream; -}); -fs.createWriteStream.mock.returned = []; - -fs.__setMockFilesystem = object => (filesystem = object); - -const watcherListByPath = new Map(); - -fs.watch.mockImplementation((filename, options, listener) => { - if (options.recursive) { - throw new Error('recursive watch not implemented'); - } - let watcherList = watcherListByPath.get(filename); - if (watcherList == null) { - watcherList = []; - watcherListByPath.set(filename, watcherList); - } - const fsWatcher = new EventEmitter(); - fsWatcher.on('change', listener); - fsWatcher.close = () => { - watcherList.splice(watcherList.indexOf(fsWatcher), 1); - fsWatcher.close = () => { - throw new Error('FSWatcher is already closed'); - }; - }; - watcherList.push(fsWatcher); -}); - -fs.__triggerWatchEvent = (eventType, filename) => { - const directWatchers = watcherListByPath.get(filename) || []; - directWatchers.forEach(wtc => wtc.emit('change', eventType)); - const dirPath = path.dirname(filename); - const dirWatchers = watcherListByPath.get(dirPath) || []; - dirWatchers.forEach(wtc => - wtc.emit('change', eventType, path.relative(dirPath, filename)), - ); -}; - -function getToNode(filepath) { - // Ignore the drive for Windows paths. - if (filepath.match(/^[a-zA-Z]:\\/)) { - filepath = filepath.substring(2); - } - - if (filepath.endsWith(path.sep)) { - filepath = filepath.slice(0, -1); - } - const parts = filepath.split(/[\/\\]/); - if (parts[0] !== '') { - throw new Error('Make sure all paths are absolute.'); - } - let node = filesystem; - parts.slice(1).forEach(part => { - if (node && node.SYMLINK) { - node = getToNode(node.SYMLINK); - } - node = node[part]; - if (node == null) { - const err = new Error( - `ENOENT: no such file or directory: \`${filepath}\``, - ); - err.code = 'ENOENT'; - throw err; - } - }); - - return node; } +fs.__setMockFilesystem = setMockFilesystem; +fs.mock = {clear: () => fs.reset()}; + module.exports = fs; diff --git a/local-cli/__tests__/fs-mock-test.js b/local-cli/__tests__/fs-mock-test.js deleted file mode 100644 index 4928f3bd9e..0000000000 --- a/local-cli/__tests__/fs-mock-test.js +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @emails oncall+javascript_foundation - * @flow - * @format - */ - -'use strict'; - -declare var jest: any; -declare var describe: any; -declare var beforeEach: any; -declare var expect: any; -declare var it: any; - -jest.mock('fs'); - -const fs = require('fs'); - -describe('fs mock', () => { - beforeEach(() => { - (fs: $FlowFixMe).mock.clear(); - }); - - describe('writeFileSync()', () => { - it('stores content correctly', () => { - fs.writeFileSync('/test', 'foobar', 'utf8'); - const content = fs.readFileSync('/test', 'utf8'); - expect(content).toEqual('foobar'); - }); - - it('fails on missing path', () => { - expect(() => - fs.writeFileSync('/dir/test', 'foobar', 'utf8'), - ).toThrowError('ENOENT: no such file or directory'); - }); - - it('properly normalizes paths', () => { - fs.writeFileSync('/test/foo/../bar/../../tadam', 'beep', 'utf8'); - const content = fs.readFileSync('/glo/../tadam', 'utf8'); - expect(content).toEqual('beep'); - }); - }); - - describe('mkdirSync()', () => { - it('creates folders that we can write files in', () => { - fs.mkdirSync('/dir', 0o777); - fs.writeFileSync('/dir/test', 'foobar', 'utf8'); - const content = fs.readFileSync('/dir/test', 'utf8'); - expect(content).toEqual('foobar'); - }); - - it('does not erase directories', () => { - fs.mkdirSync('/dir', 0o777); - fs.writeFileSync('/dir/test', 'foobar', 'utf8'); - fs.mkdirSync('/dir', 0o777); - const content = fs.readFileSync('/dir/test', 'utf8'); - expect(content).toEqual('foobar'); - }); - }); - - describe('createWriteStream()', () => { - it('writes content', done => { - const stream = fs.createWriteStream('/test'); - stream.write('hello, '); - stream.write('world'); - stream.end('!'); - process.nextTick(() => { - const content = fs.readFileSync('/test', 'utf8'); - expect(content).toEqual('hello, world!'); - done(); - }); - }); - }); - - describe('writeSync()', () => { - it('writes content', () => { - const fd = fs.openSync('/test', 'w'); - fs.writeSync(fd, 'hello, world!'); - fs.closeSync(fd); - const content = fs.readFileSync('/test', 'utf8'); - expect(content).toEqual('hello, world!'); - }); - }); -}); diff --git a/local-cli/link/__tests__/ios/writePlist.spec.js b/local-cli/link/__tests__/ios/writePlist.spec.js index 0d2e086d00..7e32b72aac 100644 --- a/local-cli/link/__tests__/ios/writePlist.spec.js +++ b/local-cli/link/__tests__/ios/writePlist.spec.js @@ -26,6 +26,9 @@ const infoPlistPath = path.join(__dirname, '../../__fixtures__/Info.plist'); fs.readFileSync = jest.fn(() => readFileSync(projectPath).toString()); +const {writeFileSync} = fs; +fs.writeFileSync = jest.fn(writeFileSync); + const project = xcode.project('/Basic/project.pbxproj'); const plist = { diff --git a/package.json b/package.json index d951833578..9e2e48f7ec 100644 --- a/package.json +++ b/package.json @@ -170,6 +170,7 @@ "metro": "^0.34.0", "metro-babel-register": "^0.34.2", "metro-core": "^0.34.0", + "metro-memory-fs": "^0.34.0", "mime": "^1.3.4", "minimist": "^1.2.0", "mkdirp": "^0.5.1",