|
|
|
|
@ -0,0 +1,816 @@
|
|
|
|
|
/**
|
|
|
|
|
* 15分钟自动化采集测试(含6个异常阶段+数据对比验证)
|
|
|
|
|
*
|
|
|
|
|
* 测试流程:
|
|
|
|
|
* 阶段0: 初始化 — 清空旧数据、启动模拟器、启动采集服务
|
|
|
|
|
* 阶段1: 正常采集(5分钟)— 建立基准数据
|
|
|
|
|
* 阶段2: HTTP 500异常(2分钟)— 模拟服务端错误
|
|
|
|
|
* 阶段3: 超时异常(2分钟)— 模拟响应超时
|
|
|
|
|
* 阶段4: 空数据返回(2分钟)— 模拟空响应
|
|
|
|
|
* 阶段5: 拒绝连接(2分钟)— 模拟网络断开
|
|
|
|
|
* 阶段6: 恢复正常采集(2分钟)— 验证恢复能力
|
|
|
|
|
* 验证: 数据对比 — 模拟器汇总 vs 数据库记录
|
|
|
|
|
* 报告: 输出详细测试报告到 docs/test-reports/
|
|
|
|
|
*
|
|
|
|
|
* 前置条件:
|
|
|
|
|
* 1. MariaDB 运行中(cnc_business + cnc_log 库已初始化)
|
|
|
|
|
* 2. CncSimulator 未启动(脚本自动启动)
|
|
|
|
|
* 3. CncCollector 未启动(脚本自动启动)
|
|
|
|
|
*
|
|
|
|
|
* 安装: npm install @playwright/test mysql2
|
|
|
|
|
* 运行: npx playwright test collector-15min.spec.ts --reporter=list --timeout=0
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { test, expect, request } from '@playwright/test';
|
|
|
|
|
import mysql from 'mysql2/promise';
|
|
|
|
|
import { execSync, spawn, ChildProcess } from 'child_process';
|
|
|
|
|
import * as fs from 'fs';
|
|
|
|
|
import * as path from 'path';
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// 常量与配置
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
const API_BASE = 'http://localhost:5800';
|
|
|
|
|
const SIM_GATEWAY = 'http://localhost:9001';
|
|
|
|
|
const API_KEY = 'collector_api_key_2026';
|
|
|
|
|
const HEADERS = { 'X-API-Key': API_KEY, 'Content-Type': 'application/json' };
|
|
|
|
|
|
|
|
|
|
const DB_BUSINESS = {
|
|
|
|
|
host: 'localhost', user: 'root', password: 'root',
|
|
|
|
|
database: 'cnc_business', charset: 'utf8mb4'
|
|
|
|
|
};
|
|
|
|
|
const DB_LOG = {
|
|
|
|
|
host: 'localhost', user: 'root', password: 'root',
|
|
|
|
|
database: 'cnc_log', charset: 'utf8mb4'
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** 每个阶段的持续时间(毫秒) */
|
|
|
|
|
const PHASE_DURATIONS = {
|
|
|
|
|
normal1: 5 * 60 * 1000, // 阶段1: 正常采集5分钟
|
|
|
|
|
http500: 2 * 60 * 1000, // 阶段2: HTTP 500异常2分钟
|
|
|
|
|
timeout: 2 * 60 * 1000, // 阶段3: 超时异常2分钟
|
|
|
|
|
empty: 2 * 60 * 1000, // 阶段4: 空数据2分钟
|
|
|
|
|
refuse: 2 * 60 * 1000, // 阶段5: 拒绝连接2分钟
|
|
|
|
|
normal2: 2 * 60 * 1000, // 阶段6: 恢复正常2分钟
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** 测试报告输出路径 */
|
|
|
|
|
const REPORT_DIR = path.resolve(__dirname, '../../../docs/test-reports');
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// 数据库辅助函数
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
async function queryBusiness(sql: string, params?: any[]) {
|
|
|
|
|
const conn = await mysql.createConnection(DB_BUSINESS);
|
|
|
|
|
try {
|
|
|
|
|
const [rows] = await conn.query(sql, params);
|
|
|
|
|
return rows as any[];
|
|
|
|
|
} finally {
|
|
|
|
|
await conn.end();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function queryLog(sql: string, params?: any[]) {
|
|
|
|
|
const conn = await mysql.createConnection(DB_LOG);
|
|
|
|
|
try {
|
|
|
|
|
const [rows] = await conn.query(sql, params);
|
|
|
|
|
return rows as any[];
|
|
|
|
|
} finally {
|
|
|
|
|
await conn.end();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function executeBusiness(sql: string, params?: any[]) {
|
|
|
|
|
const conn = await mysql.createConnection(DB_BUSINESS);
|
|
|
|
|
try {
|
|
|
|
|
await conn.execute(sql, params);
|
|
|
|
|
} finally {
|
|
|
|
|
await conn.end();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function executeLog(sql: string, params?: any[]) {
|
|
|
|
|
const conn = await mysql.createConnection(DB_LOG);
|
|
|
|
|
try {
|
|
|
|
|
await conn.execute(sql, params);
|
|
|
|
|
} finally {
|
|
|
|
|
await conn.end();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 辅助:调用采集服务管理API */
|
|
|
|
|
async function collectorApi(method: string, path: string) {
|
|
|
|
|
const ctx = await request.newContext();
|
|
|
|
|
const opts: any = { headers: HEADERS };
|
|
|
|
|
if (method === 'GET') return ctx.get(`${API_BASE}${path}`, opts);
|
|
|
|
|
return ctx.post(`${API_BASE}${path}`, opts);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 辅助:等待指定毫秒 */
|
|
|
|
|
const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));
|
|
|
|
|
|
|
|
|
|
/** 辅助:调用模拟器管理API */
|
|
|
|
|
async function simApi(port: number, path: string, body?: any) {
|
|
|
|
|
const ctx = await request.newContext();
|
|
|
|
|
if (body) {
|
|
|
|
|
return ctx.post(`http://localhost:${port}${path}`, { data: body, headers: { 'Content-Type': 'application/json' } });
|
|
|
|
|
}
|
|
|
|
|
return ctx.get(`http://localhost:${port}${path}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 辅助:调用模拟器网关API */
|
|
|
|
|
async function gatewayApi(path: string, body?: any) {
|
|
|
|
|
const ctx = await request.newContext();
|
|
|
|
|
if (body) {
|
|
|
|
|
return ctx.post(`${SIM_GATEWAY}${path}`, { data: body, headers: { 'Content-Type': 'application/json' } });
|
|
|
|
|
}
|
|
|
|
|
return ctx.get(`${SIM_GATEWAY}${path}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 日志输出带时间戳 */
|
|
|
|
|
function log(msg: string) {
|
|
|
|
|
console.log(`[${new Date().toLocaleTimeString('zh-CN')}] ${msg}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// 全局变量
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
let simulationPort = 0;
|
|
|
|
|
let testStartTime: Date;
|
|
|
|
|
let phaseTimestamps: Record<string, Date> = {};
|
|
|
|
|
|
|
|
|
|
/** 每阶段结束时采集模拟器快照 */
|
|
|
|
|
let phaseSnapshots: Record<string, any> = {};
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// 全局串行配置
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
test.describe.configure({ mode: 'serial' });
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// 阶段0: 初始化
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
test.describe('15分钟自动化采集测试', () => {
|
|
|
|
|
|
|
|
|
|
test('阶段0: 初始化 — 清空数据并启动服务', async () => {
|
|
|
|
|
test.setTimeout(120000); // 2分钟超时
|
|
|
|
|
testStartTime = new Date();
|
|
|
|
|
log('\n===== 15分钟自动化采集测试开始 =====');
|
|
|
|
|
log(`测试开始时间: ${testStartTime.toLocaleString('zh-CN')}`);
|
|
|
|
|
|
|
|
|
|
// 1. 清空采集数据
|
|
|
|
|
log('[0/6] 清空旧采集数据...');
|
|
|
|
|
await executeBusiness('TRUNCATE TABLE cnc_collect_record');
|
|
|
|
|
await executeBusiness('TRUNCATE TABLE cnc_production_segment');
|
|
|
|
|
await executeLog('TRUNCATE TABLE log_collect_raw');
|
|
|
|
|
log(' ✓ 已清空 cnc_collect_record, cnc_production_segment, log_collect_raw');
|
|
|
|
|
|
|
|
|
|
// 2. 确认采集地址配置
|
|
|
|
|
log('[1/6] 确认采集地址配置...');
|
|
|
|
|
const addrs = await queryBusiness('SELECT id, url, is_enabled, collect_interval FROM cnc_collect_address WHERE id = 1');
|
|
|
|
|
expect(addrs.length).toBe(1);
|
|
|
|
|
log(` ✓ 采集地址: id=${addrs[0].id}, url=${addrs[0].url}, interval=${addrs[0].collect_interval}s`);
|
|
|
|
|
|
|
|
|
|
// 3. 确认机床数据
|
|
|
|
|
log('[2/6] 确认机床数据...');
|
|
|
|
|
const machines = await queryBusiness(
|
|
|
|
|
'SELECT id, device_code, name, collect_address_id, is_enabled FROM cnc_machine WHERE collect_address_id = 1 AND is_enabled = 1'
|
|
|
|
|
);
|
|
|
|
|
expect(machines.length).toBeGreaterThanOrEqual(10);
|
|
|
|
|
log(` ✓ 机床数量: ${machines.length}台(采集地址1下的启用机床)`);
|
|
|
|
|
|
|
|
|
|
// 4. 通过模拟器网关启动地址模拟
|
|
|
|
|
log('[3/6] 启动模拟器...');
|
|
|
|
|
const startResp = await gatewayApi('/admin/api/start-address', {
|
|
|
|
|
dbAddressId: 1,
|
|
|
|
|
interval: 10,
|
|
|
|
|
deviceCodes: machines.map((m: any) => m.device_code)
|
|
|
|
|
});
|
|
|
|
|
expect(startResp.ok()).toBeTruthy();
|
|
|
|
|
const startResult = await startResp.json() as any;
|
|
|
|
|
simulationPort = startResult.port || 9001;
|
|
|
|
|
log(` ✓ 模拟器已启动 → 端口 ${simulationPort}`);
|
|
|
|
|
|
|
|
|
|
// 等待模拟器数据就绪
|
|
|
|
|
await sleep(3000);
|
|
|
|
|
const dataResp = await gatewayApi(`/admin/api/status`);
|
|
|
|
|
if (dataResp.ok()) {
|
|
|
|
|
const status = await dataResp.json() as any;
|
|
|
|
|
log(` ✓ 模拟器状态: ${status.totalDevices}台设备, 在线${status.onlineDevices}台`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 5. 更新采集地址URL指向模拟端口
|
|
|
|
|
await executeBusiness(
|
|
|
|
|
'UPDATE cnc_collect_address SET url = ?, is_enabled = 1 WHERE id = 1',
|
|
|
|
|
[`http://localhost:${simulationPort}/`]
|
|
|
|
|
);
|
|
|
|
|
log(` ✓ 更新采集地址URL → http://localhost:${simulationPort}/`);
|
|
|
|
|
|
|
|
|
|
// 6. 启动采集服务
|
|
|
|
|
log('[4/6] 启动采集服务...');
|
|
|
|
|
await collectorApi('POST', '/api/collector/stop');
|
|
|
|
|
await sleep(1000);
|
|
|
|
|
await collectorApi('POST', '/api/collector/refresh');
|
|
|
|
|
await sleep(500);
|
|
|
|
|
await collectorApi('POST', '/api/collector/start');
|
|
|
|
|
log(' ✓ 采集服务已启动');
|
|
|
|
|
|
|
|
|
|
// 7. 等待2个采集周期确认数据流
|
|
|
|
|
log('[5/6] 等待数据采集确认...');
|
|
|
|
|
await sleep(25000);
|
|
|
|
|
|
|
|
|
|
// 验证已有数据产生
|
|
|
|
|
const records = await queryBusiness(
|
|
|
|
|
'SELECT COUNT(*) as cnt FROM cnc_collect_record WHERE collect_time > ?',
|
|
|
|
|
[testStartTime]
|
|
|
|
|
);
|
|
|
|
|
const rawLogs = await queryLog(
|
|
|
|
|
'SELECT COUNT(*) as cnt FROM log_collect_raw WHERE request_time > ?',
|
|
|
|
|
[testStartTime]
|
|
|
|
|
);
|
|
|
|
|
log(` ✓ 采集记录数: ${records[0].cnt}, 原始日志数: ${rawLogs[0].cnt}`);
|
|
|
|
|
expect(records[0].cnt).toBeGreaterThan(0);
|
|
|
|
|
|
|
|
|
|
// 8. 保存阶段0快照
|
|
|
|
|
phaseTimestamps['init'] = new Date();
|
|
|
|
|
const snapshot0 = await gatewayApi(`/admin/api/full-summary`);
|
|
|
|
|
if (snapshot0.ok()) phaseSnapshots['init'] = await snapshot0.json();
|
|
|
|
|
|
|
|
|
|
log('[6/6] 初始化完成,开始15分钟采集测试\n');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// 阶段1: 正常采集(5分钟)
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
test('阶段1: 正常采集 — 5分钟建立基准数据', async () => {
|
|
|
|
|
test.setTimeout(PHASE_DURATIONS.normal1 + 60000);
|
|
|
|
|
log('━━━ 阶段1: 正常采集(5分钟) ━━━');
|
|
|
|
|
phaseTimestamps['normal1_start'] = new Date();
|
|
|
|
|
|
|
|
|
|
// 记录开始时的模拟器状态
|
|
|
|
|
const statsBefore = await gatewayApi(`/admin/api/stats`);
|
|
|
|
|
const statsBeforeData = statsBefore.ok() ? await statsBefore.json() : {};
|
|
|
|
|
|
|
|
|
|
log(` 开始: 总零件=${statsBeforeData.totalParts || '?'}, 在线设备=${statsBeforeData.onlineDevices || '?'}`);
|
|
|
|
|
log(` 等待 ${PHASE_DURATIONS.normal1 / 1000} 秒...`);
|
|
|
|
|
|
|
|
|
|
// 等待5分钟
|
|
|
|
|
await sleep(PHASE_DURATIONS.normal1);
|
|
|
|
|
|
|
|
|
|
// 记录结束时的状态
|
|
|
|
|
phaseTimestamps['normal1_end'] = new Date();
|
|
|
|
|
const statsAfter = await gatewayApi(`/admin/api/stats`);
|
|
|
|
|
const statsAfterData = statsAfter.ok() ? await statsAfter.json() : {};
|
|
|
|
|
|
|
|
|
|
// 保存快照
|
|
|
|
|
const snapshot = await gatewayApi(`/admin/api/full-summary`);
|
|
|
|
|
if (snapshot.ok()) phaseSnapshots['normal1'] = await snapshot.json();
|
|
|
|
|
|
|
|
|
|
log(` 结束: 总零件=${statsAfterData.totalParts || '?'}, 在线设备=${statsAfterData.onlineDevices || '?'}`);
|
|
|
|
|
log(` ✓ 阶段1完成\n`);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// 阶段2: HTTP 500异常(2分钟)
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
test('阶段2: HTTP 500异常 — 2分钟', async () => {
|
|
|
|
|
test.setTimeout(PHASE_DURATIONS.http500 + 60000);
|
|
|
|
|
log('━━━ 阶段2: HTTP 500异常(2分钟) ━━━');
|
|
|
|
|
phaseTimestamps['http500_start'] = new Date();
|
|
|
|
|
|
|
|
|
|
// 设置网络异常: HTTP 500
|
|
|
|
|
const setResp = await gatewayApi(`/admin/api/network`, { type: 'http500' });
|
|
|
|
|
if (setResp.ok()) {
|
|
|
|
|
log(' ✓ 已设置网络异常: HTTP 500');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log(` 等待 ${PHASE_DURATIONS.http500 / 1000} 秒...`);
|
|
|
|
|
await sleep(PHASE_DURATIONS.http500);
|
|
|
|
|
|
|
|
|
|
phaseTimestamps['http500_end'] = new Date();
|
|
|
|
|
|
|
|
|
|
// 保存快照
|
|
|
|
|
const snapshot = await gatewayApi(`/admin/api/full-summary`);
|
|
|
|
|
if (snapshot.ok()) phaseSnapshots['http500'] = await snapshot.json();
|
|
|
|
|
|
|
|
|
|
// 恢复网络
|
|
|
|
|
await gatewayApi(`/admin/api/network`, { type: 'normal' });
|
|
|
|
|
log(' ✓ 已恢复正常网络');
|
|
|
|
|
log(` ✓ 阶段2完成\n`);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// 阶段3: 超时异常(2分钟)
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
test('阶段3: 超时异常 — 2分钟', async () => {
|
|
|
|
|
test.setTimeout(PHASE_DURATIONS.timeout + 60000);
|
|
|
|
|
log('━━━ 阶段3: 超时异常(2分钟) ━━━');
|
|
|
|
|
phaseTimestamps['timeout_start'] = new Date();
|
|
|
|
|
|
|
|
|
|
// 设置网络异常: 超时
|
|
|
|
|
const setResp = await gatewayApi(`/admin/api/network`, { type: 'timeout' });
|
|
|
|
|
if (setResp.ok()) {
|
|
|
|
|
log(' ✓ 已设置网络异常: 超时(60秒延迟)');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log(` 等待 ${PHASE_DURATIONS.timeout / 1000} 秒...`);
|
|
|
|
|
await sleep(PHASE_DURATIONS.timeout);
|
|
|
|
|
|
|
|
|
|
phaseTimestamps['timeout_end'] = new Date();
|
|
|
|
|
|
|
|
|
|
// 保存快照
|
|
|
|
|
const snapshot = await gatewayApi(`/admin/api/full-summary`);
|
|
|
|
|
if (snapshot.ok()) phaseSnapshots['timeout'] = await snapshot.json();
|
|
|
|
|
|
|
|
|
|
// 恢复网络
|
|
|
|
|
await gatewayApi(`/admin/api/network`, { type: 'normal' });
|
|
|
|
|
await sleep(2000); // 等待恢复
|
|
|
|
|
log(' ✓ 已恢复正常网络');
|
|
|
|
|
log(` ✓ 阶段3完成\n`);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// 阶段4: 空数据返回(2分钟)
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
test('阶段4: 空数据返回 — 2分钟', async () => {
|
|
|
|
|
test.setTimeout(PHASE_DURATIONS.empty + 60000);
|
|
|
|
|
log('━━━ 阶段4: 空数据返回(2分钟) ━━━');
|
|
|
|
|
phaseTimestamps['empty_start'] = new Date();
|
|
|
|
|
|
|
|
|
|
// 设置网络异常: 空数据
|
|
|
|
|
const setResp = await gatewayApi(`/admin/api/network`, { type: 'empty' });
|
|
|
|
|
if (setResp.ok()) {
|
|
|
|
|
log(' ✓ 已设置网络异常: 空数据([])');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log(` 等待 ${PHASE_DURATIONS.empty / 1000} 秒...`);
|
|
|
|
|
await sleep(PHASE_DURATIONS.empty);
|
|
|
|
|
|
|
|
|
|
phaseTimestamps['empty_end'] = new Date();
|
|
|
|
|
|
|
|
|
|
// 保存快照
|
|
|
|
|
const snapshot = await gatewayApi(`/admin/api/full-summary`);
|
|
|
|
|
if (snapshot.ok()) phaseSnapshots['empty'] = await snapshot.json();
|
|
|
|
|
|
|
|
|
|
// 恢复网络
|
|
|
|
|
await gatewayApi(`/admin/api/network`, { type: 'normal' });
|
|
|
|
|
await sleep(2000);
|
|
|
|
|
log(' ✓ 已恢复正常网络');
|
|
|
|
|
log(` ✓ 阶段4完成\n`);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// 阶段5: 拒绝连接(2分钟)
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
test('阶段5: 拒绝连接 — 2分钟', async () => {
|
|
|
|
|
test.setTimeout(PHASE_DURATIONS.refuse + 60000);
|
|
|
|
|
log('━━━ 阶段5: 拒绝连接(2分钟) ━━━');
|
|
|
|
|
phaseTimestamps['refuse_start'] = new Date();
|
|
|
|
|
|
|
|
|
|
// 设置网络异常: 拒绝连接
|
|
|
|
|
const setResp = await gatewayApi(`/admin/api/network`, { type: 'refuse' });
|
|
|
|
|
if (setResp.ok()) {
|
|
|
|
|
log(' ✓ 已设置网络异常: 拒绝连接');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log(` 等待 ${PHASE_DURATIONS.refuse / 1000} 秒...`);
|
|
|
|
|
await sleep(PHASE_DURATIONS.refuse);
|
|
|
|
|
|
|
|
|
|
phaseTimestamps['refuse_end'] = new Date();
|
|
|
|
|
|
|
|
|
|
// 保存快照(此时可能无法访问,用之前的数据)
|
|
|
|
|
try {
|
|
|
|
|
const snapshot = await gatewayApi(`/admin/api/full-summary`);
|
|
|
|
|
if (snapshot.ok()) phaseSnapshots['refuse'] = await snapshot.json();
|
|
|
|
|
} catch {
|
|
|
|
|
log(' (拒绝连接期间无法获取模拟器快照,跳过)');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 恢复网络
|
|
|
|
|
await gatewayApi(`/admin/api/network`, { type: 'normal' });
|
|
|
|
|
await sleep(3000); // 等待HttpListener重启
|
|
|
|
|
log(' ✓ 已恢复正常网络');
|
|
|
|
|
log(` ✓ 阶段5完成\n`);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// 阶段6: 恢复正常采集(2分钟)
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
test('阶段6: 恢复正常采集 — 2分钟', async () => {
|
|
|
|
|
test.setTimeout(PHASE_DURATIONS.normal2 + 60000);
|
|
|
|
|
log('━━━ 阶段6: 恢复正常采集(2分钟) ━━━');
|
|
|
|
|
phaseTimestamps['normal2_start'] = new Date();
|
|
|
|
|
|
|
|
|
|
// 确认网络已恢复
|
|
|
|
|
await gatewayApi(`/admin/api/network`, { type: 'normal' });
|
|
|
|
|
await sleep(2000);
|
|
|
|
|
|
|
|
|
|
// 等待新数据产生
|
|
|
|
|
const dataCheck = await gatewayApi(`/admin/api/status`);
|
|
|
|
|
if (dataCheck.ok()) {
|
|
|
|
|
const status = await dataCheck.json() as any;
|
|
|
|
|
log(` 当前状态: 在线${status.onlineDevices}台, 运行${status.isRunning}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log(` 等待 ${PHASE_DURATIONS.normal2 / 1000} 秒...`);
|
|
|
|
|
await sleep(PHASE_DURATIONS.normal2);
|
|
|
|
|
|
|
|
|
|
phaseTimestamps['normal2_end'] = new Date();
|
|
|
|
|
|
|
|
|
|
// 保存最终快照
|
|
|
|
|
const snapshot = await gatewayApi(`/admin/api/full-summary`);
|
|
|
|
|
if (snapshot.ok()) phaseSnapshots['normal2'] = await snapshot.json();
|
|
|
|
|
|
|
|
|
|
// 停止采集服务
|
|
|
|
|
await collectorApi('POST', '/api/collector/stop');
|
|
|
|
|
log(' ✓ 采集服务已停止(触发service_stop结账)');
|
|
|
|
|
await sleep(3000);
|
|
|
|
|
|
|
|
|
|
// 停止模拟器
|
|
|
|
|
await gatewayApi(`/admin/api/stop-address`, { dbAddressId: 1 });
|
|
|
|
|
log(' ✓ 模拟器已停止');
|
|
|
|
|
|
|
|
|
|
log(` ✓ 阶段6完成\n`);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// 数据对比验证
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
test('验证: 数据完整性对比', async () => {
|
|
|
|
|
test.setTimeout(60000);
|
|
|
|
|
log('━━━ 数据完整性对比验证 ━━━');
|
|
|
|
|
|
|
|
|
|
// 1. 获取模拟器最终汇总
|
|
|
|
|
let simSummary: any = phaseSnapshots['normal2'] || {};
|
|
|
|
|
try {
|
|
|
|
|
const resp = await gatewayApi(`/admin/api/full-summary`);
|
|
|
|
|
if (resp.ok()) simSummary = await resp.json();
|
|
|
|
|
} catch { /* 模拟器可能已停止 */ }
|
|
|
|
|
|
|
|
|
|
log('\n [1/5] 模拟器最终汇总:');
|
|
|
|
|
log(` 总设备: ${simSummary.totalDevices || '?'}`);
|
|
|
|
|
log(` 总零件: ${simSummary.totalParts || '?'}`);
|
|
|
|
|
log(` 异常数: ${simSummary.errorCount || 0}`);
|
|
|
|
|
|
|
|
|
|
// 2. 查询数据库统计
|
|
|
|
|
const dbRecords = await queryBusiness(
|
|
|
|
|
'SELECT COUNT(*) as cnt FROM cnc_collect_record WHERE collect_time > ?',
|
|
|
|
|
[testStartTime]
|
|
|
|
|
);
|
|
|
|
|
const dbSegments = await queryBusiness(
|
|
|
|
|
'SELECT COUNT(*) as cnt FROM cnc_production_segment WHERE created_at > ?',
|
|
|
|
|
[testStartTime]
|
|
|
|
|
);
|
|
|
|
|
const dbSettled = await queryBusiness(
|
|
|
|
|
'SELECT COUNT(*) as cnt FROM cnc_production_segment WHERE is_settled = 1 AND created_at > ?',
|
|
|
|
|
[testStartTime]
|
|
|
|
|
);
|
|
|
|
|
const dbActive = await queryBusiness(
|
|
|
|
|
'SELECT COUNT(*) as cnt FROM cnc_production_segment WHERE is_settled = 0 AND created_at > ?',
|
|
|
|
|
[testStartTime]
|
|
|
|
|
);
|
|
|
|
|
const dbRawLogs = await queryLog(
|
|
|
|
|
'SELECT COUNT(*) as cnt FROM log_collect_raw WHERE request_time > ?',
|
|
|
|
|
[testStartTime]
|
|
|
|
|
);
|
|
|
|
|
const dbSuccessLogs = await queryLog(
|
|
|
|
|
'SELECT COUNT(*) as cnt FROM log_collect_raw WHERE is_success = 1 AND request_time > ?',
|
|
|
|
|
[testStartTime]
|
|
|
|
|
);
|
|
|
|
|
const dbFailLogs = await queryLog(
|
|
|
|
|
'SELECT COUNT(*) as cnt FROM log_collect_raw WHERE is_success = 0 AND request_time > ?',
|
|
|
|
|
[testStartTime]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
log('\n [2/5] 数据库统计:');
|
|
|
|
|
log(` 采集记录数: ${dbRecords[0].cnt}`);
|
|
|
|
|
log(` 产量分段数: ${dbSegments[0].cnt} (已结账=${dbSettled[0].cnt}, 未结账=${dbActive[0].cnt})`);
|
|
|
|
|
log(` 原始日志数: ${dbRawLogs[0].cnt} (成功=${dbSuccessLogs[0].cnt}, 失败=${dbFailLogs[0].cnt})`);
|
|
|
|
|
|
|
|
|
|
// 验证数据存在
|
|
|
|
|
expect(dbRecords[0].cnt).toBeGreaterThan(0);
|
|
|
|
|
expect(dbRawLogs[0].cnt).toBeGreaterThan(0);
|
|
|
|
|
log(' ✓ 采集记录和原始日志均已产生');
|
|
|
|
|
|
|
|
|
|
// 3. 按设备对比零件数
|
|
|
|
|
log('\n [3/5] 按设备零件数对比:');
|
|
|
|
|
const simParts = simSummary.devices || [];
|
|
|
|
|
let matchCount = 0;
|
|
|
|
|
let mismatchCount = 0;
|
|
|
|
|
let mismatchDetails: string[] = [];
|
|
|
|
|
|
|
|
|
|
for (const simDev of simParts) {
|
|
|
|
|
const deviceCode = simDev.deviceCode;
|
|
|
|
|
const simTotal = simDev.totalParts || 0;
|
|
|
|
|
|
|
|
|
|
// 查数据库中对应设备的分段总产量
|
|
|
|
|
const machine = await queryBusiness(
|
|
|
|
|
'SELECT id FROM cnc_machine WHERE device_code = ?',
|
|
|
|
|
[deviceCode]
|
|
|
|
|
);
|
|
|
|
|
if (machine.length === 0) {
|
|
|
|
|
log(` ⚠ ${deviceCode}: 数据库中未找到对应机床`);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const segments = await queryBusiness(
|
|
|
|
|
'SELECT program_name, quantity, is_settled, close_reason FROM cnc_production_segment WHERE machine_id = ? AND created_at > ?',
|
|
|
|
|
[machine[0].id, testStartTime]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let dbTotal = 0;
|
|
|
|
|
for (const seg of segments) {
|
|
|
|
|
dbTotal += Number(seg.quantity) || 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const diff = Math.abs(simTotal - dbTotal);
|
|
|
|
|
const tolerance = Math.max(simTotal * 0.1, 5); // 允许10%或5个的误差
|
|
|
|
|
|
|
|
|
|
if (diff <= tolerance) {
|
|
|
|
|
matchCount++;
|
|
|
|
|
if (diff > 0) {
|
|
|
|
|
log(` ≈ ${deviceCode}: 模拟器=${simTotal}, 数据库=${dbTotal} (差${diff}, 在容差内)`);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
mismatchCount++;
|
|
|
|
|
const detail = ` ✗ ${deviceCode}: 模拟器=${simTotal}, 数据库=${dbTotal} (差${diff})`;
|
|
|
|
|
mismatchDetails.push(detail);
|
|
|
|
|
log(detail);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log(`\n 匹配: ${matchCount}台, 不匹配: ${mismatchCount}台`);
|
|
|
|
|
if (mismatchCount === 0) {
|
|
|
|
|
log(' ✓ 所有设备零件数完全匹配!');
|
|
|
|
|
} else {
|
|
|
|
|
log(` ⚠ 有 ${mismatchCount} 台设备零件数不匹配,详见上方`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4. 验证分段结账正确性
|
|
|
|
|
log('\n [4/5] 分段结账验证:');
|
|
|
|
|
const closeReasons = await queryBusiness(
|
|
|
|
|
`SELECT close_reason, COUNT(*) as cnt FROM cnc_production_segment
|
|
|
|
|
WHERE created_at > ? GROUP BY close_reason`,
|
|
|
|
|
[testStartTime]
|
|
|
|
|
);
|
|
|
|
|
for (const row of closeReasons) {
|
|
|
|
|
log(` ${row.close_reason || 'NULL'}: ${row.cnt}条`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 验证所有分段已结账(service_stop触发)
|
|
|
|
|
const unsettled = await queryBusiness(
|
|
|
|
|
'SELECT machine_id, program_name FROM cnc_production_segment WHERE is_settled = 0 AND created_at > ?',
|
|
|
|
|
[testStartTime]
|
|
|
|
|
);
|
|
|
|
|
if (unsettled.length === 0) {
|
|
|
|
|
log(' ✓ 所有分段已结账');
|
|
|
|
|
} else {
|
|
|
|
|
log(` ⚠ 有 ${unsettled.length} 条分段未结账`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 5. 验证异常期间的数据
|
|
|
|
|
log('\n [5/5] 异常期间数据验证:');
|
|
|
|
|
|
|
|
|
|
// HTTP 500期间应该有失败日志
|
|
|
|
|
const http500Logs = await queryLog(
|
|
|
|
|
`SELECT COUNT(*) as cnt FROM log_collect_raw
|
|
|
|
|
WHERE is_success = 0 AND error_message LIKE '%500%'
|
|
|
|
|
AND request_time BETWEEN ? AND ?`,
|
|
|
|
|
[phaseTimestamps['http500_start'], phaseTimestamps['http500_end']]
|
|
|
|
|
);
|
|
|
|
|
log(` HTTP 500期间失败日志: ${http500Logs[0]?.cnt || 0}条`);
|
|
|
|
|
|
|
|
|
|
// 拒绝连接期间应该有失败日志
|
|
|
|
|
const refuseLogs = await queryLog(
|
|
|
|
|
`SELECT COUNT(*) as cnt FROM log_collect_raw
|
|
|
|
|
WHERE is_success = 0 AND error_message LIKE '%连接%'
|
|
|
|
|
AND request_time BETWEEN ? AND ?`,
|
|
|
|
|
[phaseTimestamps['refuse_start'], phaseTimestamps['refuse_end']]
|
|
|
|
|
);
|
|
|
|
|
log(` 拒绝连接期间失败日志: ${refuseLogs[0]?.cnt || 0}条`);
|
|
|
|
|
|
|
|
|
|
// 恢复后应有新数据
|
|
|
|
|
const recoveryRecords = await queryBusiness(
|
|
|
|
|
`SELECT COUNT(*) as cnt FROM cnc_collect_record
|
|
|
|
|
WHERE collect_time > ?`,
|
|
|
|
|
[phaseTimestamps['normal2_start']]
|
|
|
|
|
);
|
|
|
|
|
log(` 恢复后新增采集记录: ${recoveryRecords[0].cnt}条`);
|
|
|
|
|
expect(recoveryRecords[0].cnt).toBeGreaterThan(0);
|
|
|
|
|
log(' ✓ 恢复后数据正常产生');
|
|
|
|
|
|
|
|
|
|
log('\n ✓ 数据完整性对比完成\n');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// 生成测试报告
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
test('报告: 生成15分钟采集测试报告', async () => {
|
|
|
|
|
test.setTimeout(30000);
|
|
|
|
|
|
|
|
|
|
// 确保报告目录存在
|
|
|
|
|
if (!fs.existsSync(REPORT_DIR)) {
|
|
|
|
|
fs.mkdirSync(REPORT_DIR, { recursive: true });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '').slice(0, 15);
|
|
|
|
|
const reportPath = path.join(REPORT_DIR, `${timestamp}-15分钟采集测试报告.md`);
|
|
|
|
|
|
|
|
|
|
// 收集最终数据
|
|
|
|
|
let simSummary: any = phaseSnapshots['normal2'] || {};
|
|
|
|
|
try {
|
|
|
|
|
const resp = await gatewayApi(`/admin/api/full-summary`);
|
|
|
|
|
if (resp.ok()) simSummary = await resp.json();
|
|
|
|
|
} catch { /* 模拟器可能已停止 */ }
|
|
|
|
|
|
|
|
|
|
const eventHistory = await gatewayApi(`/admin/api/event-history`);
|
|
|
|
|
const eventHistoryData = eventHistory.ok() ? await eventHistory.json() : [];
|
|
|
|
|
|
|
|
|
|
const errorLog = await gatewayApi(`/admin/api/error-log`);
|
|
|
|
|
const errorLogData = errorLog.ok() ? await errorLog.json() : [];
|
|
|
|
|
|
|
|
|
|
const dbRecords = await queryBusiness(
|
|
|
|
|
'SELECT COUNT(*) as cnt FROM cnc_collect_record WHERE collect_time > ?',
|
|
|
|
|
[testStartTime]
|
|
|
|
|
);
|
|
|
|
|
const dbSegments = await queryBusiness(
|
|
|
|
|
'SELECT COUNT(*) as cnt FROM cnc_production_segment WHERE created_at > ?',
|
|
|
|
|
[testStartTime]
|
|
|
|
|
);
|
|
|
|
|
const dbSettled = await queryBusiness(
|
|
|
|
|
'SELECT COUNT(*) as cnt FROM cnc_production_segment WHERE is_settled = 1 AND created_at > ?',
|
|
|
|
|
[testStartTime]
|
|
|
|
|
);
|
|
|
|
|
const dbRawLogs = await queryLog(
|
|
|
|
|
'SELECT COUNT(*) as cnt FROM log_collect_raw WHERE request_time > ?',
|
|
|
|
|
[testStartTime]
|
|
|
|
|
);
|
|
|
|
|
const dbSuccessLogs = await queryLog(
|
|
|
|
|
'SELECT COUNT(*) as cnt FROM log_collect_raw WHERE is_success = 1 AND request_time > ?',
|
|
|
|
|
[testStartTime]
|
|
|
|
|
);
|
|
|
|
|
const dbFailLogs = await queryLog(
|
|
|
|
|
'SELECT COUNT(*) as cnt FROM log_collect_raw WHERE is_success = 0 AND request_time > ?',
|
|
|
|
|
[testStartTime]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 按设备统计
|
|
|
|
|
const machines = await queryBusiness(
|
|
|
|
|
'SELECT id, device_code, name FROM cnc_machine WHERE collect_address_id = 1'
|
|
|
|
|
);
|
|
|
|
|
let deviceStats = '';
|
|
|
|
|
for (const m of machines) {
|
|
|
|
|
const segs = await queryBusiness(
|
|
|
|
|
`SELECT program_name, SUM(quantity) as total_qty, COUNT(*) as seg_count, close_reason
|
|
|
|
|
FROM cnc_production_segment WHERE machine_id = ? AND created_at > ?
|
|
|
|
|
GROUP BY program_name`,
|
|
|
|
|
[m.id, testStartTime]
|
|
|
|
|
);
|
|
|
|
|
const totalQty = segs.reduce((s: number, r: any) => s + (Number(r.total_qty) || 0), 0);
|
|
|
|
|
const programs = segs.map((r: any) => `${r.program_name}(${r.total_qty}个/${r.seg_count}段)`).join(', ');
|
|
|
|
|
deviceStats += `| ${m.device_code} | ${m.name} | ${totalQty} | ${segs.length} | ${programs} |\n`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 阶段时间表
|
|
|
|
|
let phaseTable = '';
|
|
|
|
|
const phaseNames: Record<string, string> = {
|
|
|
|
|
'normal1_start': '阶段1: 正常采集开始',
|
|
|
|
|
'http500_start': '阶段2: HTTP 500异常开始',
|
|
|
|
|
'http500_end': '阶段2: HTTP 500异常结束',
|
|
|
|
|
'timeout_start': '阶段3: 超时异常开始',
|
|
|
|
|
'timeout_end': '阶段3: 超时异常结束',
|
|
|
|
|
'empty_start': '阶段4: 空数据返回开始',
|
|
|
|
|
'empty_end': '阶段4: 空数据返回结束',
|
|
|
|
|
'refuse_start': '阶段5: 拒绝连接开始',
|
|
|
|
|
'refuse_end': '阶段5: 拒绝连接结束',
|
|
|
|
|
'normal2_start': '阶段6: 恢复正常开始',
|
|
|
|
|
'normal2_end': '阶段6: 恢复正常结束',
|
|
|
|
|
};
|
|
|
|
|
for (const [key, name] of Object.entries(phaseNames)) {
|
|
|
|
|
const ts = phaseTimestamps[key];
|
|
|
|
|
if (ts) {
|
|
|
|
|
phaseTable += `| ${name} | ${ts.toLocaleString('zh-CN')} |\n`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 事件历史摘要
|
|
|
|
|
let eventSummary = '';
|
|
|
|
|
if (Array.isArray(eventHistoryData)) {
|
|
|
|
|
const eventTypeCounts: Record<string, number> = {};
|
|
|
|
|
for (const evt of eventHistoryData) {
|
|
|
|
|
const type = evt.eventType || 'unknown';
|
|
|
|
|
eventTypeCounts[type] = (eventTypeCounts[type] || 0) + 1;
|
|
|
|
|
}
|
|
|
|
|
for (const [type, count] of Object.entries(eventTypeCounts)) {
|
|
|
|
|
eventSummary += `| ${type} | ${count} |\n`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 异常日志摘要
|
|
|
|
|
let errorSummary = '';
|
|
|
|
|
if (Array.isArray(errorLogData) && errorLogData.length > 0) {
|
|
|
|
|
for (const err of errorLogData.slice(0, 20)) {
|
|
|
|
|
errorSummary += `| ${err.timestamp} | ${err.errorType} | ${err.description} | ${err.affectedDevices} |\n`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const report = `# 15分钟自动化采集测试报告
|
|
|
|
|
|
|
|
|
|
**生成时间:** ${new Date().toLocaleString('zh-CN')}
|
|
|
|
|
**测试开始:** ${testStartTime.toLocaleString('zh-CN')}
|
|
|
|
|
**测试结束:** ${new Date().toLocaleString('zh-CN')}
|
|
|
|
|
**总时长:** ${Math.round((Date.now() - testStartTime.getTime()) / 60000)}分钟
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 一、测试环境
|
|
|
|
|
|
|
|
|
|
| 项目 | 值 |
|
|
|
|
|
|------|------|
|
|
|
|
|
| 模拟器端口 | ${simulationPort} |
|
|
|
|
|
| 采集间隔 | 10秒 |
|
|
|
|
|
| 机床数量 | ${machines.length}台 |
|
|
|
|
|
| 采集地址 | http://localhost:${simulationPort}/ (id=1) |
|
|
|
|
|
|
|
|
|
|
## 二、测试阶段时间表
|
|
|
|
|
|
|
|
|
|
| 阶段 | 时间 |
|
|
|
|
|
|------|------|
|
|
|
|
|
| 测试开始 | ${testStartTime.toLocaleString('zh-CN')} |
|
|
|
|
|
${phaseTable}
|
|
|
|
|
| 测试结束 | ${new Date().toLocaleString('zh-CN')} |
|
|
|
|
|
|
|
|
|
|
## 三、模拟器汇总数据
|
|
|
|
|
|
|
|
|
|
| 项目 | 值 |
|
|
|
|
|
|------|------|
|
|
|
|
|
| 总设备数 | ${simSummary.totalDevices || '?'} |
|
|
|
|
|
| 在线设备数 | ${simSummary.onlineDevices || '?'} |
|
|
|
|
|
| 总加工零件数 | ${simSummary.totalParts || '?'} |
|
|
|
|
|
| 总请求数 | ${simSummary.totalRequests || '?'} |
|
|
|
|
|
| 成功请求数 | ${simSummary.successRequests || '?'} |
|
|
|
|
|
| 失败请求数 | ${simSummary.failRequests || '?'} |
|
|
|
|
|
| 异常记录数 | ${simSummary.errorCount || 0} |
|
|
|
|
|
|
|
|
|
|
## 四、数据库统计
|
|
|
|
|
|
|
|
|
|
| 项目 | 数量 |
|
|
|
|
|
|------|------|
|
|
|
|
|
| 采集记录数(cnc_collect_record) | ${dbRecords[0].cnt} |
|
|
|
|
|
| 产量分段数(cnc_production_segment) | ${dbSegments[0].cnt} |
|
|
|
|
|
| 已结账分段数 | ${dbSettled[0].cnt} |
|
|
|
|
|
| 原始日志数(log_collect_raw) | ${dbRawLogs[0].cnt} |
|
|
|
|
|
| 成功日志数 | ${dbSuccessLogs[0].cnt} |
|
|
|
|
|
| 失败日志数 | ${dbFailLogs[0].cnt} |
|
|
|
|
|
|
|
|
|
|
## 五、按设备产量对比
|
|
|
|
|
|
|
|
|
|
| 设备编码 | 设备名称 | 数据库总产量 | 分段数 | 程序明细 |
|
|
|
|
|
|---------|---------|-------------|--------|---------|
|
|
|
|
|
${deviceStats}
|
|
|
|
|
|
|
|
|
|
## 六、事件历史统计
|
|
|
|
|
|
|
|
|
|
| 事件类型 | 次数 |
|
|
|
|
|
|---------|------|
|
|
|
|
|
${eventSummary || '| (无事件) | 0 |'}
|
|
|
|
|
|
|
|
|
|
## 七、异常日志记录
|
|
|
|
|
|
|
|
|
|
| 时间 | 类型 | 描述 | 影响设备数 |
|
|
|
|
|
|------|------|------|-----------|
|
|
|
|
|
${errorSummary || '| (无异常记录) | - | - | - |'}
|
|
|
|
|
|
|
|
|
|
## 八、结论
|
|
|
|
|
|
|
|
|
|
${mismatchCount === 0 ? '✅ 所有设备零件数量完全匹配,数据采集准确性验证通过。' : `⚠️ 有 ${mismatchCount} 台设备零件数存在差异,需进一步排查。`}
|
|
|
|
|
|
|
|
|
|
- 采集记录总数: ${dbRecords[0].cnt}
|
|
|
|
|
- 产量分段总数: ${dbSegments[0].cnt} (已结账 ${dbSettled[0].cnt})
|
|
|
|
|
- 原始日志总数: ${dbRawLogs[0].cnt} (成功 ${dbSuccessLogs[0].cnt}, 失败 ${dbFailLogs[0].cnt})
|
|
|
|
|
- 异常恢复验证: ${phaseTimestamps['normal2_start'] ? '✅ 恢复后数据正常产生' : '❌ 未验证'}
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
*报告由 Playwright 自动化测试生成*
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
fs.writeFileSync(reportPath, report, 'utf-8');
|
|
|
|
|
log(`\n===== 测试报告已生成 =====`);
|
|
|
|
|
log(` 路径: ${reportPath}`);
|
|
|
|
|
log(`\n===== 15分钟自动化采集测试结束 =====\n`);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
});
|