Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add flag to follow symbolic links when crawling files #4387

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/en/CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ Alias: `-e`. Use this flag to show full diffs and errors instead of a patch.

Find and run the tests that cover a space separated list of source files that were passed in as arguments. Useful for pre-commit hook integration to run the minimal amount of tests necessary.

### `--followSymlinks`

Whether to follow symbolic linked files for file crawling. Linux/MacOSX only. Defaults to false.

### `--forceExit`

Force Jest to exit after all tests have completed running. This is useful when resources set up by test code cannot be adequately cleaned up. *Note: This feature is an escape-hatch. If Jest doesn't exit at the end of a test run, it means external resources are still being held on to or timers are still pending in your code. It is advised to tear down external resources after each test to make sure Jest can shut down cleanly.*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ exports[`--showConfig outputs config info and exits 1`] = `
\\"clover\\"
],
\\"expand\\": false,
\\"followSymlinks\\": false,
\\"listTests\\": false,
\\"mapCoverage\\": false,
\\"maxWorkers\\": \\"[maxWorkers]\\",
Expand Down
26 changes: 26 additions & 0 deletions integration_tests/__tests__/follow-symlinks.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Copyright (c) 2014-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.
*
* @flow
*/
'use strict';

const runJest = require('../runJest');
const {extractSummary} = require('../utils');

test("Don't follow symlinks by default", () => {
const {stdout} = runJest('follow-symlinks', []);
expect(stdout).toMatch('No tests found');
});

