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

feat(plugin): allow fileDescriptions to be injected #3582

Merged
merged 7 commits into from
Jun 23, 2022
Merged
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
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';
Loading