const fs = require('fs');
const path = require('path');
const vm = require('vm');
const { execFileSync } = require('child_process');
const [name, nodeVer, ...rest] = process.argv.slice(2);
const roMapIdx = rest.indexOf('--ro-map');
const roMap = roMapIdx >= 0 ? rest[roMapIdx + 1] : null;
const fixture = path.join('tests/fixtures/behav', `${name}.js`);
const casesAll = JSON.parse(fs.readFileSync(path.join('tests/fixtures/behav/cases.json'), 'utf8'));
const cases = casesAll[name].cases;
const minNode = casesAll[name].min_node || 0;
const major = parseInt(nodeVer.split('.')[0], 10);
if (major < minNode) {
console.log(JSON.stringify({ fixture: name, node: nodeVer, status: 'skip', reason: `min-node ${minNode}` }));
process.exit(0);
}
const src = fs.readFileSync(fixture, 'utf8');
const jsc = `/tmp/behav-${name}-${nodeVer}.jsc`;
const nodeBin = `${process.env.HOME}/.local/share/mise/installs/node/${nodeVer}/bin/node`;
const run = (cmd, args, extraEnv) => execFileSync(cmd, args, {
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
env: extraEnv ? { ...process.env, ...extraEnv } : process.env,
});
const topFnNames = (text) => {
const out = [];
for (const line of text.split('\n')) {
const m = /^(?:async\s+)?function\s*\*?\s*([A-Za-z_$][\w$]*)\s*\(/.exec(line);
if (m) out.push(m[1]);
}
return out;
};
const checkDeclarationsKept = (jscPath, roMapPath) => {
const args0 = ['decompile', jscPath];
if (roMapPath) args0.push('--ro-map', roMapPath);
const raw = run('./target/release/jscd', args0, { JSCD_NO_OPT: '1' });
const opt = run('./target/release/jscd', args0);
const missing = topFnNames(raw).filter((n) => !new RegExp(`function\\s*\\*?\\s*${n}\\s*\\(`).test(opt));
return missing;
};
try {
run(nodeBin, ['scripts/mkcorpus.js', fixture, jsc]);
} catch (e) {
const msg = (e.stderr || String(e)).trim().split('\n')[0];
console.log(JSON.stringify({
fixture: name, node: nodeVer, status: 'compile-fail',
detail: msg, note: '该 node 版本不支持此源码语法',
}));
process.exit(0);
}
const decArgs = ['decompile', jsc, '--runtime'];
if (roMap) decArgs.push('--ro-map', roMap);
let decoded = '';
let decompileErr = null;
try {
decoded = run('./target/release/jscd', decArgs);
} catch (e) {
decompileErr = (e.stderr || String(e)).trim().split('\n')[0];
}
const result = { fixture: name, node: nodeVer, cases: cases.length };
if (decompileErr) {
result.status = 'decompile-failed';
result.detail = decompileErr;
console.log(JSON.stringify(result));
process.exit(0);
}
try {
new vm.Script(decoded);
result.syntax = 'ok';
} catch (e) {
result.syntax = 'error';
result.detail = String(e.message).split('\n')[0];
result.status = 'syntax-error';
console.log(JSON.stringify(result));
process.exit(0);
}
(async () => {
const evalTarget = (code) => {
const ctx = vm.createContext({});
vm.runInContext(code, ctx, { timeout: 4000 });
return ctx.target;
};
let orig, mine;
try {
orig = evalTarget(src);
} catch (e) {
result.status = 'harness-error';
result.detail = 'original eval: ' + e.message;
console.log(JSON.stringify(result));
process.exit(0);
}
try {
mine = evalTarget(decoded);
} catch (e) {
result.status = 'eval-failed';
result.detail = String(e.message).split('\n')[0];
console.log(JSON.stringify(result));
process.exit(0);
}
if (typeof mine !== 'function') {
result.status = 'target-missing';
console.log(JSON.stringify(result));
process.exit(0);
}
const norm = (v) => JSON.stringify(v);
const callTimed = (code, args, label) => {
const ctx = vm.createContext({ __args: args, Promise });
try {
const raw = vm.runInContext(
`${code}\n;(function(){ return target.apply(null, __args); })()`,
ctx,
{ timeout: 3000 }
);
const out = raw && typeof raw.then === 'function' ? { thenable: raw } : { plain: raw };
return { ok: true, value: out };
} catch (e) {
var name = (e && e.constructor && e.constructor.name) || typeof e;
var msg = (e && e.message) || String(e);
return { ok: false, value: name + ': ' + String(msg).split('\n')[0] };
}
};
const settle = async (r) => {
if (!r.ok) return 'ERR ' + r.value;
try {
let raw = r.value;
if ('thenable' in raw) {
raw = await Promise.race([
Promise.resolve(raw.thenable).then((v) => ({ v }), (e) => ({ e })),
new Promise((res) => setTimeout(() => res({ timeout: true }), 2000)),
]);
if (raw && raw.timeout) return 'ERR Timeout';
if (raw && 'e' in raw) {
const e = raw.e;
var nm = (e && e.constructor && e.constructor.name) || typeof e;
return 'ERR ' + nm + ': ' + String((e && e.message) || e).split('\n')[0];
}
raw = raw.v;
}
const v = raw.plain !== undefined ? raw.plain : raw;
return JSON.stringify(v);
} catch (e) {
var name = (e && e.constructor && e.constructor.name) || typeof e;
return 'ERR ' + name + ': ' + String((e && e.message) || e).split('\n')[0];
}
};
const sameNode = !!casesAll[name].same_node_original;
const origSameNode = (args) => {
const tmp = `/tmp/behav-orig-${name}-${nodeVer}.js`;
fs.writeFileSync(
tmp,
`${src}\nconsole.log("__JSCD__" + JSON.stringify(target.apply(null, ${JSON.stringify(args)})));\n`
);
try {
const out = run(nodeBin, [tmp]);
const line = out.split('\n').filter((l) => l.startsWith("__JSCD__")).pop();
if (!line) return { ok: false, value: "orig-no-output" };
return { ok: true, value: { plain: JSON.parse(line.slice("__JSCD__".length)) } };
} catch (e) {
const msg = String((e.stdout || "") + (e.stderr || e)).split("\n")[0];
return { ok: false, value: msg };
}
};
let same = 0;
const diffs = [];
for (const args of cases) {
const a = sameNode ? origSameNode(args) : callTimed(src, args, 'orig');
const b = callTimed(decoded, args, 'mine');
const av = await settle(a);
const bv = await settle(b);
if (av === bv && a.ok) {
same++;
} else if (!a.ok && !b.ok) {
const typeOf = (s) => String(s).replace(/^ERR /, '').split(':')[0].trim();
if (typeOf(av) === typeOf(bv)) same++;
else diffs.push({ args, orig: av, mine: bv, note: 'error-type' });
} else {
diffs.push({ args, orig: av, mine: bv });
}
}
result.matched = same;
result.status = same === cases.length ? 'pass' : (same === 0 ? 'fail' : 'partial');
if (casesAll[name].known_fail && result.status !== 'pass') {
result.status = 'known-fail';
result.detail = casesAll[name].known_fail;
}
try {
const missing = checkDeclarationsKept(jsc, roMap);
if (missing.length) {
result.status = 'lost-declarations';
result.detail = `默认产物丢了顶层函数声明: ${missing.join(', ')}`;
}
} catch (e) {
result.detail = `结构门禁执行失败: ${String(e.message).split('\n')[0]}`;
}
if (diffs.length) result.diffs = diffs.slice(0, 3);
console.log(JSON.stringify(result));
process.exit(0);
})();