Skip to content

Commit

Permalink
New and fixed implementation for jest —onlyChanged:
Browse files Browse the repository at this point in the history
* Look up changed files starting from test files rather than changed files
* Deduplicate testPathDirs and changed test files
* Use the git root dir for tests, not the test path dir so we get correct absolute paths (—onlyChanged *never* worked for jest itself, what!?)
* This might not perform as well as it did before; we’ll see and I might rewrite this again, but for now this works pretty well.
* Simplify the tests. I’m going to make them more robust in a follow-up but most of the things the tests were covering is handled by DependencyGraph
  • Loading branch information
cpojer committed Nov 14, 2015
1 parent a016f09 commit 96ab6e9
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 240 deletions.
55 changes: 0 additions & 55 deletions src/HasteModuleLoader/HasteModuleLoader.js
Expand Up @@ -311,61 +311,6 @@ class Loader {
return collector ? collector.extractRuntimeCoverageInfo() : null;
}

getDependentsFromPath(modulePath) {
const _getRealPathFromNormalizedModuleID = moduleID => {
return moduleID.split(path.delimiter)[1];
};
const _getDependencyPathsFromResource = resource => {
const dependencyPaths = [];
for (let i = 0; i < resource.requiredModules.length; i++) {
let requiredModule = resource.requiredModules[i];

// *facepalm* node-haste is pretty clowny
if (resource.getModuleIDByOrigin) {
requiredModule =
resource.getModuleIDByOrigin(requiredModule) || requiredModule;
}

let moduleID;
try {
moduleID = this._getNormalizedModuleID(resource.path, requiredModule);
} catch (e) {
continue;
}

dependencyPaths.push(_getRealPathFromNormalizedModuleID(moduleID));
}
return dependencyPaths;
};

// TODO
if (this._reverseDependencyMap == null) {
const reverseDepMap = this._reverseDependencyMap = Object.create(null);
const allResources = [];
Object.keys(allResources).forEach(resourceID => {
const resource = allResources[resourceID];
if (
resource.type === 'ProjectConfiguration' ||
resource.type === 'Resource'
) {
return;
}

const dependencyPaths = _getDependencyPathsFromResource(resource);
for (let i = 0; i < dependencyPaths.length; i++) {
const requiredModulePath = dependencyPaths[i];
if (!reverseDepMap[requiredModulePath]) {
reverseDepMap[requiredModulePath] = Object.create(null);
}
reverseDepMap[requiredModulePath][resource.path] = true;
}
});
}

const reverseDeps = this._reverseDependencyMap[modulePath];
return reverseDeps ? Object.keys(reverseDeps) : [];
}

