| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313 |
- #!/usr/bin/env node
- /* ===========================================================================
- * pc-selftest-bridge.js —— 引擎侧「桥 + 心跳 + 配置绑定」的 PC 自检
- *
- * 跑法:cd tiktok-auto && $NODE tools/pc-selftest-bridge.js
- * 期望最后一行:BRIDGE_SELFTEST_PASS
- *
- * 为什么单独一个文件:`tiktok-auto` 的产物是"加载即执行"的(尾部就是
- * `TikTokAuto.main.run(...)`),在 PC 上 require 它会直接跑去开 TikTok。
- * 所以这里跟 `pc-selftest-id.js` 一样,把 **src 源码**拼进一个 vm 沙箱执行,
- * 只挑纯逻辑模块:protocol / bridge / config / wait / logger。
- *
- * ★ 线程桩必须"只登记、不执行":
- * 心跳是个 `while (running) { sleep(...) }` 的死循环,
- * 照抄 `android-stub.js` 里 `start: fn => fn()` 的写法会把 PC 进程挂死。
- * 这里只把函数存起来,心跳的每一拍由用例显式调 `heartbeatOnce()` 驱动。
- * =========================================================================== */
- 'use strict';
- const fs = require('fs');
- const path = require('path');
- const vm = require('vm');
- const SRC = path.join(__dirname, '..', 'src');
- const MODULES = ['logger', 'protocol', 'bridge', 'config', 'wait'];
- const RUN_ID = 'run-自检-1';
- const STATE = '/sdcard/tiktok_app/run-state.json';
- const BEAT = '/sdcard/tiktok_app/engine-beat.json';
- const STOP = '/sdcard/tiktok_app/stop.flag';
- const LOG = '/sdcard/tiktok_app/run-log.jsonl';
- const CONF = '/sdcard/Download/tiktok-auto/config.json';
- let passed = 0;
- const failures = [];
- function check(name, condition, detail) {
- if (condition) {
- passed += 1;
- return;
- }
- failures.push(name + (detail ? ' → ' + detail : ''));
- }
- function eq(name, actual, expected) {
- check(name, actual === expected, `期望 ${expected},实际 ${actual}`);
- }
- /* ------------------------------------------------------------ 沙箱 */
- /**
- * 造一个隔离的沙箱。
- * 每个沙箱有自己的内存文件系统与线程桩 —— 需要验"全新进程"的用例
- * (比如命令行根本没传 --run-id)必须用新沙箱:bridge 一旦 enable,
- * 进程内就没有"取消启用"这回事。
- */
- function buildJail(argv) {
- const mem = {};
- const threadsState = { started: [], shutDown: 0 };
- const jail = {
- mem: mem,
- threads: threadsState,
- throwOnStart: false
- };
- const box = {
- console: { log: () => { } },
- Date: Date, Math: Math, JSON: JSON,
- String: String, Number: Number, Object: Object,
- Array: Array, RegExp: RegExp, Boolean: Boolean,
- isNaN: isNaN, parseInt: parseInt, parseFloat: parseFloat,
- TikTokAuto: {}
- };
- box.files = {
- exists: (p) => Object.prototype.hasOwnProperty.apply(mem, [p]),
- read: (p) => (mem[p] === undefined ? '' : mem[p]),
- write: (p, content) => { mem[p] = String(content); },
- /* bridge 的日志落盘用的是 append,少了它 enable 会静默失败 */
- append: (p, content) => { mem[p] = (mem[p] || '') + String(content); },
- ensureDir: () => true
- };
- /* ★ 只登记不执行 */
- box.threads = {
- start: (fn) => {
- if (jail.throwOnStart) {
- throw new Error('自检:模拟线程起不来');
- }
- threadsState.started.push(fn);
- return { id: 'th-' + threadsState.started.length };
- },
- shutDownAll: () => { threadsState.shutDown += 1; }
- };
- box.sleep = () => { /* 心跳循环不会在自检里真跑,这里只是占位 */ };
- box.engines = { myEngine: () => ({ execArgv: argv }) };
- vm.createContext(box);
- let src = 'var TikTokAuto = this.TikTokAuto;\n';
- for (const name of MODULES) {
- src += fs.readFileSync(path.join(SRC, `${name}.js`), 'utf8') + '\n';
- }
- vm.runInContext(src, box);
- jail.api = box.TikTokAuto;
- return jail;
- }
- const jail = buildJail(['--run-id', RUN_ID, '--state', STATE, '--stop-flag', STOP, '--log', LOG, '--config', CONF]);
- const memFiles = jail.mem;
- const threadsState = jail.threads;
- const T = jail.api;
- const bridge = T.bridge;
- const config = T.config;
- /* logger 用真的,但把行收集起来(bridge.enable 会 setSink 到 files.append) */
- check('沙箱里加载出了 protocol / bridge / config',
- !!T.protocol && !!bridge && !!config);
- const readJson = (p) => {
- try { return JSON.parse(memFiles[p] || ''); } catch (e) { return null; }
- };
- /* ------------------------------------------------------------ 1. 启动握手 */
- const injected = bridge.fromExecArgv(null);
- eq('argv 里的配置路径被交给 config.resolve', injected.runtime.configPath, CONF);
- eq('enable 之后桥是启用的', bridge.isEnabled(), true);
- eq('runId 与命令行一致', bridge.currentRunId(), RUN_ID);
- let state = readJson(STATE);
- check('第一次上报就写出了 run-state', !!state);
- eq('run-state 带协议版本 v=2', state && state.v, 2);
- eq('第一个状态是 starting', state && state.status, 'starting');
- eq('runId 写进了状态', state && state.runId, RUN_ID);
- eq('心跳线程被真的请求了一次', threadsState.started.length, 1);
- check('run-state 里声明了"有心跳"',
- !!(state && state.engine && state.engine.capabilities && state.engine.capabilities.heartbeat === true),
- JSON.stringify(state && state.engine));
- let beat = readJson(BEAT);
- check('enable 时已经同步写了第一拍心跳', !!beat, '没写 beat 文件');
- eq('心跳带协议版本', beat && beat.v, 2);
- eq('心跳 runId 与本轮一致', beat && beat.runId, RUN_ID);
- eq('心跳停止确认初始为 0(还没人点停止)', beat && beat.stopSeenAt, 0);
- check('心跳序号从 1 开始', beat && beat.seq >= 1, String(beat && beat.seq));
- /* ------------------------------------------------------------ 2. 序号单调 */
- bridge.report({ phase: '启动应用' });
- state = readJson(STATE);
- const seq1 = state.seq;
- bridge.report({ status: 'running', phase: '干活' });
- state = readJson(STATE);
- check('report 的 seq 单调递增', state.seq > seq1, `${seq1} → ${state.seq}`);
- eq('report 带着 startedAt', typeof state.startedAt, 'number');
- eq('report 后的状态是 running', state.status, 'running');
- check('report 不清掉 engine.task 之类的已写字段',
- state.engine && typeof state.engine === 'object');
- bridge.setTask('video');
- state = readJson(STATE);
- eq('setTask 把任务名写进 engine.task', state.engine && state.engine.task, 'video');
- /* ------------------------------------------------------------ 3. 心跳 sticky */
- const firstBeatAt = readJson(BEAT).beatAt;
- bridge.heartbeatOnce();
- beat = readJson(BEAT);
- check('再写一拍:beatAt 前进', beat.beatAt >= firstBeatAt);
- check('再写一拍:seq 前进', beat.seq >= 2, String(beat.seq));
- /* 别人的 runId 写在 stop.flag 里 → 不算我的停止请求 */
- memFiles[STOP] = 'run-别的运行';
- bridge.heartbeatOnce();
- eq('别人的停止标志不会被当成自己的', readJson(BEAT).stopSeenAt, 0);
- /* 我自己的 runId → 记下时刻,而且**后续每一拍都要带着** */
- memFiles[STOP] = RUN_ID;
- bridge.heartbeatOnce();
- const seen1 = readJson(BEAT).stopSeenAt;
- check('本轮停止标志被心跳看见', seen1 > 0, String(seen1));
- bridge.heartbeatOnce();
- const seen2 = readJson(BEAT).stopSeenAt;
- eq('停止确认是 sticky 的(不会被下一拍抹掉)', seen2, seen1);
- bridge.heartbeatOnce();
- eq('再跳几拍也还是同一个时刻', readJson(BEAT).stopSeenAt, seen1);
- /* ------------------------------------------------------------ 4. 停止信号 */
- let stopped = false;
- let reportedStopping = false;
- try {
- bridge.checkStop();
- } catch (error) {
- stopped = bridge.isStopSignal(error);
- }
- eq('checkStop 命中时抛 StopSignal', stopped, true);
- state = readJson(STATE);
- reportedStopping = state.status === 'stopping';
- eq('抛之前先把 stopping 上报给宿主', reportedStopping, true);
- /* 空内容的 flag 不该触发(残留文件的兜底) */
- memFiles[STOP] = ' ';
- let quiet = true;
- try { bridge.checkStop(); } catch (e) { quiet = false; }
- eq('stop.flag 只有空白时不触发停止', quiet, true);
- memFiles[STOP] = RUN_ID;
- /* ------------------------------------------------------------ 5. 配置绑定 */
- /* 双方都非空且不等 → 拒绝(挡住迟到的旧引擎读到新配置就开跑) */
- memFiles[CONF] = JSON.stringify({ runtime: { runId: 'run-另一轮' } });
- let mismatch = null;
- try {
- config.resolve({ runtime: { configPath: CONF } });
- } catch (error) {
- mismatch = error;
- }
- check('配置属于另一次运行时被拒绝', !!mismatch && mismatch.name === 'ConfigError',
- mismatch ? String(mismatch.message) : '没有抛错');
- /* 配置里没有 runId(独立运行/老界面)→ 必须放行,否则引擎永远起不来 */
- memFiles[CONF] = JSON.stringify({});
- let standalone = null;
- try {
- config.resolve({ runtime: { configPath: CONF } });
- } catch (error) {
- standalone = error;
- }
- eq('配置里没写 runId 时放行(独立运行)', standalone, null);
- /* 本轮没有 runId(命令行没传,比如直接在 AutoX 里打开产物)→ 同样放行。
- * 这种"进程从没 enable 过"的状态只能在新沙箱里验:enable 一旦发生就无法撤销。 */
- const noArgvJail = buildJail([]);
- noArgvJail.mem[CONF] = JSON.stringify({ runtime: { runId: 'run-另一轮' } });
- let noArgvError = null;
- try {
- noArgvJail.api.config.resolve({ runtime: { configPath: CONF } });
- } catch (error) {
- noArgvError = error;
- }
- eq('引擎自己没 runId 时放行(命令行直跑)', noArgvError, null);
- /* 一致时正常通过 */
- memFiles[CONF] = JSON.stringify({ runtime: { runId: RUN_ID } });
- let okError = null;
- let resolved = null;
- try {
- resolved = config.resolve({ runtime: { configPath: CONF } });
- } catch (error) {
- okError = error;
- }
- eq('runId 一致时正常解析', okError, null);
- eq('解析结果里带着 runId', resolved && resolved.runtime.runId, RUN_ID);
- /* 配置文件本身是坏的 → 用默认值继续(不能让引擎直接不跑) */
- memFiles[CONF] = '{ 半截 JSON';
- let brokenError = null;
- try {
- resolved = config.resolve({ runtime: { configPath: CONF } });
- } catch (error) {
- brokenError = error;
- }
- eq('配置文件坏了也不拦启动(退回默认值)', brokenError, null);
- /* ------------------------------------------------------------ 6. 收尾 */
- bridge.finish('finished', { ok: true, videos: 2 });
- state = readJson(STATE);
- eq('finish 写出终态', state.status, 'finished');
- check('finish 带上了 summary', !!state.summary);
- check('终态里 message 是人话', typeof state.message === 'string' && state.message.length > 0,
- state.message);
- bridge.heartbeatStop();
- eq('heartbeatStop 之后心跳标记为停', bridge.isHeartbeatUp(), false);
- const seqAfterStop = readJson(BEAT).seq;
- eq('停心跳后不会自己再写(线程桩本来也没跑)', readJson(BEAT).seq, seqAfterStop);
- /* 心跳线程起不来 → 明确告诉宿主,让它退回无心跳阈值 */
- jail.throwOnStart = true;
- const up = bridge.heartbeatStart();
- eq('线程起不来时 heartbeatStart 返回 false', up, false);
- eq('起不来时 isHeartbeatUp 是 false', bridge.isHeartbeatUp(), false);
- jail.throwOnStart = false;
- /* 没启用的桥(独立运行):所有上报都是空操作,一个文件都不写 */
- const idleJail = buildJail([]);
- idleJail.api.bridge.report({ status: 'running' });
- idleJail.api.bridge.finish('finished', { ok: true });
- idleJail.api.bridge.heartbeatStart();
- check('未启用的桥不会凭空写 run-state', idleJail.mem[STATE] === undefined);
- check('未启用的桥不会凭空写心跳', idleJail.mem[BEAT] === undefined);
- eq('未启用的桥 heartbeatStart 返回 false',
- idleJail.api.bridge.heartbeatStart(), false);
- /* ------------------------------------------------------------ 汇总 */
- console.log('');
- console.log(`通过 ${passed} 项`);
- if (failures.length > 0) {
- console.log(`失败 ${failures.length} 项:`);
- failures.forEach((f) => console.log(' ✗ ' + f));
- console.log('');
- console.log('BRIDGE_SELFTEST_FAILED');
- process.exit(1);
- }
- console.log('BRIDGE_SELFTEST_PASS');
|