// Sub2API Performance Test Utilities // 性能测试工具函数 import http from 'k6/http'; import { Rate, Trend, Counter, Gauge } from 'k6/metrics'; import { config, getBaseUrl, getAdminCredentials } from '../config.js'; // 重新导出配置函数供其他模块使用 export { getBaseUrl, getAdminCredentials }; // ============= 自定义指标 ============= export const errorRate = new Rate('errors'); export const responseTimeTrend = new Trend('response_time'); export const throughputCounter = new Counter('requests_total'); // 按接口分类的指标 export const endpointMetrics = { healthCheck: new Trend('health_check_duration'), auth: new Trend('auth_duration'), apiKeys: new Trend('api_keys_duration'), gateway: new Trend('gateway_duration'), admin: new Trend('admin_duration'), }; // ============= 认证相关 ============= let authTokenCache = null; let tokenExpiry = 0; /** * 获取认证令牌(带缓存) */ export function getAuthToken() { const now = Date.now(); if (authTokenCache && now < tokenExpiry) { return authTokenCache; } const credentials = getAdminCredentials(); const loginRes = http.post( `${getBaseUrl()}/api/v1/auth/login`, JSON.stringify({ email: credentials.email, password: credentials.password, }), { headers: { 'Content-Type': 'application/json' }, tags: { name: 'auth_login' }, } ); if (loginRes.status !== 200) { throw new Error(`Login failed: ${loginRes.status} ${loginRes.body}`); } const token = loginRes.json('token'); authTokenCache = token; tokenExpiry = now + (55 * 60 * 1000); // 55分钟后过期 return token; } /** * 获取带认证的请求头 */ export function getAuthHeaders() { return { 'Content-Type': 'application/json', 'Authorization': `Bearer ${getAuthToken()}`, }; } /** * 清除认证缓存(用于测试认证失效场景) */ export function clearAuthCache() { authTokenCache = null; tokenExpiry = 0; } // ============= 请求辅助函数 ============= /** * 通用 HTTP GET 请求 */ export function httpGet(url, params = {}) { const start = Date.now(); const defaultParams = { headers: getAuthHeaders(), tags: { name: 'http_get' }, }; const mergedParams = { ...defaultParams, ...params }; const res = http.get(url, mergedParams); const duration = Date.now() - start; recordMetrics(res, duration, params.tags?.name || 'http_get'); return res; } /** * 通用 HTTP POST 请求 */ export function httpPost(url, body, params = {}) { const start = Date.now(); const defaultParams = { headers: getAuthHeaders(), tags: { name: 'http_post' }, }; const mergedParams = { ...defaultParams, ...params }; const res = http.post(url, JSON.stringify(body), mergedParams); const duration = Date.now() - start; recordMetrics(res, duration, params.tags?.name || 'http_post'); return res; } /** * 记录请求指标 */ function recordMetrics(response, duration, endpointName) { // 记录响应时间趋势 responseTimeTrend.add(duration); throughputCounter.add(1); // 记录端点特定指标 if (endpointMetrics[endpointName]) { endpointMetrics[endpointName].add(duration); } // 记录错误率(非 2xx 状态码) const isSuccess = response.status >= 200 && response.status < 300; errorRate.add(!isSuccess); // Verbose 模式打印详情 if (config.mode.verbose || config.mode.recordDetailedRequests) { console.log({ timestamp: new Date().toISOString(), endpoint: endpointName, status: response.status, duration: `${duration}ms`, url: response.url, }); } } // ============= 检查响应 ============= /** * 检查响应是否成功 */ export function checkResponse(response, checks = {}) { const results = { status: response.status >= 200 && response.status < 300, }; if (checks.jsonPath) { try { const value = response.json(checks.jsonPath); results.jsonParsed = true; if (checks.expectedValue !== undefined) { results.valueMatch = value === checks.expectedValue; } } catch (e) { results.jsonParsed = false; } } if (checks.maxDuration) { results.timingOk = response.timings.duration <= checks.maxDuration; } return results; } // ============= 睡眠/等待 ============= /** * 随机睡眠(模拟用户思考时间) */ export function randomSleep(minSec = 0.1, maxSec = 1) { const delay = minSec + Math.random() * (maxSec - minSec); sleep(delay); } /** * 睡眠指定秒数 */ export function sleepSec(seconds) { sleep(seconds); } // ============= 数据生成 ============= /** * 生成随机字符串 */ export function randomString(length = 10) { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; let result = ''; for (let i = 0; i < length; i++) { result += chars.charAt(Math.floor(Math.random() * chars.length)); } return result; } /** * 生成测试用 API Key */ export function generateTestAPIKeyName() { return `perf-test-${Date.now()}-${randomString(8)}`; } /** * 从数组中随机选择一个元素 */ export function randomChoice(array) { return array[Math.floor(Math.random() * array.length)]; } // ============= 批量操作 ============= /** * 并发执行多个请求 */ export async function batchRequests(requests) { return http.batch(requests); } /** * 创建批量请求列表 */ export function createBatchRequest(urls, params = {}) { return urls.map((url) => [ http.get, url, { headers: getAuthHeaders(), ...params, }, ]); } // ============= 报告辅助 ============= /** * 生成测试报告摘要 */ export function generateReport(data) { return { timestamp: new Date().toISOString(), duration: data.duration, totalRequests: data.metrics?.requests_total?.values?.count || 0, failedRequests: data.metrics?.errors?.values?.passes || 0, passRate: ((data.metrics?.requests_total?.values?.count - data.metrics?.errors?.values?.passes) / data.metrics?.requests_total?.values?.count * 100).toFixed(2) + '%', avgResponseTime: (data.metrics?.response_time?.values?.avg || 0).toFixed(2) + 'ms', p95ResponseTime: (data.metrics?.response_time?.values?.['p(95)'] || 0).toFixed(2) + 'ms', p99ResponseTime: (data.metrics?.response_time?.values?.['p(99)'] || 0).toFixed(2) + 'ms', maxResponseTime: (data.metrics?.response_time?.values?.max || 0).toFixed(2) + 'ms', rps: (data.metrics?.requests_total?.values?.rate || 0).toFixed(2), }; } // ============= 错误处理 ============= /** * 重试失败的请求 */ export function retryRequest(method, url, body, options = {}, maxRetries = 3) { const { retryDelay = 1, ...restOptions } = options; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { let res; if (method === 'GET') { res = http.get(url, restOptions); } else if (method === 'POST') { res = http.post(url, body, restOptions); } else { throw new Error(`Unsupported method: ${method}`); } if (res.status >= 200 && res.status < 300) { return res; } if (attempt < maxRetries && res.status >= 500) { sleep(retryDelay * (attempt + 1)); continue; } return res; } catch (e) { if (attempt === maxRetries) { throw e; } sleep(retryDelay * (attempt + 1)); } } }