Files
promptfoo--promptfoo/test/python/workerPool.test.ts
T
wehub-resource-sync 0d3cb498a3
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Waiting to run
Test and Publish Multi-arch Docker Image / test (push) Waiting to run
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-arm64 platform:linux/arm64 runner:ubuntu-24.04-arm]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Blocked by required conditions
Validate Renovate Config / Validate Renovate Configuration (push) Waiting to run
CI / Shell Format Check (push) Has been cancelled
CI / Check Ruby (3.4) (push) Has been cancelled
CI / CI Config (push) Has been cancelled
CI / Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }} (push) Has been cancelled
CI / Build on Node ${{ matrix.node }} (push) Has been cancelled
CI / Style Check (push) Has been cancelled
CI / Generate Assets (push) Has been cancelled
CI / Check Python (3.14) (push) Has been cancelled
CI / Check Python (3.9) (push) Has been cancelled
CI / Build Docs (push) Has been cancelled
CI / Code Scan Action (push) Has been cancelled
CI / Site tests (push) Has been cancelled
CI / webui tests (push) Has been cancelled
CI / Run Integration Tests (push) Has been cancelled
CI / Run Smoke Tests (push) Has been cancelled
CI / Go Tests (push) Has been cancelled
CI / Share Test (push) Has been cancelled
CI / Redteam (Production API) (push) Has been cancelled
CI / Redteam (Staging API) (push) Has been cancelled
CI / GitHub Actions Lint (push) Has been cancelled
CI / Check Ruby (3.0) (push) Has been cancelled
release-please / release-please (push) Has been cancelled
release-please / build (push) Has been cancelled
release-please / publish-npm (push) Has been cancelled
release-please / publish-npm-backfill (push) Has been cancelled
release-please / docker (push) Has been cancelled
release-please / publish-code-scan-action (push) Has been cancelled
release-please / attest-code-scan-action (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:24:08 +08:00

215 lines
6.8 KiB
TypeScript

import fs from 'fs';
import path from 'path';
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
import { PythonWorkerPool } from '../../src/python/workerPool';
// Windows CI has severe filesystem delays (antivirus, etc.) - allow up to 90s
// Non-Windows CI can also have timing variance with Python IPC, so use 15s (matching windows-path.test.ts)
const TEST_TIMEOUT = process.platform === 'win32' ? 90000 : 15000;
// Skip on Windows CI due to aggressive file security policies blocking temp file IPC
// Works fine on local Windows and all other platforms
const describeOrSkip = process.platform === 'win32' && process.env.CI ? describe.skip : describe;
describeOrSkip('PythonWorkerPool', () => {
let singleWorkerPool: PythonWorkerPool;
let multiWorkerPool: PythonWorkerPool;
let multiApiPool: PythonWorkerPool;
const testScriptPath = path.join(__dirname, 'fixtures', 'counter_provider.py');
const multiApiPath = path.join(__dirname, 'fixtures', 'pool_multi_api.py');
const fixturesDir = path.join(__dirname, 'fixtures');
beforeAll(async () => {
// Create fixtures directory if it doesn't exist
if (!fs.existsSync(fixturesDir)) {
fs.mkdirSync(fixturesDir, { recursive: true });
}
// Create test fixture with global state
fs.writeFileSync(
testScriptPath,
`
# Global counter - persists across calls within same worker
call_count = 0
def call_api(prompt, options, context):
global call_count
call_count += 1
return {"output": f"Call #{call_count}: {prompt}", "count": call_count}
def reset_call_count():
global call_count
call_count = 0
return {"count": call_count}
`,
);
fs.writeFileSync(
multiApiPath,
`
def call_api(prompt, options, context):
return {"output": f"text: {prompt}", "type": "text"}
def call_embedding_api(prompt, options, context):
return {"output": [0.1, 0.2], "type": "embedding"}
`,
);
singleWorkerPool = new PythonWorkerPool(testScriptPath, 'call_api', 1);
multiWorkerPool = new PythonWorkerPool(testScriptPath, 'call_api', 2);
multiApiPool = new PythonWorkerPool(multiApiPath, 'call_api', 2);
await Promise.all([
singleWorkerPool.initialize(),
multiWorkerPool.initialize(),
multiApiPool.initialize(),
]);
});
beforeEach(async () => {
await Promise.all([
singleWorkerPool.execute('reset_call_count', []),
...Array.from({ length: multiWorkerPool.getWorkerCount() }, () =>
multiWorkerPool.execute('reset_call_count', []),
),
]);
});
afterAll(async () => {
await Promise.all(
[singleWorkerPool, multiWorkerPool, multiApiPool]
.filter((pool): pool is PythonWorkerPool => Boolean(pool))
.map((pool) => pool.shutdown()),
);
for (const fixturePath of [testScriptPath, multiApiPath]) {
if (fs.existsSync(fixturePath)) {
fs.unlinkSync(fixturePath);
}
}
});
it(
'should initialize pool with specified worker count',
async () => {
expect(multiWorkerPool.getWorkerCount()).toBe(2);
},
TEST_TIMEOUT,
);
it('should reject invalid worker counts', async () => {
// Test zero workers
const zeroWorkerPool = new PythonWorkerPool(testScriptPath, 'call_api', 0);
await expect(zeroWorkerPool.initialize()).rejects.toThrow(
'Invalid worker count: 0. Must be at least 1.',
);
// Test negative workers
const negativeWorkerPool = new PythonWorkerPool(testScriptPath, 'call_api', -1);
await expect(negativeWorkerPool.initialize()).rejects.toThrow(
'Invalid worker count: -1. Must be at least 1.',
);
});
it(
'should execute calls sequentially with 1 worker',
async () => {
const result1 = await singleWorkerPool.execute('call_api', ['First', {}, {}]);
const result2 = await singleWorkerPool.execute('call_api', ['Second', {}, {}]);
const result3 = await singleWorkerPool.execute('call_api', ['Third', {}, {}]);
// Same worker, counter increments
expect(result1.count).toBe(1);
expect(result2.count).toBe(2);
expect(result3.count).toBe(3);
},
TEST_TIMEOUT,
);
it(
'should handle concurrent calls with multiple workers',
async () => {
// Execute 4 calls concurrently
const promises = [
multiWorkerPool.execute('call_api', ['Call 1', {}, {}]),
multiWorkerPool.execute('call_api', ['Call 2', {}, {}]),
multiWorkerPool.execute('call_api', ['Call 3', {}, {}]),
multiWorkerPool.execute('call_api', ['Call 4', {}, {}]),
];
const results = await Promise.all(promises);
// Each worker maintains its own counter
// With 2 workers, work should be distributed across both (not all to one worker)
const counts = results.map((r) => r.count);
const uniqueCounts = new Set(counts);
// Verify multiple workers were used (at least 2 different counts)
expect(uniqueCounts.size).toBeGreaterThan(1);
// Verify all calls completed successfully
expect(results.length).toBe(4);
},
TEST_TIMEOUT,
);
it(
'should queue requests when all workers busy',
async () => {
// Start 3 concurrent calls with 1 worker - should queue
const promises = [
singleWorkerPool.execute('call_api', ['Q1', {}, {}]),
singleWorkerPool.execute('call_api', ['Q2', {}, {}]),
singleWorkerPool.execute('call_api', ['Q3', {}, {}]),
];
const results = await Promise.all(promises);
// All should complete (queued and executed)
expect(results.length).toBe(3);
expect(results[0].count).toBe(1);
expect(results[1].count).toBe(2);
expect(results[2].count).toBe(3);
},
TEST_TIMEOUT,
);
it(
'should handle different function names across pool',
async () => {
const results = await Promise.all([
multiApiPool.execute('call_api', ['hello', {}, {}]),
multiApiPool.execute('call_embedding_api', ['world', {}, {}]),
multiApiPool.execute('call_api', ['again', {}, {}]),
]);
expect(results[0].type).toBe('text');
expect(results[1].type).toBe('embedding');
expect(results[2].type).toBe('text');
},
TEST_TIMEOUT,
);
it(
'should process queued requests after worker becomes available',
async () => {
// Fire off 5 requests - should queue and process sequentially
const promises = [];
for (let i = 0; i < 5; i++) {
promises.push(singleWorkerPool.execute('call_api', [`request-${i}`, {}, {}]));
}
const results = await Promise.all(promises);
// All requests should complete
expect(results.length).toBe(5);
// Counter should increment sequentially (all in same worker)
const counts = results.map((r) => r.count);
expect(counts).toEqual([1, 2, 3, 4, 5]);
},
TEST_TIMEOUT,
);
});