or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

advanced

combinators.mdconcurrency-testing.mdmodel-based-testing.md
configuration.mdindex.md
tile.json

concurrency-testing.mddocs/advanced/

Concurrency Testing

Concurrency testing helps detect race conditions and timing-dependent bugs in asynchronous code by controlling the execution order of promises and async operations.

Overview

Schedulers allow you to:

  • Control the order in which promises resolve
  • Test different execution interleavings
  • Reproduce race conditions deterministically
  • Verify async code works correctly regardless of timing

Capabilities

Scheduler Generation

Generate schedulers that control async execution order.

/**
 * Generate schedulers for testing asynchronous code with various execution orderings
 * @param constraints - Constraints for scheduler behavior
 * @returns Arbitrary generating schedulers
 */
function scheduler<TMetaData = unknown>(constraints?: SchedulerConstraints): Arbitrary<Scheduler<TMetaData>>;

interface SchedulerConstraints {
  act?: SchedulerAct;
}

type SchedulerAct = (f: () => Promise<void>) => Promise<unknown>;

Custom Schedulers

Create schedulers with predefined execution order for deterministic replay.

/**
 * Create custom scheduler with predefined resolution order
 * @param customOrderingOrConstraints - Execution order array or constraints
 * @param constraintsOrUndefined - Optional constraints when first param is ordering
 * @returns Scheduler or template tag function for creating schedulers
 */
function schedulerFor<TMetaData = unknown>(
  customOrderingOrConstraints?: number[] | SchedulerConstraints,
  constraintsOrUndefined?: SchedulerConstraints
): Scheduler<TMetaData> | ((strs: TemplateStringsArray, ...ordering: number[]) => Scheduler<TMetaData>);

Scheduler Interface

/**
 * Interface for controlling promise execution order
 */
interface Scheduler<TMetaData = unknown> {
  /**
   * Wrap an async operation for scheduled execution
   * @param task - Async function to schedule
   * @returns Promise that resolves according to schedule
   */
  schedule<T>(task: Promise<T>, label?: string, metadata?: TMetaData): Promise<T>;

  /**
   * Wrap multiple async operations
   * @param tasks - Array of promises to schedule
   * @returns Promise resolving to array of results
   */
  scheduleSequence(tasks: SchedulerSequenceItem<TMetaData>[]): Promise<unknown[]>;

  /**
   * Get number of scheduled tasks
   */
  count(): number;

  /**
   * Wait for all scheduled tasks to complete
   */
  waitAll(): Promise<void>;

  /**
   * Wait for one scheduled task to complete
   */
  waitOne(): Promise<void>;

  /**
   * Get execution report
   */
  report(): SchedulerReportItem<TMetaData>[];
}

interface SchedulerSequenceItem<TMetaData> {
  builder: () => Promise<unknown>;
  label: string;
  metadata?: TMetaData;
}

interface SchedulerReportItem<TMetaData> {
  status: 'resolved' | 'rejected' | 'pending';
  value?: unknown;
  label: string;
  metadata?: TMetaData;
}

Basic Usage Example

Test async operations with different execution orders:

import { scheduler, property, assert } from 'fast-check';

// Function to test
async function transferMoney(
  scheduler: Scheduler,
  from: Account,
  to: Account,
  amount: number
): Promise<void> {
  const fromBalance = await scheduler.schedule(from.getBalance(), 'get-from');
  const toBalance = await scheduler.schedule(to.getBalance(), 'get-to');

  if (fromBalance < amount) {
    throw new Error('Insufficient funds');
  }

  await scheduler.schedule(from.setBalance(fromBalance - amount), 'set-from');
  await scheduler.schedule(to.setBalance(toBalance + amount), 'set-to');
}

// Test with different execution orders
assert(
  property(scheduler(), async (s) => {
    const from = new Account(100);
    const to = new Account(50);

    await transferMoney(s, from, to, 30);
    await s.waitAll();

    // Verify final state
    const fromFinal = await from.getBalance();
    const toFinal = await to.getBalance();

    return fromFinal === 70 && toFinal === 80;
  })
);

Detecting Race Conditions

Test concurrent operations to find race conditions:

import { scheduler, property, assert } from 'fast-check';

class Counter {
  private value = 0;

  async increment(s: Scheduler): Promise<void> {
    const current = await s.schedule(Promise.resolve(this.value), 'read');
    await s.schedule(Promise.resolve(), 'delay'); // Simulate async work
    this.value = current + 1;
    await s.schedule(Promise.resolve(), 'write');
  }

  getValue(): number {
    return this.value;
  }
}

// This test will likely find a race condition
assert(
  property(scheduler(), async (s) => {
    const counter = new Counter();

    // Two concurrent increments
    const p1 = counter.increment(s);
    const p2 = counter.increment(s);

    await Promise.all([p1, p2]);
    await s.waitAll();

    // Should be 2, but might be 1 due to race condition
    return counter.getValue() === 2;
  })
);

Custom Execution Order

Reproduce specific scenarios with custom orderings:

import { schedulerFor } from 'fast-check';

// Define specific execution order
const s = schedulerFor([2, 1, 3]); // Tasks resolve in order: 2nd, 1st, 3rd

async function testScenario() {
  const result1 = s.schedule(asyncOp1(), 'op1'); // Executes 2nd
  const result2 = s.schedule(asyncOp2(), 'op2'); // Executes 1st
  const result3 = s.schedule(asyncOp3(), 'op3'); // Executes 3rd

  await s.waitAll();

  // Verify behavior with this specific ordering
  const report = s.report();
  console.log(report);
}

// Using template literal syntax
const s2 = schedulerFor`${2}${1}${3}`;

Integration with Model-Based Testing

Combine schedulers with model-based testing:

import {
  scheduler,
  commands,
  scheduledModelRun,
  property,
  assert,
  constant,
} from 'fast-check';

class AsyncCommand implements AsyncCommand<Model, Real, true> {
  async check(m: Readonly<Model>): Promise<boolean> {
    return true;
  }

  async run(m: Model, r: Real): Promise<void> {
    // Async operations on model and real system
    await m.performOperation();
    await r.performOperation();
  }
}

// Test with scheduler to find race conditions
assert(
  property(
    scheduler(),
    commands([constant(new AsyncCommand())], { maxCommands: 10 }),
    async (s, cmds) => {
      await scheduledModelRun(s, setup, cmds);
    }
  )
);

Debugging with Reports

Use scheduler reports to understand execution order:

import { schedulerFor } from 'fast-check';

const s = schedulerFor();

async function debug() {
  await s.schedule(asyncOp1(), 'operation-1', { priority: 'high' });
  await s.schedule(asyncOp2(), 'operation-2', { priority: 'low' });
  await s.schedule(asyncOp3(), 'operation-3', { priority: 'medium' });

  await s.waitAll();

  const report = s.report();
  report.forEach((item) => {
    console.log(`${item.label}: ${item.status}`, item.metadata);
  });
}

Advanced: Custom Act Function

Wrap scheduled tasks with custom behavior:

import { scheduler } from 'fast-check';

// Custom act function that logs all scheduled operations
const customAct = async (f: () => Promise<void>) => {
  console.log('Starting scheduled operation...');
  await f();
  console.log('Completed scheduled operation');
};

const s = scheduler({ act: customAct });

// Use scheduler with custom act function
assert(
  property(s, async (scheduler) => {
    // All scheduled operations will be logged
    await scheduler.schedule(asyncOp(), 'my-op');
    await scheduler.waitAll();
  })
);

Type Definitions

type SchedulerAct = (f: () => Promise<void>) => Promise<unknown>;