Files
Developer 349d783fd1 refactor: clean up project structure
- Remove old review reports (keep latest only)
- Move docs/ to deploy/docs-backup/
- Move performance-testing/ to deploy/
- Clean up test output files
- Organize root directory
2026-04-06 23:36:03 +08:00

304 lines
7.3 KiB
JavaScript

// 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));
}
}
}