mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-10 03:27:59 +00:00
124 lines
3.8 KiB
TypeScript
124 lines
3.8 KiB
TypeScript
// gitnexus/test/unit/move/concurrency.test.ts
|
|
import { describe, it, expect } from 'vitest';
|
|
import { mapWithConcurrency } from '../../../src/core/move/concurrency.js';
|
|
|
|
describe('mapWithConcurrency', () => {
|
|
it('never exceeds the concurrency limit and reaches it', async () => {
|
|
let inFlight = 0,
|
|
peak = 0;
|
|
await mapWithConcurrency([1, 2, 3, 4, 5, 6], 2, async () => {
|
|
inFlight++;
|
|
peak = Math.max(peak, inFlight);
|
|
await new Promise((r) => setTimeout(r, 5));
|
|
inFlight--;
|
|
});
|
|
expect(peak).toBe(2);
|
|
});
|
|
|
|
it('fail-fast returns first error and stops scheduling new work', async () => {
|
|
const started: number[] = [];
|
|
const { failure } = await mapWithConcurrency(
|
|
[1, 2, 3, 4, 5, 6],
|
|
1,
|
|
async (n) => {
|
|
started.push(n);
|
|
if (n === 2) throw new Error('boom');
|
|
},
|
|
{ failFast: true },
|
|
);
|
|
expect(failure?.error).toBeInstanceOf(Error);
|
|
expect(failure?.item).toBe(2);
|
|
expect(started).toEqual([1, 2]); // 3..6 never scheduled
|
|
});
|
|
|
|
it('fail-fast with limit=2: awaits in-flight workers, stops new scheduling, captures first error', async () => {
|
|
let resolveA!: () => void;
|
|
const latchA = new Promise<void>((r) => {
|
|
resolveA = r;
|
|
});
|
|
const aCompleted: boolean[] = [];
|
|
const started: number[] = [];
|
|
|
|
const resultPromise = mapWithConcurrency(
|
|
[1, 2, 3, 4, 5],
|
|
2,
|
|
async (n) => {
|
|
started.push(n);
|
|
if (n === 1) {
|
|
await latchA;
|
|
aCompleted.push(true);
|
|
return;
|
|
}
|
|
if (n === 2) throw new Error('worker-B-fail');
|
|
},
|
|
{ failFast: true },
|
|
);
|
|
|
|
// Allow microtasks to run so both workers start and worker B's rejection settles.
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
|
|
// Worker B has rejected; worker A is still blocked on latchA.
|
|
// Release A so mapWithConcurrency can resolve.
|
|
resolveA();
|
|
|
|
const { failure } = await resultPromise;
|
|
|
|
// (a) Worker A completed before the result resolved.
|
|
expect(aCompleted).toHaveLength(1);
|
|
// (b) Items after the failing index were never scheduled.
|
|
expect(started).not.toContain(3);
|
|
expect(started).not.toContain(4);
|
|
expect(started).not.toContain(5);
|
|
// (c) The failure captures the first (and only) error.
|
|
expect(failure?.error).toBeInstanceOf(Error);
|
|
expect((failure?.error as Error).message).toBe('worker-B-fail');
|
|
expect(failure?.item).toBe(2);
|
|
});
|
|
|
|
it('reports the earliest scheduled in-flight failure, not the first rejection to settle', async () => {
|
|
let releaseFirst!: () => void;
|
|
const firstCanFail = new Promise<void>((resolve) => {
|
|
releaseFirst = resolve;
|
|
});
|
|
|
|
const resultPromise = mapWithConcurrency(
|
|
['causal', 'collateral', 'never-started'],
|
|
2,
|
|
async (item) => {
|
|
if (item === 'causal') {
|
|
await firstCanFail;
|
|
throw new Error('causal failure');
|
|
}
|
|
if (item === 'collateral') {
|
|
releaseFirst();
|
|
throw new Error('collateral failure settled first');
|
|
}
|
|
},
|
|
{ failFast: true },
|
|
);
|
|
|
|
const { failure } = await resultPromise;
|
|
expect(failure?.item).toBe('causal');
|
|
expect((failure?.error as Error).message).toBe('causal failure');
|
|
});
|
|
|
|
it('without failFast, first worker rejection rejects the returned promise', async () => {
|
|
await expect(
|
|
mapWithConcurrency([1, 2, 3], 2, async (n) => {
|
|
if (n === 2) throw new Error('non-failfast-rejection');
|
|
}),
|
|
).rejects.toThrow('non-failfast-rejection');
|
|
});
|
|
|
|
it('limit=1 runs sequentially in order', async () => {
|
|
const order: number[] = [];
|
|
const { results } = await mapWithConcurrency([1, 2, 3], 1, async (n) => {
|
|
order.push(n);
|
|
return n * 2;
|
|
});
|
|
expect(order).toEqual([1, 2, 3]);
|
|
expect(results).toEqual([2, 4, 6]);
|
|
});
|
|
});
|