Skip to content

Commit

Permalink
Use one TestSequencer instance for all contexts.
Browse files Browse the repository at this point in the history
  • Loading branch information
cpojer committed Apr 18, 2017
1 parent a663e2e commit 7d64794
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 67 deletions.
90 changes: 44 additions & 46 deletions packages/jest-cli/src/TestSequencer.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

import type {AggregatedResult} from 'types/TestResult';
import type {Context} from 'types/Context';
import type {Tests} from 'types/TestRunner';
import type {Test, Tests} from 'types/TestRunner';

const fs = require('fs');
const getCacheFilePath = require('jest-haste-map').getCacheFilePath;
Expand All @@ -24,19 +24,31 @@ type Cache = {
};

class TestSequencer {
_context: Context;
_cache: Cache;
_cache: Map<Context, Cache>;

constructor(context: Context) {
this._context = context;
this._cache = {};
constructor() {
this._cache = new Map();
}

_getTestPerformanceCachePath() {
const {config} = this._context;
_getCachePath(context: Context) {
const {config} = context;
return getCacheFilePath(config.cacheDirectory, 'perf-cache-' + config.name);
}

_getCache(test: Test) {
const {context} = test;
if (!this._cache.has(context) && context.config.cache) {
try {
this._cache.set(
context,
JSON.parse(fs.readFileSync(this._getCachePath(context), 'utf8')),
);
} catch (e) {}
}

return this._cache.get(context) || {};
}

// When running more tests than we have workers available, sort the tests
// by size - big test files usually take longer to complete, so we run
// them first in an effort to minimize worker idle time at the end of a
Expand All @@ -47,62 +59,48 @@ class TestSequencer {
// fastest results.
sort(tests: Tests): Tests {
const stats = {};
const fileSize = filePath =>
stats[filePath] || (stats[filePath] = fs.statSync(filePath).size);
const failed = filePath =>
this._cache[filePath] && this._cache[filePath][0] === FAIL;
const time = filePath => this._cache[filePath] && this._cache[filePath][1];
const fileSize = test =>
stats[test.path] || (stats[test.path] = fs.statSync(test.path).size);
const hasFailed = (cache, test) =>
cache[test.path] && cache[test.path][0] === FAIL;
const time = (cache, test) => cache[test.path] && cache[test.path][1];

this._cache = {};
try {
if (this._context.config.cache) {
this._cache = JSON.parse(
fs.readFileSync(this._getTestPerformanceCachePath(), 'utf8'),
);
}
} catch (e) {}

tests = tests.sort(({path: pathA}, {path: pathB}) => {
const failedA = failed(pathA);
const failedB = failed(pathB);
tests.forEach(test => test.duration = time(this._getCache(test), test));
return tests.sort((testA, testB) => {
const cacheA = this._getCache(testA);
const cacheB = this._getCache(testB);
const failedA = hasFailed(cacheA, testA);
const failedB = hasFailed(cacheB, testB);
const hasTimeA = testA.duration != null;
if (failedA !== failedB) {
return failedA ? -1 : 1;
} else if (hasTimeA != (testB.duration != null)) {
// Check if only one of two tests has timing information
return hasTimeA != null ? 1 : -1;
} else if (testA.duration != null && testB.duration != null) {
return testA.duration < testB.duration ? 1 : -1;
} else {
return fileSize(testA) < fileSize(testB) ? 1 : -1;
}
const timeA = time(pathA);
const timeB = time(pathB);
const hasTimeA = timeA != null;
const hasTimeB = timeB != null;
// Check if only one of two tests has timing information
if (hasTimeA != hasTimeB) {
return hasTimeA ? 1 : -1;
}
if (timeA != null && !timeB != null) {
return timeA < timeB ? 1 : -1;
}
return fileSize(pathA) < fileSize(pathB) ? 1 : -1;
});

tests.forEach(test => test.duration = time(test.path));
return tests;
}

cacheResults(tests: Tests, results: AggregatedResult) {
const cache = this._cache;
const map = Object.create(null);
tests.forEach(({path}) => map[path] = true);
tests.forEach(test => map[test.path] = test);
results.testResults.forEach(testResult => {
if (testResult && map[testResult.testFilePath] && !testResult.skipped) {
const cache = this._getCache(map[testResult.testFilePath]);
const perf = testResult.perfStats;
cache[testResult.testFilePath] = [
testResult.numFailingTests ? FAIL : SUCCESS,
perf.end - perf.start || 0,
];
}
});
fs.writeFileSync(
this._getTestPerformanceCachePath(),
JSON.stringify(cache),
);

this._cache.forEach((cache, context) =>
fs.writeFileSync(this._getCachePath(context), JSON.stringify(cache)));
}
}

Expand Down
66 changes: 65 additions & 1 deletion packages/jest-cli/src/__tests__/TestSequencer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ const context = {
},
};

const secondContext = {
config: {
cache: true,
cacheDirectory: '/cache2',
name: 'test2',
},
};

const toTests = paths =>
paths.map(path => ({
context,
Expand All @@ -34,10 +42,11 @@ const toTests = paths =>
}));

beforeEach(() => {
sequencer = new TestSequencer(context);
sequencer = new TestSequencer();

fs.readFileSync = jest.fn(() => '{}');
fs.statSync = jest.fn(filePath => ({size: filePath.length}));
fs.writeFileSync = jest.fn();
});

test('sorts by file size if there is no timing information', () => {
Expand Down Expand Up @@ -149,3 +158,58 @@ test('writes the cache based on the results', () => {
'/test-c.js': [FAIL, 3],
});
});

test('works with multiple contexts', () => {
fs.readFileSync = jest.fn(
cacheName =>
cacheName.startsWith('/cache/')
? JSON.stringify({
'/test-a.js': [SUCCESS, 5],
'/test-b.js': [FAIL, 1],
})
: JSON.stringify({
'/test-c.js': [FAIL],
}),
);

const testPaths = [
{context, duration: null, path: '/test-a.js'},
{context, duration: null, path: '/test-b.js'},
{context: secondContext, duration: null, path: '/test-c.js'},
];
const tests = sequencer.sort(testPaths);
sequencer.cacheResults(tests, {
testResults: [
{
numFailingTests: 0,
perfStats: {end: 2, start: 1},
testFilePath: '/test-a.js',
},
{
numFailingTests: 0,
perfStats: {end: 0, start: 0},
skipped: true,
testFilePath: '/test-b.js',
},
{
numFailingTests: 0,
perfStats: {end: 4, start: 1},
testFilePath: '/test-c.js',
},
{
numFailingTests: 1,
perfStats: {end: 2, start: 1},
testFilePath: '/test-x.js',
},
],
});
const fileDataA = JSON.parse(fs.writeFileSync.mock.calls[0][1]);
expect(fileDataA).toEqual({
'/test-a.js': [SUCCESS, 1],
'/test-b.js': [FAIL, 1],
});
const fileDataB = JSON.parse(fs.writeFileSync.mock.calls[1][1]);
expect(fileDataB).toEqual({
'/test-c.js': [SUCCESS, 3],
});
});
29 changes: 9 additions & 20 deletions packages/jest-cli/src/runJest.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
'use strict';

import type {Context} from 'types/Context';
import type {Test} from 'types/TestRunner';
import type TestWatcher from './TestWatcher';

const fs = require('graceful-fs');
Expand Down Expand Up @@ -114,7 +113,6 @@ const getTestPaths = async (context, pattern, argv, pipe) => {
}
}
}

return data;
};

Expand Down Expand Up @@ -150,31 +148,23 @@ const runJest = async (
startRun: () => *,
onComplete: (testResults: any) => void,
) => {
const maxWorkers = getMaxWorkers(argv);
const context = contexts[0];
const maxWorkers = getMaxWorkers(argv);
const pattern = getTestPathPattern(argv);
const sequencer = new TestSequencer();
let allTests = [];
const testRunData = await Promise.all(
contexts.map(async context => {
const matches = await getTestPaths(context, pattern, argv, pipe);
const sequencer = new TestSequencer(context);
const tests = sequencer.sort(matches.tests);
return {context, matches, sequencer, tests};
allTests = allTests.concat(matches.tests);
return {context, matches};
}),
);

const allTests = testRunData
.reduce((tests, testRun) => tests.concat(testRun.tests), [])
.sort((a: Test, b: Test) => {
if (a.duration != null && b.duration != null) {
return a.duration < b.duration ? 1 : -1;
}
return a.duration == null ? 1 : 0;
});

allTests = sequencer.sort(allTests);
if (!allTests.length) {
new Console(pipe, pipe).log(getNoTestsFoundMessage(testRunData, pattern));
}

if (
} else if (
allTests.length === 1 &&
context.config.silent !== true &&
context.config.verbose !== false
Expand All @@ -200,8 +190,7 @@ const runJest = async (
testPathPattern: formatTestPathPattern(pattern),
}).runTests(allTests, testWatcher);

testRunData.forEach(({sequencer, tests}) =>
sequencer.cacheResults(tests, results));
sequencer.cacheResults(allTests, results);

return processResults(results, {
isJSON: argv.json,
Expand Down

0 comments on commit 7d64794

Please sign in to comment.