diff --git a/packager/react-packager/src/DependencyResolver/__tests__/fastfs-data b/packager/react-packager/src/DependencyResolver/__tests__/fastfs-data new file mode 100644 index 000000000..fe2c63885 --- /dev/null +++ b/packager/react-packager/src/DependencyResolver/__tests__/fastfs-data @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * An arbitrary module header + * @providesModule + */ + + const some: string = 'arbitrary code'; + const containing: string = 'ūñïčødę'; + +/** + * An arbitrary class that extends some thing + * It exposes a random number, which may be reset at the callers discretion + */ +class Arbitrary extends Something { + constructor() { + this.reset(); + } + + /** + * Returns the random number + * @returns number + */ + get random(): number { + return this._random; + } + + /** + * Re-creates the internal random number + * @returns void + */ + reset(): void { + this._random = Math.random(); + } +} diff --git a/packager/react-packager/src/DependencyResolver/__tests__/fastfs-integrated-test.js b/packager/react-packager/src/DependencyResolver/__tests__/fastfs-integrated-test.js new file mode 100644 index 000000000..f7fb35588 --- /dev/null +++ b/packager/react-packager/src/DependencyResolver/__tests__/fastfs-integrated-test.js @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +jest.autoMockOff(); + +const Fastfs = require('../fastfs'); + +const {EventEmitter} = require('events'); +const fs = require('fs'); +const path = require('path'); + +const fileName = path.resolve(__dirname, 'fastfs-data'); +const contents = fs.readFileSync(fileName, 'utf-8'); + +describe('fastfs:', function() { + let fastfs; + const crawling = Promise.resolve([fileName]); + const roots = [__dirname]; + const watcher = new EventEmitter(); + + beforeEach(function(done) { + fastfs = new Fastfs('arbitrary', roots, watcher, {crawling}); + fastfs.build().then(done); + }); + + describe('partial reading', () => { + // these are integrated tests that read real files from disk + + pit('reads a file while a predicate returns true', function() { + return fastfs.readWhile(fileName, () => true).then(readContent => + expect(readContent).toEqual(contents) + ); + }); + + pit('invokes the predicate with the new chunk, the invocation index, and the result collected so far', () => { + const predicate = jest.genMockFn().mockReturnValue(true); + return fastfs.readWhile(fileName, predicate).then(() => { + let aggregated = ''; + const {calls} = predicate.mock; + expect(calls).not.toEqual([]); + + calls.forEach((call, i) => { + const [chunk] = call; + aggregated += chunk; + expect(chunk).not.toBe(''); + expect(call).toEqual([chunk, i, aggregated]); + }); + + expect(aggregated).toEqual(contents); + }); + }); + + pit('stops reading when the predicate returns false', () => { + const predicate = jest.genMockFn().mockImpl((_, i) => i !== 0); + return fastfs.readWhile(fileName, predicate).then((readContent) => { + const {calls} = predicate.mock; + expect(calls.length).toBe(1); + expect(readContent).toBe(calls[0][2]); + }); + }); + + pit('after reading the whole file with `readWhile`, `read()` still works', () => { + // this test allows to reuse the results of `readWhile` for `readFile` + return fastfs.readWhile(fileName, () => true).then(() => { + fastfs.readFile(fileName).then(readContent => + expect(readContent).toEqual(contents) + ); + }); + }); + + pit('after reading parts of the file with `readWhile`, `read()` still works', () => { + return fastfs.readWhile(fileName, () => false).then(() => { + fastfs.readFile(fileName).then(readContent => + expect(readContent).toEqual(contents) + ); + }); + }); + }); +}); diff --git a/packager/react-packager/src/DependencyResolver/fastfs.js b/packager/react-packager/src/DependencyResolver/fastfs.js index ca01b86c8..f33b99278 100644 --- a/packager/react-packager/src/DependencyResolver/fastfs.js +++ b/packager/react-packager/src/DependencyResolver/fastfs.js @@ -14,6 +14,7 @@ const {EventEmitter} = require('events'); const fs = require('graceful-fs'); const path = require('path'); +const open = Promise.denodeify(fs.open); const readFile = Promise.denodeify(fs.readFile); const stat = Promise.denodeify(fs.stat); @@ -114,6 +115,14 @@ class Fastfs extends EventEmitter { return file.read(); } + readWhile(filePath, predicate) { + const file = this._getFile(filePath); + if (!file) { + throw new Error(`Unable to find file with path: ${filePath}`); + } + return file.readWhile(predicate); + } + closest(filePath, name) { for (let file = this._getFile(filePath).parent; file; @@ -241,6 +250,44 @@ class File { return this._read; } + readWhile(predicate) { + const CHUNK_SIZE = 512; + let result = ''; + + return open(this.path, 'r').then(fd => { + /* global Buffer: true */ + const buffer = new Buffer(CHUNK_SIZE); + const p = new Promise((resolve, reject) => { + let counter = 0; + const callback = (error, bytesRead) => { + if (error) { + reject(); + return; + } + + const chunk = buffer.toString('utf8', 0, bytesRead); + result += chunk; + if (bytesRead > 0 && predicate(chunk, counter++, result)) { + readChunk(fd, buffer, callback); + } else { + if (bytesRead === 0 && !this._read) { // reached EOF + this._read = Promise.resolve(result); + } + resolve(result); + } + }; + readChunk(fd, buffer, callback); + }); + + p.catch(() => fs.close(fd)); + return p; + }); + + function readChunk(fd, buffer, callback) { + fs.read(fd, buffer, 0, CHUNK_SIZE, null, callback); + } + } + stat() { if (!this._stat) { this._stat = stat(this.path);