_execModule(moduleObj) {
// If the environment was disposed, prevent this module from
// being executed.
Expand Down
55 changes: 22 additions & 33 deletions src/TestRunner.js
Expand Up @@ -129,48 +129,37 @@ class TestRunner {
return this._configDeps;
}

promiseTestPathsRelatedTo(paths) {
promiseTestPathsRelatedTo(changedPaths) {
// TODO switch this to use HasteResolver instead of a ModuleLoader instance.
return this._constructModuleLoader()
.then(moduleLoader => {
const relatedPaths = [];
const discoveredModules = {};

// If a path to a test file is given, make sure we consider that test as
// related to itself. Non-tests will be filtered at the end.
paths.forEach(path => {
discoveredModules[path] = true;
if (this._isTestFilePath(path) && fs.existsSync(path)) {
relatedPaths.push(path);
}
});

const modulesToSearch = [].concat(paths);
while (modulesToSearch.length > 0) {
const modulePath = modulesToSearch.shift();
const depPaths = moduleLoader.getDependentsFromPath(modulePath);

depPaths.forEach(depPath => {
if (!discoveredModules.hasOwnProperty(depPath)) {
discoveredModules[depPath] = true;
modulesToSearch.push(depPath);
if (this._isTestFilePath(depPath) && fs.existsSync(depPath)) {
relatedPaths.push(depPath);
.then(moduleLoader => moduleLoader.getAllTestPaths().then(paths => {
const allTests = {};
return Promise.all(
paths.map(path => moduleLoader._resolver.getDependencies(path)
.then(deps => allTests[path] = deps)
)
).then(() => {
const relatedPaths = new Set();
for (const path in allTests) {
if (this._isTestFilePath(path)) {
for (const resourcePath in allTests[path].resources) {
if (changedPaths.has(resourcePath)) {
relatedPaths.add(path);
}
}
}
});
}
return relatedPaths;
});
}
return Array.from(relatedPaths);
});
}));
}

promiseTestPathsMatching(pathPattern) {
// TODO switch this to use HasteResolver instead of a ModuleLoader instance.
return this._constructModuleLoader()
.then(moduleLoader => moduleLoader.getAllTestPaths())
.then(testPaths => testPaths.filter(
path => (
this._isTestFilePath(path) &&
pathPattern.test(path)
)
path => this._isTestFilePath(path) && pathPattern.test(path)
));
}

Expand Down
170 changes: 46 additions & 124 deletions src/__tests__/TestRunner-test.js
Expand Up @@ -11,20 +11,19 @@

jest.autoMockOff().mock('fs');

var name = 'TestRunner';
describe('TestRunner', function() {
var TestRunner;
const name = 'TestRunner';
describe('TestRunner', () => {
let TestRunner;

beforeEach(function() {
beforeEach(() => {
TestRunner = require('../TestRunner');
});

describe('_isTestFilePath', function() {
var runner;
var utils;
describe('_isTestFilePath', () => {
let runner;

beforeEach(function() {
utils = require('../lib/utils');
beforeEach(() => {
const utils = require('../lib/utils');
runner = new TestRunner(utils.normalizeConfig({
cacheDirectory: global.CACHE_DIRECTORY,
name,
Expand All @@ -33,149 +32,72 @@ describe('TestRunner', function() {
}));
});

it('supports ../ paths and unix separators', function() {
var path = '/path/to/__tests__/foo/bar/baz/../../../test.js';
var isTestFile = runner._isTestFilePath(path);
it('supports ../ paths and unix separators', () => {
const path = '/path/to/__tests__/foo/bar/baz/../../../test.js';
const isTestFile = runner._isTestFilePath(path);

return expect(isTestFile).toEqual(true);
});

it('supports unix separators', function() {
var path = '/path/to/__tests__/test.js';
var isTestFile = runner._isTestFilePath(path);
it('supports unix separators', () => {
const path = '/path/to/__tests__/test.js';
const isTestFile = runner._isTestFilePath(path);

return expect(isTestFile).toEqual(true);
});

});

describe('promiseTestPathsRelatedTo', function() {
var fakeDepsFromPath;
var fs;
var runner;
var utils;

beforeEach(function() {
fs = require('graceful-fs');
utils = require('../lib/utils');
describe('promiseTestPathsRelatedTo', () => {
const allTests = [
'__tests__/a.js',
'__tests__/b.js',
];
const dependencyGraph = {
'__tests__/a.js': {resources: {}},
'__tests__/b.js': {resources: {
'b.js': {},
}},
};
let runner;

beforeEach(() => {
const utils = require('../lib/utils');
runner = new TestRunner(utils.normalizeConfig({
cacheDirectory: global.CACHE_DIRECTORY,
name,
rootDir: '.',
testPathDirs: [],
}));

fakeDepsFromPath = {};
runner._constructModuleLoader = function() {
runner._isTestFilePath = () => true;
runner._constructModuleLoader = () => {
return Promise.resolve({
getDependentsFromPath: function(modulePath) {
return fakeDepsFromPath[modulePath] || [];
getAllTestPaths:
modulePath => Promise.resolve(allTests),
_resolver: {
getDependencies: path => {
return Promise.resolve(dependencyGraph[path]);
},
},
});
};
});

pit('finds no tests when no tests depend on the path', function() {
var path = '/path/to/module/not/covered/by/any/tests.js';
fakeDepsFromPath[path] = [];

// Mock out existsSync to return true, since our test path isn't real
fs.existsSync = function() { return true; };
pit('finds no tests when no tests depend on the path', () => {
const path = 'a.js';

return runner.promiseTestPathsRelatedTo([path])
.then(function(relatedTests) {
return runner.promiseTestPathsRelatedTo(new Set([path]))
.then(relatedTests => {
expect(relatedTests).toEqual([]);
});
});

pit('finds tests that depend directly on the path', function() {
var path = '/path/to/module/covered/by/one/test.js';
var dependentTestPath = '/path/to/test/__tests__/asdf-test.js';
fakeDepsFromPath[path] = [dependentTestPath];

// Mock out existsSync to return true, since our test path isn't real
fs.existsSync = function() { return true; };

return runner.promiseTestPathsRelatedTo([path])
.then(function(relatedTests) {
expect(relatedTests).toEqual([dependentTestPath]);
});
});

pit('finds tests that depend indirectly on the path', function() {
var path = '/path/to/module/covered/by/module/covered/by/test.js';
var dependentModulePath = '/path/to/dependent/module.js';
var dependentTestPath = '/path/to/test/__tests__/asdf-test.js';
fakeDepsFromPath[path] = [dependentModulePath];
fakeDepsFromPath[dependentModulePath] = [dependentTestPath];

// Mock out existsSync to return true, since our test path isn't real
fs.existsSync = function() { return true; };

return runner.promiseTestPathsRelatedTo([path])
.then(function(relatedTests) {
expect(relatedTests).toEqual([dependentTestPath]);
});
});

pit('finds multiple tests that depend indirectly on the path', function() {
var path = '/path/to/module/covered/by/modules/covered/by/test.js';
var dependentModulePath1 = '/path/to/dependent/module1.js';
var dependentModulePath2 = '/path/to/dependent/module2.js';
var dependentTestPath1 = '/path/to/test1/__tests__/asdf1-test.js';
var dependentTestPath2 = '/path/to/test2/__tests__/asdf2-test.js';
fakeDepsFromPath[path] = [dependentModulePath1, dependentModulePath2];
fakeDepsFromPath[dependentModulePath1] = [dependentTestPath1];
fakeDepsFromPath[dependentModulePath2] = [dependentTestPath2];

// Mock out existsSync to return true, since our test path isn't real
fs.existsSync = function() { return true; };

return runner.promiseTestPathsRelatedTo([path])
.then(function(relatedTests) {
expect(relatedTests).toEqual([
dependentTestPath1,
dependentTestPath2,
]);
});
});

pit('flattens circular dependencies', function() {
var path = '/path/to/module/covered/by/modules/covered/by/test.js';
var directDependentModulePath = '/path/to/direct/dependent/module.js';
var indirectDependentModulePath = '/path/to/indirect/dependent/module.js';
var dependentTestPath = '/path/to/test/__tests__/asdf-test.js';
fakeDepsFromPath[path] = [directDependentModulePath];
fakeDepsFromPath[directDependentModulePath] =
[indirectDependentModulePath];
fakeDepsFromPath[indirectDependentModulePath] = [
directDependentModulePath,
dependentTestPath,
];

// Mock out existsSync to return true, since our test path isn't real
fs.existsSync = function() { return true; };

return runner.promiseTestPathsRelatedTo([path])
.then(function(relatedTests) {
expect(relatedTests).toEqual([dependentTestPath]);
});
});

pit('filters test paths that don\'t exist on the filesystem', function() {
var path = '/path/to/module/covered/by/one/test.js';
var existingTestPath = '/path/to/test/__tests__/exists-test.js';
var nonExistantTestPath = '/path/to/test/__tests__/doesnt-exist-test.js';
fakeDepsFromPath[path] = [existingTestPath, nonExistantTestPath];

// Mock out existsSync to return true, since our test path isn't real
fs.existsSync = function(path) {
return path !== nonExistantTestPath;
};

return runner.promiseTestPathsRelatedTo([path])
.then(function(relatedTests) {
expect(relatedTests).toEqual([existingTestPath]);
pit('finds tests that depend directly on the path', () => {
const path = 'b.js';
return runner.promiseTestPathsRelatedTo(new Set([path]))
.then(relatedTests => {
expect(relatedTests).toEqual(['__tests__/b.js']);
});
});
});
Expand Down

0 comments on commit 96ab6e9

Please sign in to comment.