Skip to content

Commit

Permalink
Make jest-haste-map compute SHA-1s for excluded files too (as watchma…
Browse files Browse the repository at this point in the history
…n does) (#6264)
  • Loading branch information
mjesun committed May 30, 2018
1 parent 2307643 commit 2c971bd
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 52 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

* `[expect]` toMatchObject throws TypeError when a source property is null ([#6313](https://github.com/facebook/jest/pull/6313))
* `[jest-cli]` Normalize slashes in paths in CLI output on Windows ([#6310](https://github.com/facebook/jest/pull/6310))
* `[jest-haste-map`] Compute SHA-1s for non-tracked files when using Node crawler ([#6264](https://github.com/facebook/jest/pull/6264))

### Chore & Maintenance

Expand Down
81 changes: 81 additions & 0 deletions e2e/__tests__/haste_map_sha1.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

'use strict';

import os from 'os';
import path from 'path';
import JestHasteMap from 'jest-haste-map';
const {cleanup, writeFiles} = require('../Utils');

const DIR = path.resolve(os.tmpdir(), 'haste_map_sha1');

beforeEach(() => cleanup(DIR));
afterEach(() => cleanup(DIR));

test('exits the process after test are done but before timers complete', async () => {
writeFiles(DIR, {
'file.android.js': '"foo android"',
'file.ios.js': '"foo ios"',
'file.js': '"foo default"',
'file_with_extension.ignored': '"ignored file"',
'node_modules/bar/file_with_extension.ignored': '"ignored node modules"',
'node_modules/bar/image.png': '"an image"',
'node_modules/bar/index.js': '"node modules bar"',
});

const haste = new JestHasteMap({
computeSha1: true,
extensions: ['js', 'json', 'png'],
forceNodeFilesystemAPI: true,
ignorePattern: / ^/,
maxWorkers: 2,
mocksPattern: '',
name: 'tmp',
platforms: ['ios', 'android'],
retainAllFiles: true,
roots: [DIR],
useWatchman: false,
watch: false,
});

const {hasteFS} = await haste.build();

expect(hasteFS.getSha1(path.join(DIR, 'file.android.js'))).toBe(
'e376f9fd9a96d000fa019020159f996a8855f8bc',
);

expect(hasteFS.getSha1(path.join(DIR, 'file.ios.js'))).toBe(
'1271b4db2a5f47ae46cb01a1d0604a94d401e8f7',
);

expect(hasteFS.getSha1(path.join(DIR, 'file.js'))).toBe(
'c26c852220977244418f17a9fdc4ae9c192b3188',
);

expect(hasteFS.getSha1(path.join(DIR, 'node_modules/bar/image.png'))).toBe(
'8688f7e11f63d8a7eac7cb87af850337fabbd400',
);

expect(hasteFS.getSha1(path.join(DIR, 'node_modules/bar/index.js'))).toBe(
'ee245b9fbd45e1f6ad300eb2f5484844f6b5a34c',
);

// Ignored files do not get the SHA-1 computed.

expect(hasteFS.getSha1(path.join(DIR, 'file_with_extension.ignored'))).toBe(
null,
);

expect(
hasteFS.getSha1(
path.join(DIR, 'node_modules/bar/file_with_extension.ignored'),
),
).toBe(null);
});
24 changes: 20 additions & 4 deletions packages/jest-haste-map/src/__tests__/worker.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import ConditionalTest from '../../../../scripts/ConditionalTest';

import H from '../constants';

const {worker} = require('../worker');
const {worker, getSha1} = require('../worker');

let mockFs;
let readFileSync;
Expand Down Expand Up @@ -47,10 +47,8 @@ describe('worker', () => {

readFileSync = fs.readFileSync;
fs.readFileSync = jest.fn((path, options) => {
expect(options).toBe('utf8');

if (mockFs[path]) {
return mockFs[path];
return options === 'utf8' ? mockFs[path] : Buffer.from(mockFs[path]);
}

throw new Error(`Cannot read path '${path}'.`);
Expand Down Expand Up @@ -109,4 +107,22 @@ describe('worker', () => {

expect(error.message).toEqual(`Cannot read path '/kiwi.js'.`);
});

it('simply computes SHA-1s when requested', async () => {
expect(
await getSha1({computeSha1: false, filePath: '/fruits/banana.js'}),
).toEqual({sha1: null});

expect(
await getSha1({computeSha1: true, filePath: '/fruits/banana.js'}),
).toEqual({sha1: 'f24c6984cce6f032f6d55d771d04ab8dbbe63c8c'});

expect(
await getSha1({computeSha1: true, filePath: '/fruits/pear.js'}),
).toEqual({sha1: '1bf6fc618461c19553e27f8b8021c62b13ff614a'});

await expect(
getSha1({computeSha1: true, filePath: '/i/dont/exist.js'}),
).rejects.toThrow();
});
});
101 changes: 57 additions & 44 deletions packages/jest-haste-map/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import {execSync} from 'child_process';
import {version as VERSION} from '../package.json';
import {worker} from './worker';
import {getSha1, worker} from './worker';
import crypto from 'crypto';
import EventEmitter from 'events';
import getMockName from './get_mock_name';
Expand Down Expand Up @@ -86,7 +86,7 @@ type Watcher = {
close(callback: () => void): void,
};

type WorkerInterface = {worker: typeof worker};
type WorkerInterface = {worker: typeof worker, getSha1: typeof getSha1};

export type ModuleMap = HasteModuleMap;
export type FS = HasteFS;
Expand Down Expand Up @@ -407,9 +407,60 @@ class HasteMap extends EventEmitter {
moduleMap[platform] = module;
};

const fileMetadata = hasteMap.files[filePath];
const moduleMetadata = hasteMap.map[fileMetadata[H.ID]];
const computeSha1 = this._options.computeSha1 && !fileMetadata[H.SHA1];

// Callback called when the response from the worker is successful.
const workerReply = metadata => {
// `1` for truthy values instead of `true` to save cache space.
fileMetadata[H.VISITED] = 1;

const metadataId = metadata.id;
const metadataModule = metadata.module;

if (metadataId && metadataModule) {
fileMetadata[H.ID] = metadataId;
setModule(metadataId, metadataModule);
}

fileMetadata[H.DEPENDENCIES] = metadata.dependencies || [];

if (computeSha1) {
fileMetadata[H.SHA1] = metadata.sha1;
}
};

// Callback called when the response from the worker is an error.
const workerError = error => {
if (typeof error !== 'object' || !error.message || !error.stack) {
error = new Error(error);
error.stack = ''; // Remove stack for stack-less errors.
}

// $FlowFixMe: checking error code is OK if error comes from "fs".
if (!['ENOENT', 'EACCES'].includes(error.code)) {
throw error;
}

// If a file cannot be read we remove it from the file list and
// ignore the failure silently.
delete hasteMap.files[filePath];
};

// If we retain all files in the virtual HasteFS representation, we avoid
// reading them if they aren't important (node_modules).
if (this._options.retainAllFiles && this._isNodeModulesDir(filePath)) {
if (computeSha1) {
return this._getWorker(workerOptions)
.getSha1({
computeSha1,
filePath,
hasteImplModulePath: this._options.hasteImplModulePath,
})
.then(workerReply, workerError);
}

return null;
}

Expand All @@ -433,10 +484,6 @@ class HasteMap extends EventEmitter {
mocks[mockPath] = filePath;
}

const fileMetadata = hasteMap.files[filePath];
const moduleMetadata = hasteMap.map[fileMetadata[H.ID]];
const computeSha1 = this._options.computeSha1 && !fileMetadata[H.SHA1];

if (fileMetadata[H.VISITED]) {
if (!fileMetadata[H.ID]) {
return null;
Expand Down Expand Up @@ -467,41 +514,7 @@ class HasteMap extends EventEmitter {
filePath,
hasteImplModulePath: this._options.hasteImplModulePath,
})
.then(
metadata => {
// `1` for truthy values instead of `true` to save cache space.
fileMetadata[H.VISITED] = 1;

const metadataId = metadata.id;
const metadataModule = metadata.module;

if (metadataId && metadataModule) {
fileMetadata[H.ID] = metadataId;
setModule(metadataId, metadataModule);
}

fileMetadata[H.DEPENDENCIES] = metadata.dependencies || [];

if (computeSha1) {
fileMetadata[H.SHA1] = metadata.sha1;
}
},
error => {
if (typeof error !== 'object' || !error.message || !error.stack) {
error = new Error(error);
error.stack = ''; // Remove stack for stack-less errors.
}

// $FlowFixMe: checking error code is OK if error comes from "fs".
if (['ENOENT', 'EACCES'].indexOf(error.code) < 0) {
throw error;
}

// If a file cannot be read we remove it from the file list and
// ignore the failure silently.
delete hasteMap.files[filePath];
},
);
.then(workerReply, workerError);
}

_buildHasteMap(data: {
Expand Down Expand Up @@ -563,14 +576,14 @@ class HasteMap extends EventEmitter {
_getWorker(options: ?{forceInBand: boolean}): WorkerInterface {
if (!this._worker) {
if ((options && options.forceInBand) || this._options.maxWorkers <= 1) {
this._worker = {worker};
this._worker = {getSha1, worker};
} else {
// $FlowFixMe: assignment of a worker with custom properties.
this._worker = (new Worker(require.resolve('./worker'), {
exposedMethods: ['worker'],
exposedMethods: ['getSha1', 'worker'],
maxRetries: 3,
numWorkers: this._options.maxWorkers,
}): {worker: typeof worker});
}): WorkerInterface);
}
}

Expand Down
25 changes: 21 additions & 4 deletions packages/jest-haste-map/src/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ const PACKAGE_JSON = path.sep + 'package.json';
let hasteImpl: ?HasteImpl = null;
let hasteImplModulePath: ?string = null;

function computeSha1(content) {
return crypto
.createHash('sha1')
.update(content)
.digest('hex');
}

export async function worker(data: WorkerMessage): Promise<WorkerMetadata> {
if (
data.hasteImplModulePath &&
Expand Down Expand Up @@ -76,11 +83,21 @@ export async function worker(data: WorkerMessage): Promise<WorkerMetadata> {
content = fs.readFileSync(filePath);
}

sha1 = crypto
.createHash('sha1')
.update(content)
.digest('hex');
sha1 = computeSha1(content);
}

return {dependencies, id, module, sha1};
}

export async function getSha1(data: WorkerMessage): Promise<WorkerMetadata> {
const sha1 = data.computeSha1
? computeSha1(fs.readFileSync(data.filePath))
: null;

return {
dependencies: undefined,
id: undefined,
module: undefined,
sha1,
};
}

0 comments on commit 2c971bd

Please sign in to comment.