@stryker-mutator/karma-runner
Version:
A plugin to use the karma test runner in Stryker, the JavaScript mutation testing framework
166 lines (150 loc) • 5.25 kB
text/typescript
import { determineHitLimitReached, DryRunResult, DryRunStatus, TestResult, TestStatus } from '@stryker-mutator/api/test-runner';
import { MutantCoverage } from '@stryker-mutator/api/core';
import type karma from 'karma';
import { Task } from '@stryker-mutator/util';
export interface KarmaSpec {
description: string;
id: string;
skipped: boolean;
success: boolean;
time: number;
suite: string[];
log: string[];
}
export interface Browser {
id: string;
state: string;
}
export function strykerReporterFactory(karmaServer: karma.Server, config: karma.Config): StrykerReporter {
StrykerReporter.instance.karmaServer = karmaServer;
StrykerReporter.instance.karmaConfig = config;
return StrykerReporter.instance;
}
strykerReporterFactory.$inject = ['server', 'config'];
/**
* This is a singleton implementation of a KarmaReporter.
* It is loaded by karma and functions as a bridge between the karma world and the stryker world
*
* It uses properties as functions because karma is not able to find actual methods.
*
* i.e. use `public readonly onFoo = () => {}` instead of `onFoo() { }`.
*/
export class StrykerReporter implements karma.Reporter {
public adapters: any[] = [];
public karmaServer: karma.Server | undefined;
public karmaConfig: karma.Config | undefined;
public runResultHandler: ((result: DryRunResult) => void) | undefined;
private testResults: TestResult[] = [];
private errorMessage: string | undefined;
private mutantCoverage: MutantCoverage | undefined;
private hitCount: number | undefined;
private hitLimit: number | undefined;
private initTask: Task | undefined;
private runTask: Task<DryRunResult> | undefined;
private karmaRunResult: karma.TestResults | undefined;
private browserIsRestarting = false;
private static readonly _instance = new StrykerReporter();
public static get instance(): StrykerReporter {
return this._instance;
}
public readonly onBrowsersReady = (): void => {
this.initTask?.resolve();
this.runTask?.resolve(this.collectRunResult());
};
public configureHitLimit(hitLimit: number | undefined): void {
this.hitLimit = hitLimit;
this.hitCount = undefined;
}
public whenBrowsersReady(): Promise<void> {
this.initTask = new Task();
return this.initTask.promise.finally(() => {
this.initTask = undefined;
});
}
public whenRunCompletes(): Promise<DryRunResult> {
this.runTask = new Task();
return this.runTask.promise.finally(() => {
this.runTask = undefined;
});
}
public readonly onSpecComplete = (_browser: unknown, spec: KarmaSpec): void => {
const name = spec.suite.reduce((specName, suite) => specName + suite + ' ', '') + spec.description;
const id = spec.id || name;
if (spec.skipped) {
this.testResults.push({
id,
name,
timeSpentMs: spec.time,
status: TestStatus.Skipped,
});
} else if (spec.success) {
this.testResults.push({
id,
name,
timeSpentMs: spec.time,
status: TestStatus.Success,
});
} else {
this.testResults.push({
id,
name,
timeSpentMs: spec.time,
status: TestStatus.Failed,
failureMessage: spec.log.join(', '),
});
}
};
public readonly onRunStart = (): void => {
this.testResults = [];
this.errorMessage = undefined;
this.mutantCoverage = undefined;
this.karmaRunResult = undefined;
this.browserIsRestarting = false;
};
public readonly onRunComplete = (_browsers: unknown, runResult: karma.TestResults): void => {
this.karmaRunResult = runResult;
if (!this.browserIsRestarting) {
this.runTask!.resolve(this.collectRunResult());
}
};
public readonly onBrowserComplete = (
_browser: unknown,
result: {
mutantCoverage: MutantCoverage | undefined;
hitCount: number | undefined;
},
): void => {
this.mutantCoverage = result.mutantCoverage;
this.hitCount = result.hitCount;
};
public readonly onBrowserError = (browser: Browser, error: any): void => {
if (this.initTask) {
this.initTask.reject(error);
} else {
if (browser.state.toUpperCase().includes('DISCONNECTED')) {
// Restart the browser for next run
this.karmaServer!.get('launcher').restart(browser.id);
this.browserIsRestarting = true;
}
// Karma 2.0 has different error messages
if (error.message) {
this.errorMessage = error.message;
} else {
this.errorMessage = error.toString();
}
}
};
private collectRunResult(): DryRunResult {
const timeoutResult = determineHitLimitReached(this.hitCount, this.hitLimit);
if (timeoutResult) {
return timeoutResult;
}
if (this.karmaRunResult?.disconnected) {
return { status: DryRunStatus.Timeout, reason: `Browser disconnected during test execution. Karma error: ${this.errorMessage}` };
} else if (this.karmaRunResult?.error) {
return { status: DryRunStatus.Error, errorMessage: this.errorMessage ?? 'A runtime error occurred' };
} else {
return { status: DryRunStatus.Complete, tests: this.testResults, mutantCoverage: this.mutantCoverage };
}
}
}