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

[DRAFT] Mutation server #4877

Closed
wants to merge 43 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
a7f747b
feat: Add initial mutation server exec
jaspervdveen Apr 3, 2024
e7c0cb4
wip: rpc via stdout/stdin pipes
jaspervdveen Apr 4, 2024
cb3d107
wip: Use TCP socket instead of anonymous pipes
jaspervdveen Apr 4, 2024
db23058
wip: change tcp socket to web socket server
jaspervdveen Apr 5, 2024
3540060
chore: send server started output
jaspervdveen Apr 5, 2024
29bed47
feat: instrument returns mutation test report
jaspervdveen Apr 5, 2024
c10507c
refactor(server): reuse setup for subsequent instrument runs
jaspervdveen Apr 10, 2024
edfe4ca
feat: set websocket port via program args
jaspervdveen Apr 10, 2024
85a2c42
fix: JSON-RPC error handling in StrykerServer
jaspervdveen Apr 11, 2024
6ed071e
refactor: rename default test coverage to empty test coverage
jaspervdveen Apr 11, 2024
d2ca741
wip: Add mutate method to Stryker Server
jaspervdveen Apr 11, 2024
36db521
fix: incorrect mutantresult location in instrument report
jaspervdveen Apr 11, 2024
639b9b4
chore: no longer keep preparation context in memory
jaspervdveen Apr 12, 2024
b88f460
Merge pull request #1 from jaspervdveen/feat/mutation-server
Apr 15, 2024
3a8cb30
refactor: Use MutantResult array type for instrument run result
jaspervdveen Apr 15, 2024
979c4e1
chore: check for mutation server method implementation
jaspervdveen Apr 15, 2024
67f1a82
chore: Setup ability to send JSON-RCP notifications to client
jaspervdveen Apr 16, 2024
d67bb55
feat: during test run, send progress notifications
jaspervdveen Apr 16, 2024
b64dd6f
Merge pull request #2 from jaspervdveen/feat/mutation-testing
Apr 22, 2024
f38fe93
refactor: use empty reporter to forward partial results
jaspervdveen Apr 23, 2024
40f7ca7
Merge pull request #3 from jaspervdveen/develop
Apr 24, 2024
c9d0895
refactor: Bind server methods
jaspervdveen Apr 25, 2024
3ea89ae
refactor(server): Extract server methods from server class
jaspervdveen Apr 25, 2024
fb72288
refactor(server): Extract json-rpc and websocket logic to separate cl…
jaspervdveen Apr 25, 2024
df227ba
refactor(server): Introduce transporter interface
jaspervdveen Apr 25, 2024
a7b6ac2
chore(websocket): Pick a random available port if no port is explicit…
jaspervdveen Apr 26, 2024
2ff02b2
fix(websocket): Do not overwrite existing callback on registering
jaspervdveen Apr 26, 2024
32b5601
refactor(transporter): Use EventEmitter in transporter
jaspervdveen Apr 29, 2024
3120511
Merge pull request #4 from jaspervdveen/feature/realtime-updates
Apr 29, 2024
8eb24c3
Merge branch 'stryker-mutator:master' into master
May 1, 2024
cf0006d
test(server): Add tests for StrykerServer class
jaspervdveen May 1, 2024
ebaabe7
test(MutationServerProtocolHandler): Add test for protocol handler
jaspervdveen May 1, 2024
e9bd67b
test(MutationServerProtocolHandler): Add tests for protocol handler
jaspervdveen May 2, 2024
d579d40
test(webSocketTransporter): Add tests
jaspervdveen May 2, 2024
7eb9877
test(instrumentMethod): Add tests for InstrumentMethod class
jaspervdveen May 13, 2024
69cd5fc
test(mutationTestMethod): Add tests for mutation test method
jaspervdveen May 13, 2024
c7145c1
test(mutantInstrumenterExecutor): Add tests for mutantInstrumenterExe…
jaspervdveen May 13, 2024
e607981
fix(mutationTestMethod): Empty reporter not reporting mutant tested e…
jaspervdveen May 14, 2024
95ff198
test(instrumentMethod): Assert run instrumentation result
jaspervdveen May 15, 2024
b902b9d
Merge pull request #5 from jaspervdveen/test/server
May 15, 2024
48592d5
feat(server): Abort mutation test run
May 17, 2024
74ab60c
feat(server): Configure the Stryker config file uri via an initialize…
Jun 10, 2024
393f0ec
Fix tests, refactor mutate request, change init request (#8)
jaspervdveen Oct 18, 2024
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
42 changes: 40 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/api/src/plugin/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const commonTokens = Object.freeze({
options: stringLiteral('options'),
fileDescriptions: stringLiteral('fileDescriptions'),
target,
abortSignal: stringLiteral('abortSignal'),
});

/**
Expand Down
5 changes: 5 additions & 0 deletions packages/core/bin/stryker-server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env node
import { StrykerServer } from '../dist/src/index.js';

process.title = 'stryker-server';
new StrykerServer(process.argv);
11 changes: 8 additions & 3 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
"test:all": "npm run test:unit && npm run test:integration",
"test:unit": "mocha 'dist/test/unit/**/*.js'",
"test:integration": "mocha --timeout 60000 'dist/test/integration/**/*.js'",
"stryker": "node bin/stryker.js run"
"stryker": "node bin/stryker.js run",
"stryker-server": "node bin/stryker-server.js"
},
"repository": {
"type": "git",
Expand Down Expand Up @@ -65,7 +66,8 @@
},
"homepage": "https://stryker-mutator.io/",
"bin": {
"stryker": "./bin/stryker.js"
"stryker": "./bin/stryker.js",
"stryker-server": "./bin/stryker-server.js"
},
"dependencies": {
"@stryker-mutator/api": "8.2.6",
Expand All @@ -81,6 +83,7 @@
"get-port": "~7.0.0",
"glob": "~10.3.10",
"inquirer": "~9.2.13",
"json-rpc-2.0": "^1.7.0",
"lodash.groupby": "~4.6.0",
"log4js": "~6.9.1",
"minimatch": "~9.0.3",
Expand All @@ -95,7 +98,8 @@
"tree-kill": "~1.2.2",
"tslib": "2.6.2",
"typed-inject": "~4.0.0",
"typed-rest-client": "~1.8.11"
"typed-rest-client": "~1.8.11",
"ws": "^8.16.0"
},
"devDependencies": {
"@stryker-mutator/test-helpers": "8.2.6",
Expand All @@ -105,6 +109,7 @@
"@types/node": "20.12.7",
"@types/progress": "2.0.7",
"@types/semver": "7.5.8",
"@types/ws": "^8.5.10",
"flatted": "3.3.1"
}
}
3 changes: 2 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { StrykerCli } from './stryker-cli.js';
import { StrykerServer } from './stryker-server.js';
import { Stryker } from './stryker.js';

