UNPKG

@stryker-mutator/core

Version:

The extendable JavaScript mutation testing framework

133 lines 5.68 kB
import { notEmpty } from '@stryker-mutator/util'; import { BehaviorSubject, filter, ignoreElements, lastValueFrom, mergeMap, ReplaySubject, Subject, takeUntil, tap, zip } from 'rxjs'; import { tokens } from 'typed-inject'; import { coreTokens } from '../di/index.js'; const MAX_CONCURRENT_INIT = 2; createTestRunnerPool.inject = tokens(coreTokens.testRunnerFactory, coreTokens.testRunnerConcurrencyTokens); export function createTestRunnerPool(factory, concurrencyToken$) { return new Pool(factory, concurrencyToken$); } createCheckerPool.inject = tokens(coreTokens.checkerFactory, coreTokens.checkerConcurrencyTokens); export function createCheckerPool(factory, concurrencyToken$) { return new Pool(factory, concurrencyToken$); } /** * Represents a work item: an input with a task and with a `result$` observable where the result (exactly one) will be streamed to. */ class WorkItem { /** * @param input The input to the ask * @param task The task, where a resource and input is presented */ constructor(input, task) { this.input = input; this.task = task; this.resultSubject = new Subject(); this.result$ = this.resultSubject.asObservable(); } async execute(resource) { try { const output = await this.task(resource, this.input); this.resultSubject.next(output); this.resultSubject.complete(); } catch (err) { this.resultSubject.error(err); } } reject(error) { this.resultSubject.error(error); } complete() { this.resultSubject.complete(); } } /** * Represents a pool of resources. Use `schedule` to schedule work to be executed on the resources. * The pool will automatically recycle the resources, but will make sure only one task is executed * on one resource at any one time. Creates as many resources as the concurrency tokens allow. * Also takes care of the initialing of the resources (with `init()`) */ export class Pool { constructor(factory, concurrencyToken$) { // The init subject. Using an RxJS subject instead of a promise, so errors are silently ignored when nobody is listening this.initSubject = new ReplaySubject(); // The disposedSubject emits true when it is disposed, and false when not disposed yet this.disposedSubject = new BehaviorSubject(false); // The dispose$ only emits one `true` value when disposed (never emits `false`). Useful for `takeUntil` this.dispose$ = this.disposedSubject.pipe(filter((isDisposed) => isDisposed)); this.createdResources = []; // The queued work items. This is a replay subject, so scheduled work items can easily be rejected after it was picked up this.todoSubject = new ReplaySubject(); // Stream resources that are ready to pick up work const resourcesSubject = new Subject(); // Stream ongoing work. zip(resourcesSubject, this.todoSubject) .pipe(mergeMap(async ([resource, workItem]) => { await workItem.execute(resource); resourcesSubject.next(resource); // recycle resource so it can pick up more work }), ignoreElements(), takeUntil(this.dispose$)) .subscribe({ error: (error) => { this.todoSubject.subscribe((workItem) => workItem.reject(error)); }, }); // Create resources concurrencyToken$ .pipe(takeUntil(this.dispose$), mergeMap(async () => { var _a; if (this.disposedSubject.value) { // Don't create new resources when disposed return; } const resource = factory(); this.createdResources.push(resource); await ((_a = resource.init) === null || _a === void 0 ? void 0 : _a.call(resource)); return resource; }, MAX_CONCURRENT_INIT), filter(notEmpty), tap({ complete: () => { // Signal init complete this.initSubject.next(); this.initSubject.complete(); }, error: (err) => { this.initSubject.error(err); }, })) .subscribe({ next: (resource) => resourcesSubject.next(resource), error: (err) => resourcesSubject.error(err), }); } /** * Returns a promise that resolves if all concurrency tokens have resulted in initialized resources. * This is optional, resources will get initialized either way. */ async init() { await lastValueFrom(this.initSubject); } /** * Schedules a task to be executed on resources in the pool. Each input is paired with a resource, which allows async work to be done. * @param input$ The inputs to pair up with a resource. * @param task The task to execute on each resource */ schedule(input$, task) { return input$.pipe(mergeMap((input) => { const workItem = new WorkItem(input, task); this.todoSubject.next(workItem); return workItem.result$; })); } /** * Dispose the pool */ async dispose() { if (!this.disposedSubject.value) { this.disposedSubject.next(true); this.todoSubject.subscribe((workItem) => workItem.complete()); this.todoSubject.complete(); await Promise.all(this.createdResources.map((resource) => { var _a; return (_a = resource.dispose) === null || _a === void 0 ? void 0 : _a.call(resource); })); } } } //# sourceMappingURL=pool.js.map