test('Follow symlinks when followSymlinks flag is used', () => {
const {stderr, stdout} = runJest('follow-symlinks', ['--followSymlinks']);
expect(stdout).not.toMatch('No tests found');
const {rest, summary} = extractSummary(stderr);
expect(rest).not.toMatch('Test suite failed to run');
expect(summary).toMatch('1 passed, 1 total');
});
3 changes: 3 additions & 0 deletions integration_tests/follow-symlinks/actual-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
test('actual-test', () => {
expect(true).toBeTruthy();
});
5 changes: 5 additions & 0 deletions integration_tests/follow-symlinks/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"jest": {
"testEnvironment": "node"
}
}
5 changes: 5 additions & 0 deletions packages/jest-cli/src/cli/args.js
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,11 @@ const options = {
'the minimal amount of tests necessary.',
type: 'boolean',
},
followSymlinks: {
default: undefined,
description: 'Whether to include symbolic links in file crawling.',
type: 'boolean',
},
forceExit: {
default: undefined,
description:
Expand Down
1 change: 1 addition & 0 deletions packages/jest-cli/src/cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ const _buildContextsAndHasteMaps = async (
createDirectory(config.cacheDirectory);
const hasteMapInstance = Runtime.createHasteMap(config, {
console: new Console(outputStream, outputStream),
followSymlinks: globalConfig.followSymlinks,
maxWorkers: globalConfig.maxWorkers,
resetCache: !config.cache,
watch: globalConfig.watch,
Expand Down
1 change: 1 addition & 0 deletions packages/jest-config/src/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ module.exports = ({
coveragePathIgnorePatterns: [NODE_MODULES_REGEXP],
coverageReporters: ['json', 'text', 'lcov', 'clover'],
expand: false,
followSymlinks: false,
globals: {},
haste: {
providesModuleNodeModules: [],
Expand Down
1 change: 1 addition & 0 deletions packages/jest-config/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ const getConfigs = (
coverageThreshold: options.coverageThreshold,
expand: options.expand,
findRelatedTests: options.findRelatedTests,
followSymlinks: options.followSymlinks,
forceExit: options.forceExit,
json: options.json,
lastCommit: options.lastCommit,
Expand Down
1 change: 1 addition & 0 deletions packages/jest-config/src/normalize.js
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,7 @@ function normalize(options: InitialOptions, argv: Argv) {
case 'expand':
case 'globals':
case 'findRelatedTests':
case 'followSymlinks':
case 'forceExit':
case 'listTests':
case 'logHeapUsage':
Expand Down
1 change: 1 addition & 0 deletions packages/jest-config/src/valid_config.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ module.exports = ({
},
displayName: 'project-name',
expand: false,
followSymlinks: false,
forceExit: false,
globals: {},
haste: {
Expand Down
122 changes: 102 additions & 20 deletions packages/jest-haste-map/src/crawlers/__tests__/node.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,35 +34,44 @@ jest.mock('child_process', () => ({

jest.mock('fs', () => {
let mtime = 32;
const stat = (path, callback) => {
setTimeout(
() =>
callback(null, {
isDirectory() {
return path.endsWith('/directory');
},
isSymbolicLink() {
return false;
},
mtime: {
getTime() {
return mtime++;
const fakeStat = ({isSymbolicLink}) => {
return jest.fn((path, callback) => {
setTimeout(
() =>
callback(null, {
isDirectory() {
return path.endsWith('/directory');
},
},
}),
0,
);
isSymbolicLink() {
return isSymbolicLink(path);
},
mtime: {
getTime() {
return mtime++;
},
},
}),
0,
);
});
};
return {
lstat: jest.fn(stat),
lstat: fakeStat({
isSymbolicLink: path => path.indexOf('symlink') !== -1,
}),
readdir: jest.fn((dir, callback) => {
if (dir === '/fruits') {
setTimeout(() => callback(null, ['directory', 'tomato.js']), 0);
} else if (dir === '/fruits/directory') {
setTimeout(() => callback(null, ['strawberry.js']), 0);
setTimeout(
() => callback(null, ['kiwi-symlink.js', 'strawberry.js']),
0,
);
}
}),
stat: jest.fn(stat),
stat: fakeStat({
isSymbolicLink: path => false,
}),
};
});

Expand Down Expand Up @@ -132,6 +141,59 @@ describe('node crawler', () => {
return promise;
});

it('crawls symlinks if followSymlinks flag is set to true', () => {
process.platform = 'linux';

childProcess = require('child_process');
nodeCrawl = require('../node');

mockResponse = [
'/fruits/pear.js',
'/fruits/strawberry.js',
'/fruits/tomato.js',
'/vegetables/melon.json',
].join('\n');

const promise = nodeCrawl({
data: {
files: Object.create(null),
},
extensions: ['js', 'json'],
followSymlinks: true,
ignore: pearMatcher,
roots: ['/fruits', '/vegtables'],
}).then(data => {
expect(childProcess.spawn).lastCalledWith('find', [
'/fruits',
'/vegtables',
'(',
'-type',
'f', // regular files
'-o',
'-type',
'l', // symbolic links
')',
'(',
'-iname',
'*.js',
'-o',
'-iname',
'*.json',
')',
]);

expect(data.files).not.toBe(null);

expect(data.files).toEqual({
'/fruits/strawberry.js': ['', 32, 0, []],
'/fruits/tomato.js': ['', 33, 0, []],
'/vegetables/melon.json': ['', 34, 0, []],
});
});

return promise;
});

it('updates only changed files', () => {
process.platform = 'linux';

Expand Down Expand Up @@ -199,6 +261,26 @@ describe('node crawler', () => {
});
});

it('crawls symlinks if followSymlinks flag is set to true and using node fs APIs', () => {
nodeCrawl = require('../node');

const files = Object.create(null);
return nodeCrawl({
data: {files},
extensions: ['js'],
followSymlinks: true,
forceNodeFilesystemAPI: true,
ignore: pearMatcher,
roots: ['/fruits'],
}).then(data => {
expect(data.files).toEqual({
'/fruits/directory/kiwi-symlink.js': ['', 34, 0, []],
'/fruits/directory/strawberry.js': ['', 33, 0, []],
'/fruits/tomato.js': ['', 32, 0, []],
});
});
});

it('completes with emtpy roots', () => {
process.platform = 'win32';

Expand Down
39 changes: 34 additions & 5 deletions packages/jest-haste-map/src/crawlers/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,25 @@ function find(
roots: Array<string>,
extensions: Array<string>,
ignore: IgnoreMatcher,
followSymlinks: boolean,
callback: Callback,
): void {
const result = [];
let activeCalls = 0;

let stat = fs.lstat;
if (followSymlinks) {
stat = (file, cb) => {
fs.lstat(file, (err, stat) => {
if (stat.isSymbolicLink()) {
fs.stat(file, cb);
} else {
cb(err, stat);
}
});
};
}

function search(directory: string): void {
activeCalls++;
fs.readdir(directory, (err, names) => {
Expand All @@ -39,7 +53,7 @@ function find(
}
activeCalls++;

fs.lstat(file, (err, stat) => {
stat(file, (err, stat) => {
activeCalls--;

if (!err && stat && !stat.isSymbolicLink()) {
Expand Down Expand Up @@ -75,10 +89,18 @@ function findNative(
roots: Array<string>,
extensions: Array<string>,
ignore: IgnoreMatcher,
followSymlinks: boolean,
callback: Callback,
): void {
const args = [].concat(roots);
args.push('-type', 'f');
if (followSymlinks) {
args.push('(');
}
args.push('-type', 'f'); // regular files
if (followSymlinks) {
args.push('-o', '-type', 'l'); // symbolic links
args.push(')');
}
if (extensions.length) {
args.push('(');
}
Expand Down Expand Up @@ -122,7 +144,14 @@ function findNative(
module.exports = function nodeCrawl(
options: CrawlerOptions,
): Promise<InternalHasteMap> {
const {data, extensions, forceNodeFilesystemAPI, ignore, roots} = options;
const {
data,
extensions,
followSymlinks,
forceNodeFilesystemAPI,
ignore,
roots,
} = options;

return new Promise(resolve => {
const callback = list => {
Expand All @@ -143,9 +172,9 @@ module.exports = function nodeCrawl(
};

if (forceNodeFilesystemAPI || process.platform === 'win32') {
find(roots, extensions, ignore, callback);
find(roots, extensions, ignore, followSymlinks, callback);
} else {
findNative(roots, extensions, ignore, callback);
findNative(roots, extensions, ignore, followSymlinks, callback);
}
});
};
11 changes: 11 additions & 0 deletions packages/jest-haste-map/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ type Options = {
cacheDirectory?: string,
console?: Console,
extensions: Array<string>,
followSymlinks?: boolean,
forceNodeFilesystemAPI?: boolean,
hasteImplModulePath?: string,
ignorePattern: HasteRegExp,
Expand All @@ -65,6 +66,7 @@ type Options = {
type InternalOptions = {
cacheDirectory: string,
extensions: Array<string>,
followSymlinks: boolean,
forceNodeFilesystemAPI: boolean,
hasteImplModulePath?: string,
ignorePattern: HasteRegExp,
Expand Down Expand Up @@ -207,6 +209,7 @@ class HasteMap extends EventEmitter {
this._options = {
cacheDirectory: options.cacheDirectory || os.tmpdir(),
extensions: options.extensions,
followSymlinks: !!options.followSymlinks,
forceNodeFilesystemAPI: !!options.forceNodeFilesystemAPI,
hasteImplModulePath: options.hasteImplModulePath,
ignorePattern: options.ignorePattern,
Expand All @@ -224,6 +227,12 @@ class HasteMap extends EventEmitter {
watch: !!options.watch,
};
this._console = options.console || global.console;
if (this._options.followSymlinks && this._options.useWatchman) {
this._console.warn(
'watchman can not be used with followSymlinks, watchman will be disabled',
);
this._options.useWatchman = false;
}
if (!(options.ignorePattern instanceof RegExp)) {
this._console.warn(
'jest-haste-map: the `ignorePattern` options as a function is being ' +
Expand Down Expand Up @@ -537,6 +546,7 @@ class HasteMap extends EventEmitter {
return nodeCrawl({
data: hasteMap,
extensions: options.extensions,
followSymlinks: options.followSymlinks,
forceNodeFilesystemAPI: options.forceNodeFilesystemAPI,
ignore,
roots: options.roots,
Expand All @@ -556,6 +566,7 @@ class HasteMap extends EventEmitter {
return crawl({
data: hasteMap,
extensions: options.extensions,
followSymlinks: options.followSymlinks,
forceNodeFilesystemAPI: options.forceNodeFilesystemAPI,
ignore,
roots: options.roots,
Expand Down
Loading