diff --git a/.yarn/versions/7b62c29f.yml b/.yarn/versions/7b62c29f.yml new file mode 100644 index 00000000..64d70248 --- /dev/null +++ b/.yarn/versions/7b62c29f.yml @@ -0,0 +1,5 @@ +releases: + "@fast-check/worker": patch + +declined: + - "@fast-check/jest" diff --git a/packages/worker/src/internals/MainThreadRunner.ts b/packages/worker/src/internals/MainThreadRunner.ts index f524194b..90257168 100644 --- a/packages/worker/src/internals/MainThreadRunner.ts +++ b/packages/worker/src/internals/MainThreadRunner.ts @@ -5,6 +5,7 @@ import type { IWorkerPool, Payload, PooledWorker } from './worker-pool/IWorkerPo import { OneTimePool } from './worker-pool/OneTimePool.js'; import { GlobalPool } from './worker-pool/GlobalPool.js'; import { buildWorkerProperty } from './worker-property/WorkerPropertyBuilder.js'; +import { PreconditionFailure } from 'fast-check'; /** * Create a property able to run in the main thread and firing workers whenever required @@ -39,7 +40,9 @@ export function runMainThread( reject(new Error('Badly initialized worker, unable to run the property')); return; } - worker.register(predicateId, property.getPayload(inputs), resolve, reject); + worker.register(predicateId, property.getPayload(inputs), resolve, reject, () => + reject(new PreconditionFailure()), + ); }); }, randomSource === 'worker', diff --git a/packages/worker/src/internals/worker-pool/BasicPool.ts b/packages/worker/src/internals/worker-pool/BasicPool.ts index fa8cdae0..0bbeeb5e 100644 --- a/packages/worker/src/internals/worker-pool/BasicPool.ts +++ b/packages/worker/src/internals/worker-pool/BasicPool.ts @@ -1,11 +1,13 @@ import { Worker } from 'node:worker_threads'; -import type { - OnErrorCallback, - OnSuccessCallback, - IWorkerPool, - PooledWorker, - WorkerToPoolMessage, - PoolToWorkerMessage, +import { + type OnErrorCallback, + type OnSuccessCallback, + type OnSkippedCallback, + type IWorkerPool, + type PooledWorker, + type WorkerToPoolMessage, + type PoolToWorkerMessage, + WorkerToPoolMessageStatus, } from './IWorkerPool.js'; /** @@ -36,6 +38,7 @@ export class BasicPool implements IWorkerPool; onFailure: OnErrorCallback; + onSkipped: OnSkippedCallback; } | null = null; const worker = new Worker(this.workerFileUrl, { workerData: { fastcheckWorker: true } }); @@ -59,10 +62,12 @@ export class BasicPool implements IWorkerPool implements IWorkerPool { + register: (predicateId, payload, onSuccess, onFailure, onSkipped) => { if (!isAvailable()) { throw new Error('This instance of PooledWorker is currently in use'); } const currentRunId = ++runIdInWorker; - registration = { currentRunId, onSuccess, onFailure }; + registration = { currentRunId, onSuccess, onFailure, onSkipped }; const message: PoolToWorkerMessage = { targetPredicateId: predicateId, payload, diff --git a/packages/worker/src/internals/worker-pool/IWorkerPool.ts b/packages/worker/src/internals/worker-pool/IWorkerPool.ts index bb306a2a..96cb5133 100644 --- a/packages/worker/src/internals/worker-pool/IWorkerPool.ts +++ b/packages/worker/src/internals/worker-pool/IWorkerPool.ts @@ -2,6 +2,7 @@ import type { ValueState } from '../ValueFromState.js'; export type OnSuccessCallback = (value: TSuccess) => void; export type OnErrorCallback = (error: unknown) => void; +export type OnSkippedCallback = () => void; /** * Worker API @@ -14,6 +15,7 @@ export type PooledWorker = { payload: TPayload, onSuccess: OnSuccessCallback, onFailure: OnErrorCallback, + onSkipped: OnSkippedCallback, ) => void; terminateIfStillRunning: () => Promise; }; @@ -32,12 +34,22 @@ export type PoolToWorkerMessage = { */ export type Payload = { source: 'main'; value: TValue } | ({ source: 'worker' } & ValueState); +/** + * Status of the execution by the worker + */ +export enum WorkerToPoolMessageStatus { + Success = 'o', + Skipped = '-', + Failure = 'x', +} + /** * Message exchanged from the worker to the pool */ export type WorkerToPoolMessage = { runId: number } & ( - | { success: true; output: TSuccess } - | { success: false; error: unknown } + | { status: WorkerToPoolMessageStatus.Success; output: TSuccess } + | { status: WorkerToPoolMessageStatus.Skipped } + | { status: WorkerToPoolMessageStatus.Failure; error: unknown } ); /** diff --git a/packages/worker/src/internals/worker-runner/NoWorkerRunner.ts b/packages/worker/src/internals/worker-runner/NoWorkerRunner.ts index fa2cab5d..b1bb3b34 100644 --- a/packages/worker/src/internals/worker-runner/NoWorkerRunner.ts +++ b/packages/worker/src/internals/worker-runner/NoWorkerRunner.ts @@ -1,5 +1,6 @@ import type { MessagePort } from 'node:worker_threads'; import type { MainThreadToWorkerMessage, WorkerToMainThreadMessage } from '../SharedTypes.js'; +import { WorkerToPoolMessageStatus } from '../worker-pool/IWorkerPool.js'; /** * Setup the fallback worker listening to all predicates and rejecting any that has never been registered @@ -16,7 +17,7 @@ export function runNoWorker(parentPort: MessagePort, registeredPredicates: Set( const inputs = payload.source === 'main' ? payload.value : buildInputs(payload); wrapAndRunAsPromise(predicate, inputs).then( (output) => { - const message: WorkerToMainThreadMessage = { success: true, output, runId }; + const message: WorkerToMainThreadMessage = { status: WorkerToPoolMessageStatus.Success, output, runId }; parentPort.postMessage(message); }, (error) => { - const message: WorkerToMainThreadMessage = { success: false, error, runId }; + const message: WorkerToMainThreadMessage = PreconditionFailure.isFailure(error) + ? { status: WorkerToPoolMessageStatus.Skipped, runId } + : { status: WorkerToPoolMessageStatus.Failure, error, runId }; parentPort.postMessage(message); }, ); diff --git a/packages/worker/test/e2e/__properties__/supportPre.cjs b/packages/worker/test/e2e/__properties__/supportPre.cjs new file mode 100644 index 00000000..766c2c0a --- /dev/null +++ b/packages/worker/test/e2e/__properties__/supportPre.cjs @@ -0,0 +1,17 @@ +// @ts-check +const { pathToFileURL } = require('node:url'); +const fc = require('fast-check'); +const { propertyFor } = require('@fast-check/worker'); + +const property = propertyFor(pathToFileURL(__filename)); + +exports.supportPreProperty = property( + fc.integer({ min: -1000, max: 1000 }), + fc.integer({ min: -1000, max: 1000 }), + (from, to) => { + fc.pre(from < to); + if (from >= to) { + throw new Error('Oups'); + } + }, +); diff --git a/packages/worker/test/e2e/supportPre.spec.ts b/packages/worker/test/e2e/supportPre.spec.ts new file mode 100644 index 00000000..d8e8b008 --- /dev/null +++ b/packages/worker/test/e2e/supportPre.spec.ts @@ -0,0 +1,24 @@ +import { isMainThread } from 'node:worker_threads'; +import type { Parameters } from 'fast-check'; +import { assert } from '@fast-check/worker'; + +/* eslint-disable @typescript-eslint/ban-ts-comment */ +// @ts-ignore +import { supportPreProperty } from './__properties__/supportPre.cjs'; + +if (isMainThread) { + describe('@fast-check/worker', () => { + const jestTimeout = 10000; + const assertTimeout = 1000; + const defaultOptions: Parameters = { timeout: assertTimeout, seed: -340565462 }; + + it( + 'should support pre', + async () => { + // Arrange / Act / Assert + await expect(assert(supportPreProperty, defaultOptions)).resolves.not.toThrow(); + }, + jestTimeout, + ); + }); +} diff --git a/packages/worker/test/internals/worker-pool/BasicPool.spec.ts b/packages/worker/test/internals/worker-pool/BasicPool.spec.ts index 02acd5af..2a3496cb 100644 --- a/packages/worker/test/internals/worker-pool/BasicPool.spec.ts +++ b/packages/worker/test/internals/worker-pool/BasicPool.spec.ts @@ -1,4 +1,8 @@ -import type { PoolToWorkerMessage, WorkerToPoolMessage } from '../../../src/internals/worker-pool/IWorkerPool.js'; +import { + WorkerToPoolMessageStatus, + type PoolToWorkerMessage, + type WorkerToPoolMessage, +} from '../../../src/internals/worker-pool/IWorkerPool.js'; import { jest } from '@jest/globals'; jest.unstable_mockModule('node:worker_threads', () => ({ @@ -53,13 +57,14 @@ describe('BasicPool', () => { const predicateId = 0; const onSuccess = jest.fn(); const onFailure = jest.fn(); + const onSkipped = jest.fn(); const pool = new BasicPool(workerFileUrl); const workerPromise = pool.spawnNewWorker(); fireOnlineEvent(on); const worker = await workerPromise; // Act - worker.register(predicateId, 'to-worker', onSuccess, onFailure); + worker.register(predicateId, 'to-worker', onSuccess, onFailure, onSkipped); // Assert expect(worker.isAvailable()).toBe(false); @@ -67,6 +72,7 @@ describe('BasicPool', () => { expect(postMessage).toHaveBeenCalledTimes(1); expect(onSuccess).not.toHaveBeenCalled(); expect(onFailure).not.toHaveBeenCalled(); + expect(onSkipped).not.toHaveBeenCalled(); }); it('should call success handler and release the worker as soon as receiving a successful answer', async () => { @@ -76,17 +82,22 @@ describe('BasicPool', () => { const predicateId = 0; const onSuccess = jest.fn(); const onFailure = jest.fn(); + const onSkipped = jest.fn(); const successMessage = 'success!'; const pool = new BasicPool(workerFileUrl); const workerPromise = pool.spawnNewWorker(); fireOnlineEvent(on); const worker = await workerPromise; - worker.register(predicateId, 'to-worker', onSuccess, onFailure); + worker.register(predicateId, 'to-worker', onSuccess, onFailure, onSkipped); // Act const receivedMessage: PoolToWorkerMessage = postMessage.mock.calls[0][0]; const receivedRunId = receivedMessage.runId; - const message: WorkerToPoolMessage = { runId: receivedRunId, success: true, output: successMessage }; + const message: WorkerToPoolMessage = { + runId: receivedRunId, + status: WorkerToPoolMessageStatus.Success, + output: successMessage, + }; const onMessageHandler = on.mock.calls.find(([eventName]) => eventName === 'message')![1]; onMessageHandler(message); // emulate success @@ -97,6 +108,7 @@ describe('BasicPool', () => { expect(worker.isFaulty()).toBe(false); expect(postMessage).toHaveBeenCalledTimes(1); expect(onFailure).not.toHaveBeenCalled(); + expect(onSkipped).not.toHaveBeenCalled(); }); it('should call failure handler and release the worker as soon as receiving a failed answer', async () => { @@ -106,17 +118,22 @@ describe('BasicPool', () => { const predicateId = 0; const onSuccess = jest.fn(); const onFailure = jest.fn(); + const onSkipped = jest.fn(); const errorMessage = 'oups there was an error!'; const pool = new BasicPool(workerFileUrl); const workerPromise = pool.spawnNewWorker(); fireOnlineEvent(on); const worker = await workerPromise; - worker.register(predicateId, 'to-worker', onSuccess, onFailure); + worker.register(predicateId, 'to-worker', onSuccess, onFailure, onSkipped); // Act const receivedMessage: PoolToWorkerMessage = postMessage.mock.calls[0][0]; const receivedRunId = receivedMessage.runId; - const message: WorkerToPoolMessage = { runId: receivedRunId, success: false, error: errorMessage }; + const message: WorkerToPoolMessage = { + runId: receivedRunId, + status: WorkerToPoolMessageStatus.Failure, + error: errorMessage, + }; const onMessageHandler = on.mock.calls.find(([eventName]) => eventName === 'message')![1]; onMessageHandler(message); // emulate failure @@ -127,6 +144,40 @@ describe('BasicPool', () => { expect(worker.isFaulty()).toBe(false); expect(postMessage).toHaveBeenCalledTimes(1); expect(onSuccess).not.toHaveBeenCalled(); + expect(onSkipped).not.toHaveBeenCalled(); + }); + + it('should call skipped handler and release the worker as soon as receiving a skipped answer', async () => { + // Arrange + const { on, postMessage } = mockWorker(); + const workerFileUrl = new URL('file:///worker.cjs'); + const predicateId = 0; + const onSuccess = jest.fn(); + const onFailure = jest.fn(); + const onSkipped = jest.fn(); + const pool = new BasicPool(workerFileUrl); + const workerPromise = pool.spawnNewWorker(); + fireOnlineEvent(on); + const worker = await workerPromise; + worker.register(predicateId, 'to-worker', onSuccess, onFailure, onSkipped); + + // Act + const receivedMessage: PoolToWorkerMessage = postMessage.mock.calls[0][0]; + const receivedRunId = receivedMessage.runId; + const message: WorkerToPoolMessage = { + runId: receivedRunId, + status: WorkerToPoolMessageStatus.Skipped, + }; + const onMessageHandler = on.mock.calls.find(([eventName]) => eventName === 'message')![1]; + onMessageHandler(message); // emulate failure + + // Assert + expect(onSkipped).toHaveBeenCalledTimes(1); + expect(worker.isAvailable()).toBe(true); + expect(worker.isFaulty()).toBe(false); + expect(postMessage).toHaveBeenCalledTimes(1); + expect(onSuccess).not.toHaveBeenCalled(); + expect(onFailure).not.toHaveBeenCalled(); }); it('should ignore success or failures not related to the current run', async () => { @@ -136,18 +187,31 @@ describe('BasicPool', () => { const predicateId = 0; const onSuccess = jest.fn(); const onFailure = jest.fn(); + const onSkipped = jest.fn(); const pool = new BasicPool(workerFileUrl); const workerPromise = pool.spawnNewWorker(); fireOnlineEvent(on); const worker = await workerPromise; - worker.register(predicateId, 'to-worker', onSuccess, onFailure); + worker.register(predicateId, 'to-worker', onSuccess, onFailure, onSkipped); // Act const receivedMessage: PoolToWorkerMessage = postMessage.mock.calls[0][0]; const receivedRunId = receivedMessage.runId; - const message1: WorkerToPoolMessage = { runId: receivedRunId - 1, success: true, output: 'm1' }; - const message2: WorkerToPoolMessage = { runId: receivedRunId + 1, success: true, output: 'm2' }; - const message3: WorkerToPoolMessage = { runId: receivedRunId + 1, success: false, error: 'm3' }; + const message1: WorkerToPoolMessage = { + runId: receivedRunId - 1, + status: WorkerToPoolMessageStatus.Success, + output: 'm1', + }; + const message2: WorkerToPoolMessage = { + runId: receivedRunId + 1, + status: WorkerToPoolMessageStatus.Success, + output: 'm2', + }; + const message3: WorkerToPoolMessage = { + runId: receivedRunId + 1, + status: WorkerToPoolMessageStatus.Failure, + error: 'm3', + }; const onMessageHandler = on.mock.calls.find(([eventName]) => eventName === 'message')![1]; onMessageHandler(message1); onMessageHandler(message2); @@ -159,6 +223,7 @@ describe('BasicPool', () => { expect(postMessage).toHaveBeenCalledTimes(1); expect(onSuccess).not.toHaveBeenCalled(); expect(onFailure).not.toHaveBeenCalled(); + expect(onSkipped).not.toHaveBeenCalled(); }); it('should call failure handler and mark the worker as faulty when receiving messageerror message', async () => { @@ -168,11 +233,12 @@ describe('BasicPool', () => { const predicateId = 0; const onSuccess = jest.fn(); const onFailure = jest.fn(); + const onSkipped = jest.fn(); const pool = new BasicPool(workerFileUrl); const workerPromise = pool.spawnNewWorker(); fireOnlineEvent(on); const worker = await workerPromise; - worker.register(predicateId, 'to-worker', onSuccess, onFailure); + worker.register(predicateId, 'to-worker', onSuccess, onFailure, onSkipped); // Act const onErrorHandler = on.mock.calls.find(([eventName]) => eventName === 'messageerror')![1]; @@ -184,6 +250,7 @@ describe('BasicPool', () => { expect(worker.isFaulty()).toBe(false); expect(postMessage).toHaveBeenCalledTimes(1); expect(onSuccess).not.toHaveBeenCalled(); + expect(onSkipped).not.toHaveBeenCalled(); }); it('should call failure handler and mark the worker as faulty when receiving error message', async () => { @@ -193,11 +260,12 @@ describe('BasicPool', () => { const predicateId = 0; const onSuccess = jest.fn(); const onFailure = jest.fn(); + const onSkipped = jest.fn(); const pool = new BasicPool(workerFileUrl); const workerPromise = pool.spawnNewWorker(); fireOnlineEvent(on); const worker = await workerPromise; - worker.register(predicateId, 'to-worker', onSuccess, onFailure); + worker.register(predicateId, 'to-worker', onSuccess, onFailure, onSkipped); // Act const onErrorHandler = on.mock.calls.find(([eventName]) => eventName === 'error')![1]; @@ -209,6 +277,7 @@ describe('BasicPool', () => { expect(worker.isFaulty()).toBe(true); expect(postMessage).toHaveBeenCalledTimes(1); expect(onSuccess).not.toHaveBeenCalled(); + expect(onSkipped).not.toHaveBeenCalled(); }); it('should call failure handler and mark the worker as faulty when receiving exit message', async () => { @@ -218,11 +287,12 @@ describe('BasicPool', () => { const predicateId = 0; const onSuccess = jest.fn(); const onFailure = jest.fn(); + const onSkipped = jest.fn(); const pool = new BasicPool(workerFileUrl); const workerPromise = pool.spawnNewWorker(); fireOnlineEvent(on); const worker = await workerPromise; - worker.register(predicateId, 'to-worker', onSuccess, onFailure); + worker.register(predicateId, 'to-worker', onSuccess, onFailure, onSkipped); // Act const exitCode = 101; @@ -235,6 +305,7 @@ describe('BasicPool', () => { expect(worker.isFaulty()).toBe(true); expect(postMessage).toHaveBeenCalledTimes(1); expect(onSuccess).not.toHaveBeenCalled(); + expect(onSkipped).not.toHaveBeenCalled(); }); }); @@ -313,14 +384,19 @@ describe('BasicPool', () => { const predicateId = 0; const onSuccess = jest.fn(); const onFailure = jest.fn(); + const onSkipped = jest.fn(); const pool = new BasicPool(workerFileUrl); const workerPromise = pool.spawnNewWorker(); fireOnlineEvent(on); const worker = await workerPromise; - worker.register(predicateId, 'to-worker', onSuccess, onFailure); + worker.register(predicateId, 'to-worker', onSuccess, onFailure, onSkipped); const receivedMessage: PoolToWorkerMessage = postMessage.mock.calls[0][0]; const receivedRunId = receivedMessage.runId; - const message: WorkerToPoolMessage = { runId: receivedRunId, success: true, output: 'successMessage' }; + const message: WorkerToPoolMessage = { + runId: receivedRunId, + status: WorkerToPoolMessageStatus.Success, + output: 'successMessage', + }; const onMessageHandler = on.mock.calls.find(([eventName]) => eventName === 'message')![1]; onMessageHandler(message); // emulate success @@ -334,6 +410,7 @@ describe('BasicPool', () => { expect(pool.getFirstAvailableWorker()).toBe(worker); // in the pool expect(onSuccess).toHaveBeenCalledTimes(1); expect(onFailure).not.toHaveBeenCalled(); + expect(onSkipped).not.toHaveBeenCalled(); }); it('should not alter a worker that rejected', async () => { @@ -343,14 +420,19 @@ describe('BasicPool', () => { const predicateId = 0; const onSuccess = jest.fn(); const onFailure = jest.fn(); + const onSkipped = jest.fn(); const pool = new BasicPool(workerFileUrl); const workerPromise = pool.spawnNewWorker(); fireOnlineEvent(on); const worker = await workerPromise; - worker.register(predicateId, 'to-worker', onSuccess, onFailure); + worker.register(predicateId, 'to-worker', onSuccess, onFailure, onSkipped); const receivedMessage: PoolToWorkerMessage = postMessage.mock.calls[0][0]; const receivedRunId = receivedMessage.runId; - const message: WorkerToPoolMessage = { runId: receivedRunId, success: false, error: 'errorMessage' }; + const message: WorkerToPoolMessage = { + runId: receivedRunId, + status: WorkerToPoolMessageStatus.Failure, + error: 'errorMessage', + }; const onMessageHandler = on.mock.calls.find(([eventName]) => eventName === 'message')![1]; onMessageHandler(message); // emulate failure @@ -364,6 +446,7 @@ describe('BasicPool', () => { expect(pool.getFirstAvailableWorker()).toBe(worker); // in the pool expect(onSuccess).not.toHaveBeenCalled(); expect(onFailure).toHaveBeenCalledTimes(1); + expect(onSkipped).not.toHaveBeenCalled(); }); it('should stop a registered worker still being running', async () => { @@ -373,11 +456,12 @@ describe('BasicPool', () => { const predicateId = 0; const onSuccess = jest.fn(); const onFailure = jest.fn(); + const onSkipped = jest.fn(); const pool = new BasicPool(workerFileUrl); const workerPromise = pool.spawnNewWorker(); fireOnlineEvent(on); const worker = await workerPromise; - worker.register(predicateId, 'to-worker', onSuccess, onFailure); + worker.register(predicateId, 'to-worker', onSuccess, onFailure, onSkipped); expect(terminate).not.toHaveBeenCalled(); // Act @@ -390,6 +474,7 @@ describe('BasicPool', () => { expect(pool.getFirstAvailableWorker()).toBe(undefined); // no worker available expect(onSuccess).not.toHaveBeenCalled(); expect(onFailure).not.toHaveBeenCalled(); + expect(onSkipped).not.toHaveBeenCalled(); }); }); });