UNPKG

@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
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 }; } } }