export { Stryker, StrykerCli };
export { Stryker, StrykerCli, StrykerServer };

// One default export for backward compatibility
// eslint-disable-next-line import/no-default-export
Expand Down
1 change: 0 additions & 1 deletion packages/core/src/process/1-prepare-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { Timer } from '../utils/timer.js';
import { MetaSchemaBuilder, OptionsValidator } from '../config/index.js';
import { BroadcastReporter } from '../reporters/broadcast-reporter.js';
import { UnexpectedExitHandler } from '../unexpected-exit-handler.js';

import { FileSystem, ProjectReader } from '../fs/index.js';

import { MutantInstrumenterContext } from './index.js';
Expand Down
14 changes: 10 additions & 4 deletions packages/core/src/process/4-mutation-test-executor.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { from, partition, merge, Observable, lastValueFrom, EMPTY, concat, bufferTime, mergeMap } from 'rxjs';
import { toArray, map, shareReplay, tap } from 'rxjs/operators';
import { from, partition, merge, Observable, lastValueFrom, EMPTY, concat, bufferTime, mergeMap, fromEvent } from 'rxjs';
import { toArray, map, shareReplay, tap, take, takeUntil } from 'rxjs/operators';
import { tokens, commonTokens } from '@stryker-mutator/api/plugin';
import { MutantResult, Mutant, StrykerOptions, PlanKind, MutantTestPlan, MutantRunPlan } from '@stryker-mutator/api/core';
import { TestRunner, CompleteDryRunResult } from '@stryker-mutator/api/test-runner';
Expand Down Expand Up @@ -50,6 +50,7 @@ export class MutationTestExecutor {
coreTokens.timer,
coreTokens.concurrencyTokenProvider,
coreTokens.dryRunResult,
commonTokens.abortSignal,
);

