Skip to content

Commit 05a356d

Browse files
nicojssimondel
authored andcommitted
feat(exclude-files): Exclude files with a ! (#188)
* feat(exclude-files): Exclude files with a `!` * Refactor `InputFileResolver`, included a new class `PatternResolver` in order to reuse functionality for both `mutate` and `files`. * Ignore patterns starting with an `!` as long as they are provided as strings * Deduplicate files, first occurance wins. * fix(deps): Pin typescript version * refactor(InputFileResolver): Move global functions Move global functions to be static methods on the 2 classes. * refactor(input-file-resolver): Static -> instance * Move static methods to be instance methods
1 parent 28e1e5d commit 05a356d

File tree

5 files changed

+162
-88
lines changed

5 files changed

+162
-88
lines changed

README.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,25 +59,28 @@ The mutators that are supported by Stryker can be found on [our website](http://
5959
All options can be configured either via the command line or via a config file.
6060

6161
Both `files` and `mutate` support globbing expressions using [node glob](https://github.com/isaacs/node-glob). This is the same globbing format you might know from
62-
[Grunt](https://github.com/gruntjs/grunt) and [Karma](https://github.com/karma-runner/karma).
62+
[Grunt](https://github.com/gruntjs/grunt) and [Karma](https://github.com/karma-runner/karma).
63+
It is possible to *ignore* files by adding an exclamation mark `!` to the start of the expression.
6364

6465
#### Files
6566
**Command line:** `--files node_modules/a-lib/**/*.js,src/**/*.js,a.js,test/**/*.js` or `-f node_modules/a-lib/**/*.js,src/**/*.js,a.js,test/**/*.js`
66-
**Config file:** `files: ['test/helpers/**/*.js', 'test/unit/**/*.js', { pattern: 'src/**/*.js', included: false, mutated: true }]`
67+
**Config file:** `files: ['{ pattern: 'src/**/*.js', mutated: true }, '!src/**/index.js', 'test/**/*.js']`
6768
**Default value:** *none*
6869
**Description:**
6970
With `files` you configure all files needed to run the tests. If the test runner you use already provides the test framework (jasmine, mocha, etc),
7071
you should not add those files here as well. The order in this list is important, because that will be the order in which the files are loaded.
7172

7273
When using the command line, the list can only contain a comma seperated list of globbing expressions.
73-
When using the config file you can fill an array with strings or objects:
74+
When using the config file you can fill an array with `string`s or `InputFileDescriptor` objects:
7475

75-
* `string`: A globbing expression used for selecting the files needed to run the tests.
76-
* `{ pattern: 'pattern', included: true, mutated: false }` :
77-
* The `pattern` property is mandatory and contains the globbing expression used for selecting the files
76+
* `string`: The globbing expression used for selecting the files needed to run the tests.
77+
* `InputFileDescriptor` object: `{ pattern: 'pattern', included: true, mutated: false }` :
78+
* The `pattern` property is mandatory and contains the globbing expression used for selecting the files. Using `!` to ignore files is *not* supported here.
7879
* The `included` property is optional and determines whether or not this file should be loaded initially by the test-runner (default: true)
7980
* The `mutated` property is optional and determines whether or not this file should be targeted for mutations (default: false)
8081

82+
*Note*: To include a file/folder which start with an exclamation mark (`!`), use the `InputFileDescriptor` syntax
83+
8184
#### Files to mutate
8285
**Command line:** `-m src/**/*.js,a.js` or `--mutate src/**/*.js,a.js`
8386
**Config file:** `mutate: ['src/**/*.js', 'a.js']`

src/InputFileResolver.ts

Lines changed: 93 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,41 @@ import * as _ from 'lodash';
44
import * as log4js from 'log4js';
55

66
const log = log4js.getLogger('InputFileResolver');
7+
78
const DEFAULT_INPUT_FILE_PROPERTIES = { mutated: false, included: true };
89

910
export default class InputFileResolver {
1011

11-
private inputFileDescriptors: InputFileDescriptor[];
12-
private mutateFileExpressions: string[];
12+
private inputFileResolver: PatternResolver;
13+
private mutateResolver: PatternResolver;
1314

1415
constructor(mutate: string[], allFileExpressions: Array<InputFileDescriptor | string>) {
15-
this.mutateFileExpressions = mutate || [];
16-
this.inputFileDescriptors = allFileExpressions.map(maybePattern => {
17-
if (InputFileResolver.isInputFileDescriptor(maybePattern)) {
18-
return maybePattern;
19-
} else {
20-
return <InputFileDescriptor>_.assign({ pattern: maybePattern }, DEFAULT_INPUT_FILE_PROPERTIES);
21-
}
22-
});
16+
this.validateFileDescriptor(allFileExpressions);
17+
this.mutateResolver = PatternResolver.parse(mutate || []);
18+
this.inputFileResolver = PatternResolver.parse(allFileExpressions);
2319
}
2420

2521
public resolve(): Promise<InputFile[]> {
26-
let mutateFilePromise = this.resolveMutateFileGlobs();
27-
return this.resolveInputFileGlobs().then((allInputFiles) => mutateFilePromise.then(additionalMutateFiles => {
28-
InputFileResolver.markAdditionalFilesToMutate(allInputFiles, additionalMutateFiles);
29-
InputFileResolver.warnAboutNoFilesToMutate(allInputFiles);
30-
return allInputFiles;
31-
}));
22+
return Promise.all([this.inputFileResolver.resolve(), this.mutateResolver.resolve()]).then(results => {
23+
const inputFiles = results[0];
24+
const mutateFiles = results[1];
25+
this.markAdditionalFilesToMutate(inputFiles, mutateFiles.map(m => m.path));
26+
this.logFilesToMutate(inputFiles);
27+
return inputFiles;
28+
});
29+
}
30+
31+
private validateFileDescriptor(maybeInputFileDescriptors: Array<InputFileDescriptor | string>) {
32+
maybeInputFileDescriptors.forEach(maybeInputFileDescriptor => {
33+
if (_.isObject(maybeInputFileDescriptor)) {
34+
if (Object.keys(maybeInputFileDescriptor).indexOf('pattern') === -1) {
35+
throw Error(`File descriptor ${JSON.stringify(maybeInputFileDescriptor)} is missing mandatory property 'pattern'.`);
36+
}
37+
}
38+
});
3239
}
3340

34-
private static markAdditionalFilesToMutate(allInputFiles: InputFile[], additionalMutateFiles: string[]) {
41+
private markAdditionalFilesToMutate(allInputFiles: InputFile[], additionalMutateFiles: string[]) {
3542
let errors: string[] = [];
3643
additionalMutateFiles.forEach(mutateFile => {
3744
if (!allInputFiles.filter(inputFile => inputFile.path === mutateFile).length) {
@@ -43,57 +50,97 @@ export default class InputFileResolver {
4350
}
4451
allInputFiles.forEach(file => file.mutated = additionalMutateFiles.some(mutateFile => mutateFile === file.path) || file.mutated);
4552
}
46-
private static warnAboutNoFilesToMutate(allInputFiles: InputFile[]) {
53+
54+
private logFilesToMutate(allInputFiles: InputFile[]) {
4755
let mutateFiles = allInputFiles.filter(file => file.mutated);
4856
if (mutateFiles.length) {
49-
log.info(`Found ${mutateFiles.length} file(s) to be mutated.`);
57+
log.info(`Found ${mutateFiles.length} of ${allInputFiles.length} file(s) to be mutated.`);
5058
} else {
5159
log.warn(`No files marked to be mutated, stryker will perform a dry-run without actually mutating anything.`);
5260
}
61+
if (log.isDebugEnabled) {
62+
log.debug('All input files in order:%s', allInputFiles.map(file => '\n\t' + JSON.stringify(file)));
63+
}
5364
}
65+
}
5466

55-
private static reportEmptyGlobbingExpression(expression: string) {
56-
log.warn(`Globbing expression "${expression}" did not result in any files.`);
57-
}
67+
class PatternResolver {
5868

59-
private static isInputFileDescriptor(maybeInputFileDescriptor: InputFileDescriptor | string): maybeInputFileDescriptor is InputFileDescriptor {
60-
if (_.isObject(maybeInputFileDescriptor)) {
61-
if (Object.keys(maybeInputFileDescriptor).indexOf('pattern') > -1) {
62-
return true;
63-
} else {
64-
throw Error(`File descriptor ${JSON.stringify(maybeInputFileDescriptor)} is missing mandatory property 'pattern'.`);
69+
private ignore = false;
70+
private descriptor: InputFileDescriptor;
71+
72+
constructor(descriptor: InputFileDescriptor | string, private previous?: PatternResolver) {
73+
if (typeof descriptor === 'string') {
74+
this.descriptor = <InputFileDescriptor>_.assign({ pattern: descriptor }, DEFAULT_INPUT_FILE_PROPERTIES);
75+
this.ignore = descriptor.indexOf('!') === 0;
76+
if (this.ignore) {
77+
this.descriptor.pattern = descriptor.substring(1);
6578
}
6679
} else {
67-
return false;
80+
this.descriptor = descriptor;
6881
}
6982
}
7083

71-
private resolveMutateFileGlobs(): Promise<string[]> {
72-
return Promise.all(this.mutateFileExpressions.map(InputFileResolver.resolveFileGlob))
73-
.then(files => _.flatten(files));
84+
resolve(): Promise<InputFile[]> {
85+
// When the first expression starts with an '!', we skip that one
86+
if (this.ignore && !this.previous) {
87+
return Promise.resolve([]);
88+
} else {
89+
// Start the globbing task for the current descriptor
90+
const globbingTask = this.resolveFileGlob(this.descriptor.pattern)
91+
.then(filePaths => filePaths.map(filePath => this.createInputFile(filePath)));
92+
if (this.previous) {
93+
// If there is a previous globbing expression, resolve that one as well
94+
return Promise.all([this.previous.resolve(), globbingTask]).then(results => {
95+
const previousFiles = results[0];
96+
const currentFiles = results[1];
97+
// If this expression started with a '!', exclude current files
98+
if (this.ignore) {
99+
return previousFiles.filter(previousFile => currentFiles.some(currentFile => previousFile.path !== currentFile.path));
100+
} else {
101+
// Only add files which were not already added
102+
return previousFiles.concat(currentFiles.filter(currentFile => !previousFiles.some(file => file.path === currentFile.path)));
103+
}
104+
});
105+
} else {
106+
return globbingTask;
107+
}
108+
}
74109
}
75110

76-
private resolveInputFileGlobs(): Promise<InputFile[]> {
77-
return Promise.all(
78-
this.inputFileDescriptors.map(descriptor => InputFileResolver.resolveFileGlob(descriptor.pattern)
79-
.then(sourceFiles => sourceFiles.map(sourceFile => InputFileResolver.createInputFile(sourceFile, descriptor))))
80-
).then(promises => _.flatten(promises));
111+
static empty(): PatternResolver {
112+
const emptyResolver = new PatternResolver('');
113+
emptyResolver.ignore = true;
114+
return emptyResolver;
81115
}
82116

83-
private static createInputFile(path: string, descriptor: InputFileDescriptor): InputFile {
84-
let inputFile: InputFile = <InputFile>_.assign({ path }, DEFAULT_INPUT_FILE_PROPERTIES, descriptor);
85-
delete (<any>inputFile)['pattern'];
86-
return inputFile;
117+
static parse(inputFileExpressions: Array<string | InputFileDescriptor>): PatternResolver {
118+
const expressions = inputFileExpressions.map(i => i); // work on a copy as we're changing the array state
119+
let current = PatternResolver.empty();
120+
while (expressions.length) {
121+
current = new PatternResolver(expressions.shift(), current);
122+
}
123+
return current;
87124
}
88125

89-
90-
private static resolveFileGlob(expression: string): Promise<string[]> {
91-
return glob(expression).then(files => {
126+
private resolveFileGlob(pattern: string): Promise<string[]> {
127+
return glob(pattern).then(files => {
92128
if (files.length === 0) {
93-
this.reportEmptyGlobbingExpression(expression);
129+
this.reportEmptyGlobbingExpression(pattern);
94130
}
95131
normalize(files);
96132
return files;
97133
});
98134
}
99-
}
135+
136+
private reportEmptyGlobbingExpression(expression: string) {
137+
log.warn(`Globbing expression "${expression}" did not result in any files.`);
138+
}
139+
140+
private createInputFile(path: string): InputFile {
141+
let inputFile: InputFile = <InputFile>_.assign({ path }, DEFAULT_INPUT_FILE_PROPERTIES, this.descriptor);
142+
delete (<any>inputFile)['pattern'];
143+
return inputFile;
144+
}
145+
}
146+

src/stryker-cli.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Optional location to the stryker.conf.js file as last argument. That file should
2222
strykerConfig = config;
2323
})
2424
.option('-f, --files <allFiles>', `A comma seperated list of globbing expression used for selecting all files needed to run the tests. For a more detailed way of selecting inputfiles, please use a configFile.
25-
Example: node_modules/a-lib/**/*.js,src/**/*.js,a.js,test/**/*.js`, list)
25+
Example: node_modules/a-lib/**/*.js,src/**/*.js,!src/index.js,a.js,test/**/*.js`, list)
2626
.option('-m, --mutate <filesToMutate>', `A comma seperated list of globbing expression used for selecting the files that should be mutated.
2727
Example: src/**/*.js,a.js`, list)
2828
.option('--coverageAnalysis <perTest|all|off>', `The coverage analysis strategy you want to use. Default value: "perTest"`)

0 commit comments

Comments
 (0)