- 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
304 lines
7.3 KiB
JavaScript
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));
|
|
}
|
|
}
|
|
}
|