constructor(
Expand All @@ -64,6 +65,7 @@ export class MutationTestExecutor {
private readonly timer: I<Timer>,
private readonly concurrencyTokenProvider: I<ConcurrencyTokenProvider>,
private readonly dryRunResult: CompleteDryRunResult,
private readonly abortSignal: AbortSignal | undefined,
) {}

public async execute(): Promise<MutantResult[]> {
Expand All @@ -72,17 +74,21 @@ export class MutationTestExecutor {
return [];
}

if (this.dryRunResult.tests.length === 0 && this.options.allowEmpty) {
if ((this.dryRunResult.tests.length === 0 && this.options.allowEmpty) || this.abortSignal?.aborted) {
this.logDone();
return [];
}

const stop = this.abortSignal ? fromEvent(this.abortSignal, 'abort').pipe(take(1)) : EMPTY;

const mutantTestPlans = await this.planner.makePlan(this.mutants);
const { earlyResult$, runMutant$ } = this.executeEarlyResult(from(mutantTestPlans));
const { passedMutant$, checkResult$ } = this.executeCheck(runMutant$);
const { coveredMutant$, noCoverageResult$ } = this.executeNoCoverage(passedMutant$);
const testRunnerResult$ = this.executeRunInTestRunner(coveredMutant$);
const results = await lastValueFrom(merge(testRunnerResult$, checkResult$, noCoverageResult$, earlyResult$).pipe(toArray()));

const results = await lastValueFrom(merge(testRunnerResult$, checkResult$, noCoverageResult$, earlyResult$).pipe(takeUntil(stop), toArray()));

await this.mutationTestReportHelper.reportAll(results);
await this.reporter.wrapUp();
this.logDone();
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/reporters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const strykerPlugins = [
declareClassPlugin(PluginKind.Reporter, 'event-recorder', EventRecorderReporter),
declareClassPlugin(PluginKind.Reporter, 'html', HtmlReporter),
declareClassPlugin(PluginKind.Reporter, 'json', JsonReporter),
declareClassPlugin(PluginKind.Reporter, 'empty', class {}),
declareFactoryPlugin(PluginKind.Reporter, 'dashboard', dashboardReporterFactory),
];

Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/server/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './mutation-server-protocol-handler.js';
export * from './mutation-server-protocol.js';
export * from './server-tokens.js';
export * as serverTokens from './server-tokens.js';
2 changes: 2 additions & 0 deletions packages/core/src/server/methods/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './instrument-method.js';
export * from './mutation-test-method.js';
27 changes: 27 additions & 0 deletions packages/core/src/server/methods/instrument-method.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { createInjector } from 'typed-inject';
import { MutantResult, PartialStrykerOptions } from '@stryker-mutator/api/core';

import { provideLogger } from './../../di/provide-logger.js';
import { PrepareExecutor } from './../../process/1-prepare-executor.js';
import { MutantInstrumenterExecutor as ServerMutantInstrumenterExecutor } from './process/mutant-instrument-executor.js';

export class InstrumentMethod {
/**
* Get the mutant results for the given glob patterns, without running the mutation tests.
* @param globPatterns The glob patterns to instrument.
* @returns The mutant results.
*/
public static async runInstrumentation(options: PartialStrykerOptions, injectorFactory = createInjector): Promise<MutantResult[]> {
const rootInjector = injectorFactory();

const loggerProvider = provideLogger(rootInjector);

const prepareExecutor = loggerProvider.injectClass(PrepareExecutor);

const mutantInstrumenterInjector = await prepareExecutor.execute(options);

const mutantInstrumenter = mutantInstrumenterInjector.injectClass(ServerMutantInstrumenterExecutor);

return await mutantInstrumenter.execute();
}
}
98 changes: 98 additions & 0 deletions packages/core/src/server/methods/mutation-test-method.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { createInjector } from 'typed-inject';
import { MutantResult, PartialStrykerOptions } from '@stryker-mutator/api/core';
import { commonTokens } from '@stryker-mutator/api/plugin';

import { MutationTestExecutor } from '../../process/4-mutation-test-executor.js';
import { provideLogger } from '../../di/provide-logger.js';
import { PrepareExecutor } from '../../process/1-prepare-executor.js';
import { MutantInstrumenterExecutor } from '../../process/2-mutant-instrumenter-executor.js';
import { Stryker } from '../../stryker.js';
import { BroadcastReporter } from '../../reporters/index.js';
import { DryRunExecutor } from '../../process/3-dry-run-executor.js';
import { coreTokens } from '../../di/index.js';
import { ConfigError, retrieveCause } from '../../errors.js';
import { LogConfigurator } from '../../logging/log-configurator.js';

export class MutationTestMethod {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a copy of the Stryker class. Why have 2 separate processes for the same thing? I think we should reuse the Stryker class.

constructor(private readonly injectorFactory = createInjector) {}

/**
* Run a mutation test and get partial results via a callback.
* @param globPatterns The glob patterns to mutation test.
* @param onMutantTested A callback that is called when a mutant is tested.
*/
public async runMutationTestRealtime(
options: PartialStrykerOptions,
abortSignal: AbortSignal,
onMutantTested: (result: Readonly<MutantResult>) => void,
): Promise<void> {
options.reporters = ['empty']; // used to stream results

const rootInjector = this.injectorFactory();
const loggerProvider = provideLogger(rootInjector);

try {
// 1. Prepare. Load Stryker configuration, load the input files and starts the logging server
const prepareExecutor = loggerProvider.injectClass(PrepareExecutor);

const mutantInstrumenterInjector = await prepareExecutor.execute(options);

const broadcastReporter = mutantInstrumenterInjector.resolve(coreTokens.reporter) as BroadcastReporter;
const emptyReporter = broadcastReporter.reporters.empty;
if (!emptyReporter) {
throw new Error('Reporter unavailable');
}

emptyReporter.onMutantTested = onMutantTested;
try {
// 2. Mutate and instrument the files and write to the sandbox.
const mutantInstrumenter = mutantInstrumenterInjector.injectClass(MutantInstrumenterExecutor);
const dryRunExecutorInjector = await mutantInstrumenter.execute();

// 3. Perform a 'dry run' (initial test run). Runs the tests without active mutants and collects coverage.
const dryRunExecutor = dryRunExecutorInjector.injectClass(DryRunExecutor);
const mutationRunExecutorInjector = (await dryRunExecutor.execute()).provideValue(commonTokens.abortSignal, abortSignal);

// 4. Actual mutation testing. Will check every mutant and if valid run it in an available test runner.
const mutationRunExecutor = mutationRunExecutorInjector.injectClass(MutationTestExecutor);

await mutationRunExecutor.execute();
} catch (error) {
if (mutantInstrumenterInjector.resolve(commonTokens.options).cleanTempDir !== 'always') {
const log = loggerProvider.resolve(commonTokens.getLogger)(Stryker.name);
log.debug('Not removing the temp dir because an error occurred');
mutantInstrumenterInjector.resolve(coreTokens.temporaryDirectory).removeDuringDisposal = false;
}
throw error;
}
} catch (error) {
const log = loggerProvider.resolve(commonTokens.getLogger)(Stryker.name);
const cause = retrieveCause(error);
if (cause instanceof ConfigError) {
log.error(cause.message);
} else {
log.error('Unexpected error occurred while running Stryker', error);
log.info('This might be a known problem with a solution documented in our troubleshooting guide.');
log.info('You can find it at https://stryker-mutator.io/docs/stryker-js/troubleshooting/');
if (!log.isTraceEnabled()) {
log.info(
'Still having trouble figuring out what went wrong? Try `npx stryker run --fileLogLevel trace --logLevel debug` to get some more info.',
);
}
}
throw cause;
} finally {
await rootInjector.dispose();
await LogConfigurator.shutdown();
}
}

/**
* Run a mutation test.
* @param globPatterns The glob patterns to mutation test.
* @returns The mutant results.
*/
public static async runMutationTest(abortSignal: AbortSignal, options: PartialStrykerOptions): Promise<MutantResult[]> {
return await new Stryker(options).runMutationTest(abortSignal);
}
}
Loading