Skip to content

Commit

Permalink
feat(exclude-files): Exclude files with a ! (#188)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
nicojs authored and simondel committed Dec 15, 2016
1 parent 28e1e5d commit 05a356d
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 88 deletions.
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,25 +59,28 @@ The mutators that are supported by Stryker can be found on [our website](http://
All options can be configured either via the command line or via a config file.

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
[Grunt](https://github.com/gruntjs/grunt) and [Karma](https://github.com/karma-runner/karma).
[Grunt](https://github.com/gruntjs/grunt) and [Karma](https://github.com/karma-runner/karma).
It is possible to *ignore* files by adding an exclamation mark `!` to the start of the expression.

#### Files
**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`
**Config file:** `files: ['test/helpers/**/*.js', 'test/unit/**/*.js', { pattern: 'src/**/*.js', included: false, mutated: true }]`
**Config file:** `files: ['{ pattern: 'src/**/*.js', mutated: true }, '!src/**/index.js', 'test/**/*.js']`
**Default value:** *none*
**Description:**
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),
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.

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

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

*Note*: To include a file/folder which start with an exclamation mark (`!`), use the `InputFileDescriptor` syntax

#### Files to mutate
**Command line:** `-m src/**/*.js,a.js` or `--mutate src/**/*.js,a.js`
**Config file:** `mutate: ['src/**/*.js', 'a.js']`
Expand Down
139 changes: 93 additions & 46 deletions src/InputFileResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,41 @@ import * as _ from 'lodash';
import * as log4js from 'log4js';

const log = log4js.getLogger('InputFileResolver');

const DEFAULT_INPUT_FILE_PROPERTIES = { mutated: false, included: true };

export default class InputFileResolver {

private inputFileDescriptors: InputFileDescriptor[];
private mutateFileExpressions: string[];
private inputFileResolver: PatternResolver;
private mutateResolver: PatternResolver;

constructor(mutate: string[], allFileExpressions: Array<InputFileDescriptor | string>) {
this.mutateFileExpressions = mutate || [];
this.inputFileDescriptors = allFileExpressions.map(maybePattern => {
if (InputFileResolver.isInputFileDescriptor(maybePattern)) {
return maybePattern;
} else {
return <InputFileDescriptor>_.assign({ pattern: maybePattern }, DEFAULT_INPUT_FILE_PROPERTIES);
}
});
this.validateFileDescriptor(allFileExpressions);
this.mutateResolver = PatternResolver.parse(mutate || []);
this.inputFileResolver = PatternResolver.parse(allFileExpressions);
}

public resolve(): Promise<InputFile[]> {
let mutateFilePromise = this.resolveMutateFileGlobs();
return this.resolveInputFileGlobs().then((allInputFiles) => mutateFilePromise.then(additionalMutateFiles => {
InputFileResolver.markAdditionalFilesToMutate(allInputFiles, additionalMutateFiles);
InputFileResolver.warnAboutNoFilesToMutate(allInputFiles);
return allInputFiles;
}));
return Promise.all([this.inputFileResolver.resolve(), this.mutateResolver.resolve()]).then(results => {
const inputFiles = results[0];
const mutateFiles = results[1];
this.markAdditionalFilesToMutate(inputFiles, mutateFiles.map(m => m.path));
this.logFilesToMutate(inputFiles);
return inputFiles;
});
}

private validateFileDescriptor(maybeInputFileDescriptors: Array<InputFileDescriptor | string>) {
maybeInputFileDescriptors.forEach(maybeInputFileDescriptor => {
if (_.isObject(maybeInputFileDescriptor)) {
if (Object.keys(maybeInputFileDescriptor).indexOf('pattern') === -1) {
throw Error(`File descriptor ${JSON.stringify(maybeInputFileDescriptor)} is missing mandatory property 'pattern'.`);
}
}
});
}

private static markAdditionalFilesToMutate(allInputFiles: InputFile[], additionalMutateFiles: string[]) {
private markAdditionalFilesToMutate(allInputFiles: InputFile[], additionalMutateFiles: string[]) {
let errors: string[] = [];
additionalMutateFiles.forEach(mutateFile => {
if (!allInputFiles.filter(inputFile => inputFile.path === mutateFile).length) {
Expand All @@ -43,57 +50,97 @@ export default class InputFileResolver {
}
allInputFiles.forEach(file => file.mutated = additionalMutateFiles.some(mutateFile => mutateFile === file.path) || file.mutated);
}
private static warnAboutNoFilesToMutate(allInputFiles: InputFile[]) {

private logFilesToMutate(allInputFiles: InputFile[]) {
let mutateFiles = allInputFiles.filter(file => file.mutated);
if (mutateFiles.length) {
log.info(`Found ${mutateFiles.length} file(s) to be mutated.`);
log.info(`Found ${mutateFiles.length} of ${allInputFiles.length} file(s) to be mutated.`);
} else {
log.warn(`No files marked to be mutated, stryker will perform a dry-run without actually mutating anything.`);
}
if (log.isDebugEnabled) {
log.debug('All input files in order:%s', allInputFiles.map(file => '\n\t' + JSON.stringify(file)));
}
}
}

private static reportEmptyGlobbingExpression(expression: string) {
log.warn(`Globbing expression "${expression}" did not result in any files.`);
}
class PatternResolver {

private static isInputFileDescriptor(maybeInputFileDescriptor: InputFileDescriptor | string): maybeInputFileDescriptor is InputFileDescriptor {
if (_.isObject(maybeInputFileDescriptor)) {
if (Object.keys(maybeInputFileDescriptor).indexOf('pattern') > -1) {
return true;
} else {
throw Error(`File descriptor ${JSON.stringify(maybeInputFileDescriptor)} is missing mandatory property 'pattern'.`);
private ignore = false;
private descriptor: InputFileDescriptor;

constructor(descriptor: InputFileDescriptor | string, private previous?: PatternResolver) {
if (typeof descriptor === 'string') {
this.descriptor = <InputFileDescriptor>_.assign({ pattern: descriptor }, DEFAULT_INPUT_FILE_PROPERTIES);
this.ignore = descriptor.indexOf('!') === 0;
if (this.ignore) {
this.descriptor.pattern = descriptor.substring(1);
}
} else {
return false;
this.descriptor = descriptor;
}
}

private resolveMutateFileGlobs(): Promise<string[]> {
return Promise.all(this.mutateFileExpressions.map(InputFileResolver.resolveFileGlob))
.then(files => _.flatten(files));
resolve(): Promise<InputFile[]> {
// When the first expression starts with an '!', we skip that one
if (this.ignore && !this.previous) {
return Promise.resolve([]);
} else {
// Start the globbing task for the current descriptor
const globbingTask = this.resolveFileGlob(this.descriptor.pattern)
.then(filePaths => filePaths.map(filePath => this.createInputFile(filePath)));
if (this.previous) {
// If there is a previous globbing expression, resolve that one as well
return Promise.all([this.previous.resolve(), globbingTask]).then(results => {
const previousFiles = results[0];
const currentFiles = results[1];
// If this expression started with a '!', exclude current files
if (this.ignore) {
return previousFiles.filter(previousFile => currentFiles.some(currentFile => previousFile.path !== currentFile.path));
} else {
// Only add files which were not already added
return previousFiles.concat(currentFiles.filter(currentFile => !previousFiles.some(file => file.path === currentFile.path)));
}
});
} else {
return globbingTask;
}
}
}

private resolveInputFileGlobs(): Promise<InputFile[]> {
return Promise.all(
this.inputFileDescriptors.map(descriptor => InputFileResolver.resolveFileGlob(descriptor.pattern)
.then(sourceFiles => sourceFiles.map(sourceFile => InputFileResolver.createInputFile(sourceFile, descriptor))))
).then(promises => _.flatten(promises));
static empty(): PatternResolver {
const emptyResolver = new PatternResolver('');
emptyResolver.ignore = true;
return emptyResolver;
}

private static createInputFile(path: string, descriptor: InputFileDescriptor): InputFile {
let inputFile: InputFile = <InputFile>_.assign({ path }, DEFAULT_INPUT_FILE_PROPERTIES, descriptor);
delete (<any>inputFile)['pattern'];
return inputFile;
static parse(inputFileExpressions: Array<string | InputFileDescriptor>): PatternResolver {
const expressions = inputFileExpressions.map(i => i); // work on a copy as we're changing the array state
let current = PatternResolver.empty();
while (expressions.length) {
current = new PatternResolver(expressions.shift(), current);
}
return current;
}


private static resolveFileGlob(expression: string): Promise<string[]> {
return glob(expression).then(files => {
private resolveFileGlob(pattern: string): Promise<string[]> {
return glob(pattern).then(files => {
if (files.length === 0) {
this.reportEmptyGlobbingExpression(expression);
this.reportEmptyGlobbingExpression(pattern);
}
normalize(files);
return files;
});
}
}

private reportEmptyGlobbingExpression(expression: string) {
log.warn(`Globbing expression "${expression}" did not result in any files.`);
}

private createInputFile(path: string): InputFile {
let inputFile: InputFile = <InputFile>_.assign({ path }, DEFAULT_INPUT_FILE_PROPERTIES, this.descriptor);
delete (<any>inputFile)['pattern'];
return inputFile;
}
}

2 changes: 1 addition & 1 deletion src/stryker-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Optional location to the stryker.conf.js file as last argument. That file should
strykerConfig = config;
})
.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.
Example: node_modules/a-lib/**/*.js,src/**/*.js,a.js,test/**/*.js`, list)
Example: node_modules/a-lib/**/*.js,src/**/*.js,!src/index.js,a.js,test/**/*.js`, list)
.option('-m, --mutate <filesToMutate>', `A comma seperated list of globbing expression used for selecting the files that should be mutated.
Example: src/**/*.js,a.js`, list)
.option('--coverageAnalysis <perTest|all|off>', `The coverage analysis strategy you want to use. Default value: "perTest"`)
Expand Down
Loading

0 comments on commit 05a356d

Please sign in to comment.