/** * 集成测试套件 - 黑盒功能测试 * * 测试矩阵:输入代码 + 变量 → 预期输出 * 覆盖真实使用场景,不关心内部实现 */ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { ProcessPool } from '../../src/pool/process-pool'; import { PythonProcessPool } from '../../src/pool/python-process-pool'; // ============================================================ // 测试用例矩阵类型 // ============================================================ interface TestCase { name: string; code: string; variables?: Record; expect: { success: boolean; codeReturn?: any; // 精确匹配 codeReturnMatch?: Record; // 部分匹配 errorMatch?: RegExp; // 错误信息匹配 hasLog?: boolean; // 是否有日志输出 logMatch?: RegExp; // 日志内容匹配 }; } function runMatrix(getPool: () => ProcessPool | PythonProcessPool, cases: TestCase[]) { for (const tc of cases) { it(tc.name, async () => { const result = await getPool().execute({ code: tc.code, variables: tc.variables || {} }); expect(result.success).toBe(tc.expect.success); if (tc.expect.codeReturn !== undefined) { expect(result.data?.codeReturn).toEqual(tc.expect.codeReturn); } if (tc.expect.codeReturnMatch) { for (const [key, val] of Object.entries(tc.expect.codeReturnMatch)) { expect(result.data?.codeReturn?.[key]).toEqual(val); } } if (tc.expect.errorMatch) { expect(result.message).toMatch(tc.expect.errorMatch); } if (tc.expect.hasLog) { expect(result.data?.log).toBeTruthy(); expect(result.data!.log!.length).toBeGreaterThan(0); } if (tc.expect.logMatch) { expect(result.data?.log).toMatch(tc.expect.logMatch); } }); } } // ============================================================ // JS 功能测试矩阵 // ============================================================ describe('JS 功能测试', () => { let pool: ProcessPool; beforeAll(async () => { pool = new ProcessPool(1); await pool.init(); }); afterAll(async () => { await pool.shutdown(); }); // --- 基础运算 --- describe('基础运算', () => { runMatrix( () => pool, [ { name: '返回简单对象', code: `async function main() { return { hello: 'world' }; }`, expect: { success: true, codeReturn: { hello: 'world' } } }, { name: '数学运算', code: `async function main() { return { sum: 1 + 2, product: 3 * 4, division: 10 / 3 }; }`, expect: { success: true, codeReturnMatch: { sum: 3, product: 12 } } }, { name: '字符串操作', code: `async function main() { const s = 'Hello, World!'; return { upper: s.toUpperCase(), len: s.length, includes: s.includes('World') }; }`, expect: { success: true, codeReturn: { upper: 'HELLO, WORLD!', len: 13, includes: true } } }, { name: '数组操作', code: `async function main() { const arr = [3, 1, 4, 1, 5, 9]; return { sorted: [...arr].sort((a,b) => a-b), sum: arr.reduce((a,b) => a+b, 0), len: arr.length }; }`, expect: { success: true, codeReturn: { sorted: [1, 1, 3, 4, 5, 9], sum: 23, len: 6 } } }, { name: 'JSON 解析与序列化', code: `async function main() { const obj = { a: 1, b: [2, 3] }; const json = JSON.stringify(obj); const parsed = JSON.parse(json); return { json, equal: JSON.stringify(parsed) === json }; }`, expect: { success: true, codeReturn: { json: '{"a":1,"b":[2,3]}', equal: true } } }, { name: '正则表达式', code: `async function main() { const text = 'Email: test@example.com, Phone: 123-456-7890'; const email = text.match(/[\\w.]+@[\\w.]+/)?.[0]; const phone = text.match(/\\d{3}-\\d{3}-\\d{4}/)?.[0]; return { email, phone }; }`, expect: { success: true, codeReturn: { email: 'test@example.com', phone: '123-456-7890' } } }, { name: 'Date 操作', code: `async function main() { const d = new Date('2024-01-15T12:00:00Z'); return { year: d.getUTCFullYear(), month: d.getUTCMonth() + 1, day: d.getUTCDate() }; }`, expect: { success: true, codeReturn: { year: 2024, month: 1, day: 15 } } }, { name: 'Promise.all 并发', code: `async function main() { const results = await Promise.all([ Promise.resolve(1), Promise.resolve(2), Promise.resolve(3) ]); return { results, sum: results.reduce((a,b) => a+b, 0) }; }`, expect: { success: true, codeReturn: { results: [1, 2, 3], sum: 6 } } }, { name: 'Map 和 Set', code: `async function main() { const m = new Map([['a', 1], ['b', 2]]); const s = new Set([1, 2, 2, 3, 3]); return { mapSize: m.size, mapGet: m.get('b'), setSize: s.size }; }`, expect: { success: true, codeReturn: { mapSize: 2, mapGet: 2, setSize: 3 } } } ] ); }); // --- 变量传递 --- describe('变量传递', () => { runMatrix( () => pool, [ { name: '接收字符串变量', code: `async function main(v) { return { greeting: 'Hello, ' + v.name + '!' }; }`, variables: { name: 'Alice' }, expect: { success: true, codeReturn: { greeting: 'Hello, Alice!' } } }, { name: '接收数字变量', code: `async function main(v) { return { doubled: v.num * 2 }; }`, variables: { num: 21 }, expect: { success: true, codeReturn: { doubled: 42 } } }, { name: '接收复杂对象变量', code: `async function main(v) { const items = JSON.parse(v.items); return { count: items.length, first: items[0] }; }`, variables: { items: '[{"id":1,"name":"foo"},{"id":2,"name":"bar"}]' }, expect: { success: true, codeReturn: { count: 2, first: { id: 1, name: 'foo' } } } }, { name: '空变量对象', code: `async function main(v) { return { keys: Object.keys(v || {}).length }; }`, variables: {}, expect: { success: true, codeReturn: { keys: 0 } } }, { name: '多个变量', code: `async function main(v) { return { result: v.a + ' ' + v.b + ' ' + v.c }; }`, variables: { a: 'hello', b: 'beautiful', c: 'world' }, expect: { success: true, codeReturn: { result: 'hello beautiful world' } } } ] ); }); // --- console 日志 --- describe('日志输出', () => { runMatrix( () => pool, [ { name: 'console.log 被捕获', code: `async function main() { console.log('debug info'); return { done: true }; }`, expect: { success: true, codeReturn: { done: true }, logMatch: /debug info/ } }, { name: 'console.error 不报错,内容被捕获', code: `async function main() { console.error('err msg'); return { done: true }; }`, expect: { success: true, codeReturn: { done: true }, logMatch: /err msg/ } }, { name: 'console.warn 不报错,内容被捕获', code: `async function main() { console.warn('warn msg'); return { done: true }; }`, expect: { success: true, codeReturn: { done: true }, logMatch: /warn msg/ } }, { name: 'console.info 不报错,内容被捕获', code: `async function main() { console.info('info msg'); return { done: true }; }`, expect: { success: true, codeReturn: { done: true }, logMatch: /info msg/ } }, { name: 'console.debug 不报错,内容被捕获', code: `async function main() { console.debug('debug msg'); return { done: true }; }`, expect: { success: true, codeReturn: { done: true }, logMatch: /debug msg/ } }, { name: '多次 console 调用均被捕获', code: `async function main() { console.log('a'); console.warn('b'); console.error('c'); return { done: true }; }`, expect: { success: true, codeReturn: { done: true }, logMatch: /a/ } } ] ); }); // --- 白名单模块 --- describe('白名单模块', () => { runMatrix( () => pool, [ { name: 'require crypto-js', code: `async function main() { const CryptoJS = require('crypto-js'); const hash = CryptoJS.MD5('hello').toString(); return { hash }; }`, expect: { success: true, codeReturnMatch: { hash: '5d41402abc4b2a76b9719d911017c592' } } }, { name: 'require moment', code: `async function main() { const moment = require('moment'); const d = moment('2024-01-15'); return { formatted: d.format('YYYY/MM/DD') }; }`, expect: { success: true, codeReturn: { formatted: '2024/01/15' } } }, { name: 'require lodash', code: `async function main() { const _ = require('lodash'); return { chunk: _.chunk([1,2,3,4,5,6], 2) }; }`, expect: { success: true, codeReturn: { chunk: [ [1, 2], [3, 4], [5, 6] ] } } }, { name: 'require lodash groupBy', code: `async function main({ items }) { const _ = require('lodash'); const grouped = _.groupBy(items, 'type'); const counts = _.mapValues(grouped, arr => arr.length); return counts; }`, variables: { items: [ { type: 'a', v: 1 }, { type: 'b', v: 2 }, { type: 'a', v: 3 }, { type: 'c', v: 4 } ] }, expect: { success: true, codeReturn: { a: 2, b: 1, c: 1 } } }, { name: 'require dayjs', code: `async function main() { const dayjs = require('dayjs'); const d = dayjs('2024-01-15'); return { formatted: d.format('YYYY/MM/DD'), month: d.month() + 1 }; }`, expect: { success: true, codeReturn: { formatted: '2024/01/15', month: 1 } } }, { name: 'require uuid', code: `async function main() { const { v4 } = require('uuid'); const id = v4(); return { valid: /^[0-9a-f-]{36}$/.test(id) }; }`, expect: { success: true, codeReturn: { valid: true } } }, { name: 'require crypto-js AES 加解密', code: `async function main() { const CryptoJS = require('crypto-js'); const encrypted = CryptoJS.AES.encrypt('hello', 'secret').toString(); const decrypted = CryptoJS.AES.decrypt(encrypted, 'secret').toString(CryptoJS.enc.Utf8); return { match: decrypted === 'hello' }; }`, expect: { success: true, codeReturn: { match: true } } } ] ); }); // --- 错误处理 --- describe('错误处理', () => { runMatrix( () => pool, [ { name: '语法错误', code: `async function main() { return {{{ }`, expect: { success: false } }, { name: '运行时异常', code: `async function main() { throw new Error('boom'); }`, expect: { success: false, errorMatch: /boom/ } }, { name: '未定义变量', code: `async function main() { return { val: undefinedVar }; }`, expect: { success: false } }, { name: '超时', code: `async function main() { while(true) {} return {}; }`, expect: { success: false } }, { name: '无限递归', code: `function recurse() { return recurse(); } async function main() { return recurse(); }`, expect: { success: false } } ] ); }); // --- 内置工具函数 --- describe('内置工具函数', () => { runMatrix( () => pool, [ { name: 'delay 正常延迟', code: `async function main() { const start = Date.now(); await delay(500); const elapsed = Date.now() - start; return { elapsed: elapsed >= 400 }; }`, expect: { success: true, codeReturn: { elapsed: true } } }, { name: 'strToBase64 编码', code: `async function main() { const encoded = strToBase64('Hello, World!'); return { encoded }; }`, expect: { success: true, codeReturn: { encoded: 'SGVsbG8sIFdvcmxkIQ==' } } }, { name: 'countToken 计算', code: `async function main({ text }) { return { tokens: countToken(text) }; }`, variables: { text: 'Hello, this is a test sentence.' }, expect: { success: true } } ] ); }); // --- 网络请求 --- describe('网络请求', () => { runMatrix( () => pool, [ { name: 'httpRequest GET', code: `async function main() { const res = await httpRequest('https://www.baidu.com'); return { status: res.status, hasData: res.data.length > 0 }; }`, expect: { success: true, codeReturnMatch: { status: 200, hasData: true } } }, { name: 'httpRequest POST JSON', code: `async function main() { const res = await httpRequest('https://www.baidu.com', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: { message: 'hello' } }); return { hasStatus: typeof res.status === 'number' }; }`, expect: { success: true, codeReturnMatch: { hasStatus: true } } } ] ); }); }); // ============================================================ // Python 功能测试矩阵 // ============================================================ describe('Python 功能测试', () => { let pool: PythonProcessPool; beforeAll(async () => { pool = new PythonProcessPool(1); await pool.init(); }); afterAll(async () => { await pool.shutdown(); }); // --- 基础运算 --- describe('基础运算', () => { runMatrix( () => pool, [ { name: '返回简单字典', code: `def main():\n return {'hello': 'world'}`, expect: { success: true, codeReturn: { hello: 'world' } } }, { name: '数学运算', code: `def main():\n return {'sum': 1 + 2, 'product': 3 * 4, 'power': 2 ** 10}`, expect: { success: true, codeReturn: { sum: 3, product: 12, power: 1024 } } }, { name: '字符串操作', code: `def main():\n s = 'Hello, World!'\n return {'upper': s.upper(), 'len': len(s), 'split': s.split(', ')}`, expect: { success: true, codeReturn: { upper: 'HELLO, WORLD!', len: 13, split: ['Hello', 'World!'] } } }, { name: '列表操作', code: `def main():\n arr = [3, 1, 4, 1, 5, 9]\n return {'sorted': sorted(arr), 'sum': sum(arr), 'len': len(arr)}`, expect: { success: true, codeReturn: { sorted: [1, 1, 3, 4, 5, 9], sum: 23, len: 6 } } }, { name: '字典推导式', code: `def main():\n d = {k: v**2 for k, v in {'a': 1, 'b': 2, 'c': 3}.items()}\n return d`, expect: { success: true, codeReturn: { a: 1, b: 4, c: 9 } } }, { name: '列表推导式', code: `def main():\n evens = [x for x in range(10) if x % 2 == 0]\n return {'evens': evens}`, expect: { success: true, codeReturn: { evens: [0, 2, 4, 6, 8] } } }, { name: 'try/except 异常处理', code: `def main():\n try:\n result = 1 / 0\n except ZeroDivisionError as e:\n return {'caught': True}\n return {'caught': False}`, expect: { success: true, codeReturn: { caught: true } } } ] ); }); // --- 变量传递 --- describe('变量传递', () => { runMatrix( () => pool, [ { name: '接收字符串变量', code: `def main(v):\n return {'greeting': f"Hello, {v['name']}!"}`, variables: { name: 'Bob' }, expect: { success: true, codeReturn: { greeting: 'Hello, Bob!' } } }, { name: '接收数字变量', code: `def main(v):\n return {'doubled': int(v['num']) * 2}`, variables: { num: '21' }, expect: { success: true, codeReturn: { doubled: 42 } } }, { name: '多个变量', code: `def main(v):\n return {'result': f"{v['a']} {v['b']} {v['c']}"}`, variables: { a: 'hello', b: 'beautiful', c: 'world' }, expect: { success: true, codeReturn: { result: 'hello beautiful world' } } }, { name: 'main() 无参数', code: `def main():\n return {'ok': True}`, variables: { unused: 1 }, expect: { success: true, codeReturn: { ok: true } } }, { name: 'main(a, b) 多参数展开', code: `def main(name, age):\n return {'name': name, 'age': age}`, variables: { name: 'test', age: 25 }, expect: { success: true, codeReturn: { name: 'test', age: 25 } } }, { name: 'main(content) 单参数按名取字符串值,可调用 split', code: `def main(content):\n parts = content.split(',')\n return {'parts': parts, 'count': len(parts)}`, variables: { content: 'a,b,c' }, expect: { success: true, codeReturn: { parts: ['a', 'b', 'c'], count: 3 } } }, { name: 'main(items) 单参数按名取列表值', code: `def main(items):\n return {'sum': sum(items), 'len': len(items)}`, variables: { items: [1, 2, 3, 4, 5] }, expect: { success: true, codeReturn: { sum: 15, len: 5 } } }, { name: 'main(v) 单参数名不在 variables 时回退传整个 dict', code: `def main(v):\n return {'a': v['x'], 'b': v['y']}`, variables: { x: 10, y: 20 }, expect: { success: true, codeReturn: { a: 10, b: 20 } } } ] ); }); // --- 安全模块使用 --- describe('安全模块', () => { runMatrix( () => pool, [ { name: 'import json', code: `import json\ndef main():\n data = json.dumps({'key': 'value'}, ensure_ascii=False)\n parsed = json.loads(data)\n return {'data': data, 'key': parsed['key']}`, expect: { success: true, codeReturnMatch: { key: 'value' } } }, { name: 'import math', code: `import math\ndef main():\n return {'pi': round(math.pi, 4), 'sqrt2': round(math.sqrt(2), 4), 'e': round(math.e, 4)}`, expect: { success: true, codeReturn: { pi: 3.1416, sqrt2: 1.4142, e: 2.7183 } } }, { name: 'import re', code: `import re\ndef main():\n text = 'Price: $12.99, Tax: $1.30'\n prices = re.findall(r'\\$(\\d+\\.\\d+)', text)\n return {'prices': prices}`, expect: { success: true, codeReturn: { prices: ['12.99', '1.30'] } } }, { name: 'from datetime import datetime', code: `from datetime import datetime\ndef main():\n d = datetime(2024, 1, 15, 12, 0, 0)\n return {'iso': d.isoformat(), 'year': d.year}`, expect: { success: true, codeReturn: { iso: '2024-01-15T12:00:00', year: 2024 } } }, { name: 'import hashlib', code: `import hashlib\ndef main():\n h = hashlib.md5(b'hello').hexdigest()\n return {'md5': h}`, expect: { success: true, codeReturn: { md5: '5d41402abc4b2a76b9719d911017c592' } } }, { name: 'import base64', code: `import base64\ndef main():\n encoded = base64.b64encode(b'Hello').decode()\n decoded = base64.b64decode(encoded).decode()\n return {'encoded': encoded, 'decoded': decoded}`, expect: { success: true, codeReturn: { encoded: 'SGVsbG8=', decoded: 'Hello' } } }, { name: 'import hashlib sha256', code: `import hashlib\ndef main():\n h = hashlib.sha256(b'hello').hexdigest()\n return {'hash': h}`, expect: { success: true, codeReturn: { hash: '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824' } } }, { name: 'from collections import Counter', code: `from collections import Counter\ndef main(v):\n c = Counter(v['items'])\n return dict(c.most_common())`, variables: { items: ['a', 'b', 'a', 'c', 'a', 'b'] }, expect: { success: true, codeReturnMatch: { a: 3, b: 2 } } }, { name: 'datetime + timedelta', code: `from datetime import datetime, timedelta\ndef main():\n d = datetime(2024, 1, 15)\n next_week = d + timedelta(days=7)\n return {'formatted': d.strftime('%Y/%m/%d'), 'next_week': next_week.strftime('%Y/%m/%d')}`, expect: { success: true, codeReturn: { formatted: '2024/01/15', next_week: '2024/01/22' } } } ] ); }); // --- 内置工具函数 --- describe('内置工具函数', () => { runMatrix( () => pool, [ { name: 'str_to_base64 编码', code: `def main():\n encoded = str_to_base64('Hello, World!')\n return {'encoded': encoded}`, expect: { success: true, codeReturn: { encoded: 'SGVsbG8sIFdvcmxkIQ==' } } }, { name: 'delay 正常延迟', code: `import time\ndef main():\n start = time.time()\n delay(500)\n elapsed = time.time() - start\n return {'elapsed_ok': elapsed >= 0.4}`, expect: { success: true, codeReturn: { elapsed_ok: true } } }, { name: 'count_token 计算', code: `def main(v):\n return {'tokens': count_token(v['text'])}`, variables: { text: 'Hello, this is a test sentence.' }, expect: { success: true } } ] ); }); // --- print 日志 --- describe('日志输出', () => { runMatrix( () => pool, [ { name: 'print 被捕获', code: `def main():\n print('hello log')\n return {'done': True}`, expect: { success: true, codeReturn: { done: true }, logMatch: /hello log/ } }, { name: '多次 print 均被捕获', code: `def main():\n print('line1')\n print('line2')\n return {'done': True}`, expect: { success: true, logMatch: /line1/ } } ] ); }); // --- 错误处理 --- describe('错误处理', () => { runMatrix( () => pool, [ { name: '语法错误', code: `def main():\n return {{{`, expect: { success: false } }, { name: '运行时异常', code: `def main():\n raise ValueError('boom')`, expect: { success: false, errorMatch: /boom/ } }, { name: '未定义变量', code: `def main():\n return {'val': undefined_var}`, expect: { success: false } }, { name: '超时', code: `def main():\n while True:\n pass\n return {}`, expect: { success: false } }, { name: '除零错误', code: `def main():\n return {'value': 1 / 0}`, expect: { success: false } }, { name: '索引越界', code: `def main():\n arr = [1, 2, 3]\n return {'value': arr[10]}`, expect: { success: false } }, { name: '无限递归', code: `def recurse():\n return recurse()\ndef main():\n return recurse()`, expect: { success: false } } ] ); }); // --- 网络请求 --- describe('网络请求', () => { runMatrix( () => pool, [ { name: 'http_request GET', code: `import json\ndef main():\n res = http_request('https://www.baidu.com')\n return {'status': res['status'], 'hasData': len(res['data']) > 0}`, expect: { success: true, codeReturnMatch: { status: 200, hasData: true } } }, { name: 'http_request POST JSON', code: `import json\ndef main():\n res = http_request('https://www.baidu.com', method='POST', body={'message': 'hello'})\n return {'hasStatus': type(res['status']) == int}`, expect: { success: true, codeReturnMatch: { hasStatus: true } } } ] ); }); // --- 复杂场景 --- describe('复杂场景', () => { runMatrix( () => pool, [ { name: '数据处理管道:解析CSV → 过滤 → 聚合', code: `def main(v): lines = v['csv'].strip().split('\\n') header = lines[0].split(',') rows = [dict(zip(header, line.split(','))) for line in lines[1:]] adults = [r for r in rows if int(r['age']) >= 18] avg_age = sum(int(r['age']) for r in adults) / len(adults) return {'total': len(rows), 'adults': len(adults), 'avg_age': avg_age}`, variables: { csv: 'name,age\nAlice,25\nBob,17\nCharlie,30\nDiana,15' }, expect: { success: true, codeReturn: { total: 4, adults: 2, avg_age: 27.5 } } }, { name: '递归:斐波那契', code: `def main(): def fib(n): if n <= 1: return n return fib(n-1) + fib(n-2) return {'fib10': fib(10), 'fib20': fib(20)}`, expect: { success: true, codeReturn: { fib10: 55, fib20: 6765 } } }, { name: '类定义和使用', code: `def main(): class Point: def __init__(self, x, y): self.x = x self.y = y def distance(self, other): return ((self.x - other.x)**2 + (self.y - other.y)**2)**0.5 p1 = Point(0, 0) p2 = Point(3, 4) return {'distance': p1.distance(p2)}`, expect: { success: true, codeReturn: { distance: 5.0 } } } ] ); }); });