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
This commit is contained in:
303
deploy/performance-testing/common/utils.js
Normal file
303
deploy/performance-testing/common/utils.js
Normal file
@@ -0,0 +1,303 @@
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user