Skip to content

Commit

Permalink
feat(plugin): allow fileDescriptions to be injected (#3582)
Browse files Browse the repository at this point in the history
Allow `fileDescriptions` to be injected in plugins. File descriptions allow plugins to act on the file names discovered in the project (excluding any ignored files).

A `FileDescriptions` object contain file names as keys and a new `FileDescription` as value. A file description currently only houses a `mutate` property (of type `MutationDescription`).

```ts
class MyChecker {
  static inject = [commonTokens.fileDescriptions] as const;
  constructor(fileDescriptions: FileDescriptions) { }
}
```
  • Loading branch information
nicojs committed Jun 23, 2022
1 parent 3e6b7a3 commit fa2b77e
Show file tree
Hide file tree
Showing 97 changed files with 2,446 additions and 1,689 deletions.
2 changes: 1 addition & 1 deletion e2e/test/jasmine-javascript/verify/verify.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ describe('After running stryker with test runner jasmine, test framework jasmine
});
it('should write to a log file', async () => {
const strykerLog = await fsPromises.readFile('./stryker.log', 'utf8');
expect(strykerLog).matches(/INFO InputFileResolver Found 2 of \d+ file\(s\) to be mutated/);
expect(strykerLog).matches(/INFO ProjectReader Found 2 of \d+ file\(s\) to be mutated/);
expect(strykerLog).matches(/Done in \d+ second/);
// TODO, we now have an error because of a memory leak: https://github.com/jasmine/jasmine-npm/issues/134
// expect(strykerLog).not.contains('ERROR');
Expand Down
15 changes: 15 additions & 0 deletions packages/api/src/core/file-description.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { MutationRange } from './mutation-range.js';

/**
* Input files by file name.
*/
export type FileDescriptions = Record<string, FileDescription>;

export type MutateDescription = MutationRange[] | boolean;

/**
* The metadata of a input file
*/
export interface FileDescription {
mutate: MutateDescription;
}
3 changes: 2 additions & 1 deletion packages/api/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ export * from './partial-stryker-options.js';
export * from './instrument.js';
export * from './mutant-coverage.js';
export * from './mutant-test-plan.js';
export * from './file-description.js';
export * from './mutation-range.js';
/**
* Re-export all members from "mutation-testing-report-schema" under the `schema` key
*/
export * as schema from 'mutation-testing-report-schema/api';
export * from './mutation-range.js';
17 changes: 4 additions & 13 deletions packages/api/src/core/mutation-range.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,16 @@
import { Position } from './position.js';

/**
* Represents a range of mutants that the instrumenter should instrument
*/
export interface MutationRange {
/**
* The filename of the file that this range belongs to
*/
fileName: string;

/**
* The start of the range to instrument, by line and column number, inclusive
*/
start: {
line: number;
column: number;
};
start: Position;

/**
* The end of the range to instrument, by line and number, inclusive
*/
end: {
line: number;
column: number;
};
end: Position;
}
3 changes: 2 additions & 1 deletion packages/api/src/plugin/contexts.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { StrykerOptions } from '../core/index.js';
import { FileDescriptions, StrykerOptions } from '../core/index.js';
import { Logger, LoggerFactoryMethod } from '../logging/index.js';

import { commonTokens } from './tokens.js';
Expand All @@ -17,4 +17,5 @@ export interface BaseContext {
*/
export interface PluginContext extends BaseContext {
[commonTokens.options]: StrykerOptions;
[commonTokens.fileDescriptions]: FileDescriptions;
}
1 change: 1 addition & 0 deletions packages/api/src/plugin/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const commonTokens = Object.freeze({
injector,
logger: stringLiteral('logger'),
options: stringLiteral('options'),
fileDescriptions: stringLiteral('fileDescriptions'),
target,
});

Expand Down
10 changes: 8 additions & 2 deletions packages/core/src/checker/checker-child-process-proxy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { URL } from 'url';

import { Mutant, StrykerOptions } from '@stryker-mutator/api/core';
import { FileDescriptions, Mutant, StrykerOptions } from '@stryker-mutator/api/core';
import { Disposable } from 'typed-inject';

import { ChildProcessProxy } from '../child-proxy/child-process-proxy.js';
Expand All @@ -13,11 +13,17 @@ import { CheckerResource } from './checker-resource.js';
export class CheckerChildProcessProxy implements CheckerResource, Disposable, Resource {
private readonly childProcess: ChildProcessProxy<CheckerWorker>;

constructor(options: StrykerOptions, pluginModulePaths: readonly string[], loggingContext: LoggingClientContext) {
constructor(
options: StrykerOptions,
fileDescriptions: FileDescriptions,
pluginModulePaths: readonly string[],
loggingContext: LoggingClientContext
) {
this.childProcess = ChildProcessProxy.create(
new URL('./checker-worker.js', import.meta.url).toString(),
loggingContext,
options,
fileDescriptions,
pluginModulePaths,
process.cwd(),
CheckerWorker,
Expand Down
13 changes: 10 additions & 3 deletions packages/core/src/checker/checker-factory.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { StrykerOptions } from '@stryker-mutator/api/core';
import { FileDescriptions, StrykerOptions } from '@stryker-mutator/api/core';
import { LoggerFactoryMethod } from '@stryker-mutator/api/logging';
import { commonTokens, tokens } from '@stryker-mutator/api/plugin';

Expand All @@ -9,9 +9,16 @@ import { CheckerChildProcessProxy } from './checker-child-process-proxy.js';
import { CheckerFacade } from './checker-facade.js';
import { CheckerRetryDecorator } from './checker-retry-decorator.js';

createCheckerFactory.inject = tokens(commonTokens.options, coreTokens.loggingContext, coreTokens.pluginModulePaths, commonTokens.getLogger);
createCheckerFactory.inject = tokens(
commonTokens.options,
commonTokens.fileDescriptions,
coreTokens.loggingContext,
coreTokens.pluginModulePaths,
commonTokens.getLogger
);
export function createCheckerFactory(
options: StrykerOptions,
fileDescriptions: FileDescriptions,
loggingContext: LoggingClientContext,
pluginModulePaths: readonly string[],
getLogger: LoggerFactoryMethod
Expand All @@ -20,7 +27,7 @@ export function createCheckerFactory(
new CheckerFacade(
() =>
new CheckerRetryDecorator(
() => new CheckerChildProcessProxy(options, pluginModulePaths, loggingContext),
() => new CheckerChildProcessProxy(options, fileDescriptions, pluginModulePaths, loggingContext),
getLogger(CheckerRetryDecorator.name)
)
);
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/child-proxy/child-process-proxy-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,12 @@ export class ChildProcessProxyWorker {
const message = deserialize<WorkerMessage>(String(serializedMessage));
switch (message.kind) {
case WorkerMessageKind.Init:
// eslint-disable-next-line @typescript-eslint/no-floating-promises -- No handle needed, handleInit has try catch
this.handleInit(message);
this.removeAnyAdditionalMessageListeners(this.handleMessage);
break;
case WorkerMessageKind.Call:
// eslint-disable-next-line @typescript-eslint/no-floating-promises -- No handle needed, handleCall has try catch
this.handleCall(message);
this.removeAnyAdditionalMessageListeners(this.handleMessage);
break;
Expand All @@ -64,7 +66,9 @@ export class ChildProcessProxyWorker {
this.handlePromiseRejections();

// Load plugins in the child process
const pluginInjector = provideLogger(this.injectorFactory()).provideValue(commonTokens.options, message.options);
const pluginInjector = provideLogger(this.injectorFactory())
.provideValue(commonTokens.options, message.options)
.provideValue(commonTokens.fileDescriptions, message.fileDescriptions);
const pluginLoader = pluginInjector.injectClass(PluginLoader);
const { pluginsByKind } = await pluginLoader.load(message.pluginModulePaths);
const injector: Injector<ChildProcessContext> = pluginInjector
Expand Down
18 changes: 15 additions & 3 deletions packages/core/src/child-proxy/child-process-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import childProcess from 'child_process';
import os from 'os';
import { fileURLToPath, URL } from 'url';

import { StrykerOptions } from '@stryker-mutator/api/core';
import { FileDescriptions, StrykerOptions } from '@stryker-mutator/api/core';
import { isErrnoException, Task, ExpirableTask, StrykerError } from '@stryker-mutator/util';
import log4js from 'log4js';
import { Disposable, InjectableClass, InjectionToken } from 'typed-inject';
Expand Down Expand Up @@ -48,6 +48,7 @@ export class ChildProcessProxy<T> implements Disposable {
namedExport: string,
loggingContext: LoggingClientContext,
options: StrykerOptions,
fileDescriptions: FileDescriptions,
pluginModulePaths: readonly string[],
workingDirectory: string,
execArgv: string[]
Expand All @@ -63,6 +64,7 @@ export class ChildProcessProxy<T> implements Disposable {
kind: WorkerMessageKind.Init,
loggingContext,
options,
fileDescriptions,
pluginModulePaths,
namedExport: namedExport,
modulePath: modulePath,
Expand All @@ -81,12 +83,22 @@ export class ChildProcessProxy<T> implements Disposable {
modulePath: string,
loggingContext: LoggingClientContext,
options: StrykerOptions,
fileDescriptions: FileDescriptions,
pluginModulePaths: readonly string[],
workingDirectory: string,
injectableClass: InjectableClass<ChildProcessContext, R, Tokens>,
execArgv: string[]
): ChildProcessProxy<R> {
return new ChildProcessProxy(modulePath, injectableClass.name, loggingContext, options, pluginModulePaths, workingDirectory, execArgv);
return new ChildProcessProxy(
modulePath,
injectableClass.name,
loggingContext,
options,
fileDescriptions,
pluginModulePaths,
workingDirectory,
execArgv
);
}

private send(message: WorkerMessage) {
Expand Down Expand Up @@ -162,7 +174,7 @@ export class ChildProcessProxy<T> implements Disposable {
case ParentMessageKind.InitError:
this.fatalError = new StrykerError(message.error);
this.reportError(this.fatalError);
this.dispose();
void this.dispose();
break;
default:
this.logUnidentifiedMessage(message);
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/child-proxy/message-protocol.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { StrykerOptions } from '@stryker-mutator/api/core';
import { FileDescriptions, StrykerOptions } from '@stryker-mutator/api/core';

import { LoggingClientContext } from '../logging/index.js';

Expand Down Expand Up @@ -46,6 +46,7 @@ export interface InitMessage {
kind: WorkerMessageKind.Init;
loggingContext: LoggingClientContext;
options: StrykerOptions;
fileDescriptions: FileDescriptions;
pluginModulePaths: readonly string[];
workingDirectory: string;
namedExport: string;
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/config/options-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { coreTokens } from '../di/index.js';
import { ConfigError } from '../errors.js';
import { objectUtils, optionsPath } from '../utils/index.js';
import { CommandTestRunner } from '../test-runner/command-test-runner.js';
import { IGNORE_PATTERN_CHARACTER, MUTATION_RANGE_REGEX } from '../input/index.js';
import { IGNORE_PATTERN_CHARACTER, MUTATION_RANGE_REGEX } from '../fs/index.js';

import { describeErrors } from './validation-errors.js';

Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/di/core-tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ export const checkerFactory = 'checkerFactory';
export const checkerConcurrencyTokens = 'checkerConcurrencyTokens';
export const disableTypeChecksHelper = 'disableTypeChecksHelper';
export const execa = 'execa';
export const inputFiles = 'inputFiles';
export const dryRunResult = 'dryRunResult';
export const files = 'files';
export const mutants = 'mutants';
export const mutantTestPlanner = 'mutantTestPlanner';
export const process = 'process';
Expand All @@ -27,3 +25,5 @@ export const pluginsByKind = 'pluginsByKind';
export const validationSchema = 'validationSchema';
export const optionsValidator = 'optionsValidator';
export const requireFromCwd = 'requireFromCwd';
export const fs = 'fs';
export const project = 'project';
59 changes: 59 additions & 0 deletions packages/core/src/fs/file-system.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import fs from 'fs';

import { Task } from '@stryker-mutator/util';
import { mergeMap, Subject } from 'rxjs';
import { Disposable } from 'typed-inject';

const MAX_CONCURRENT_FILE_IO = 256;

class FileSystemAction<TOut> {
public readonly task = new Task<TOut>();

/**
* @param work The task, where a resource and input is presented
*/
constructor(private readonly work: () => Promise<TOut>) {}

public async execute() {
try {
const output = await this.work();
this.task.resolve(output);
} catch (err) {
this.task.reject(err);
}
}
}

/**
* A wrapper around nodejs's 'fs' core module, for dependency injection purposes.
*
* Also has but with build-in buffering with a concurrency limit (like graceful-fs).
*/
export class FileSystem implements Disposable {
private readonly todoSubject = new Subject<FileSystemAction<any>>();
private readonly subscription = this.todoSubject
.pipe(
mergeMap(async (action) => {
await action.execute();
}, MAX_CONCURRENT_FILE_IO)
)
.subscribe();

public dispose(): void {
this.subscription.unsubscribe();
}

public readonly readFile = this.forward('readFile');
public readonly copyFile = this.forward('copyFile');
public readonly writeFile = this.forward('writeFile');
public readonly mkdir = this.forward('mkdir');
public readonly readdir = this.forward('readdir');

private forward<TMethod extends keyof typeof fs.promises>(method: TMethod): typeof fs.promises[TMethod] {
return (...args: any[]) => {
const action = new FileSystemAction(() => (fs.promises[method] as any)(...args));
this.todoSubject.next(action);
return action.task.promise as any;
};
}
}
4 changes: 4 additions & 0 deletions packages/core/src/fs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './file-system.js';
export * from './project.js';
export * from './project-file.js';
export * from './project-reader.js';

0 comments on commit fa2b77e

Please sign in to comment.