140 lines
8.9 KiB
JavaScript
140 lines
8.9 KiB
JavaScript
// Regression tests for variable scopes and return control flow in the editor interpreter.
|
|
// Run: node scripts/test-cpp-regressions.cjs
|
|
const assert = require('assert');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const vm = require('vm');
|
|
const babel = require('@babel/core');
|
|
const parserPath = path.resolve(__dirname, '../src/components/stage-cpp/cpp-parser.js');
|
|
const source = fs.readFileSync(parserPath, 'utf8');
|
|
const compiled = babel.transformSync(source, {
|
|
filename: parserPath, configFile: false, babelrc: false,
|
|
plugins: ['@babel/plugin-transform-modules-commonjs'],
|
|
presets: process.argv.includes('--transpiled') ? ['@babel/preset-env'] : []
|
|
}).code;
|
|
const silentConsole = {log() {}, warn() {}, error() {}};
|
|
const context = {exports: {}, require, setTimeout, console: silentConsole, window: {}};
|
|
vm.runInNewContext(compiled, context, {filename: parserPath});
|
|
const {CppCodeExecutor, validateCppSyntax, createCppExecutor} = context.exports;
|
|
const main = body => `int main() {\n${body}\n}`;
|
|
// Complete program from the first email screenshot; the second repeats this issue.
|
|
const customerEmailCode = `void test() {
|
|
for (int i = 0; i < 2; i++) {
|
|
}
|
|
}
|
|
|
|
int main() {
|
|
// 在这里编写你的C++代码来控制角色
|
|
turnLeft(-1);
|
|
for (int i = 0; i < 1; i++) {
|
|
test();
|
|
playerMove(i);
|
|
}
|
|
return 0;
|
|
}`;
|
|
async function run(code, expected, configure) {
|
|
const moves = [];
|
|
const turns = [];
|
|
context.window = {isScriptRunOver: false, cppGameAPI: {
|
|
playerMove: async n => moves.push(n),
|
|
turnLeft: async n => turns.push(n),
|
|
cmdPrint: () => {}
|
|
}};
|
|
const executor = new CppCodeExecutor(code, null);
|
|
if (configure) configure(executor, context.window);
|
|
const validation = await validateCppSyntax(code);
|
|
assert.strictEqual(validation.isValid, true, JSON.stringify(validation.errors));
|
|
const exitCode = await executor.run();
|
|
if (Object.prototype.hasOwnProperty.call(expected, 'exitCode')) assert.strictEqual(exitCode, expected.exitCode);
|
|
if (expected.moves) assert.deepStrictEqual(moves, expected.moves);
|
|
if (expected.turns) assert.deepStrictEqual(turns, expected.turns);
|
|
assert.strictEqual(executor.variables, executor.globalVariables, 'scope must unwind after execution');
|
|
assert.strictEqual(executor.callStack.length, 0);
|
|
assert.strictEqual(executor.currentExecutionContext, null);
|
|
assert.strictEqual(executor.isRunning, false);
|
|
return executor;
|
|
}
|
|
|
|
const cases = [
|
|
['screenshot: main return 1 is a normal exit', main('playerMove(4);\nreturn 1;'), {moves: [4], exitCode: 1}],
|
|
['return 1 skips remaining main statements', main('return 1;\nplayerMove(9);'), {moves: [], exitCode: 1}],
|
|
['return 0 also stops main early', main('return 0;\nplayerMove(9);'), {moves: [], exitCode: 0}],
|
|
['negative exit code', main('return -1;'), {moves: [], exitCode: -1}],
|
|
['parenthesized return expression', main('int status = 2;\nreturn (status + 1);'), {moves: [], exitCode: 3}],
|
|
['main fallthrough returns zero', main('playerMove(1);'), {moves: [1], exitCode: 0}],
|
|
['return from for loop exits main', main('for (int i = 0; i < 3; i++) {\nplayerMove(i);\nreturn 1;\n}\nplayerMove(9);'), {moves: [0], exitCode: 1}],
|
|
['return zero inside loop is not ignored', main('for (int i = 0; i < 3; i++) {\nreturn 0;\n}\nplayerMove(9);'), {moves: [], exitCode: 0}],
|
|
['return through while and if exits main', main('while (true) {\nif (true) {\nreturn 2;\n}\nplayerMove(9);\n}\nplayerMove(8);'), {moves: [], exitCode: 2}],
|
|
['function return 0 skips only its own remaining statements', 'int f() {\nreturn 0;\nplayerMove(9);\n}\n' + main('f();\nplayerMove(1);\nreturn 1;'), {moves: [1], exitCode: 1}],
|
|
['function return inside loop resumes caller', 'int f() {\nfor (int i = 0; i < 3; i++) {\nreturn 1;\n}\nplayerMove(9);\n}\n' + main('f();\nplayerMove(1);'), {moves: [1], exitCode: 0}],
|
|
['identifier starting with return is not a return statement', main('int returnValue = 1;\nreturnValue = 2;\nplayerMove(returnValue);\nreturn 0;'), {moves: [2], exitCode: 0}],
|
|
['customer email complete program (screenshots 1 and 2)', customerEmailCode, {moves: [0], turns: [-1]}],
|
|
['nested loops', main('for (int i = 0; i < 2; i++) {\nfor (int i = 0; i < 2; i++) {\n}\nplayerMove(i);\n}'), {moves: [0, 1]}],
|
|
['loop initializer restores shadowed value', main('int i = 9;\nfor (int i = 0; i < 2; i++) {\n}\nplayerMove(i);'), {moves: [9]}],
|
|
['loop assignment updates existing value', main('int i = 9;\nfor (i = 0; i < 2; i++) {\n}\nplayerMove(i);'), {moves: [2]}],
|
|
['block declaration versus assignment', main('int n = 1;\nif (true) {\nint n = 7;\nplayerMove(n);\n}\nif (true) {\nn = 3;\n}\nplayerMove(n);'), {moves: [7, 3]}],
|
|
['for body scope per iteration', main('int x = 9;\nfor (int i = 0; i < 2; i++) {\nplayerMove(x);\nint x = 2;\n}\nplayerMove(x);'), {moves: [9, 9, 9]}],
|
|
['while body scope per iteration', main('int x = 9;\nint i = 0;\nwhile (i < 2) {\nplayerMove(x);\nint x = 2;\ni++;\n}\nplayerMove(x);'), {moves: [9, 9, 9]}],
|
|
['function locals', 'void f() {\nint n = 7;\nplayerMove(n);\n}\n' + main('int n = 2;\nf();\nplayerMove(n);'), {moves: [7, 2]}],
|
|
['all arguments evaluated before binding', 'void f(int a, int b) {\nplayerMove(a);\nplayerMove(b);\n}\n' + main('int a = 2;\nf(7, a);\nplayerMove(a);'), {moves: [7, 2, 2]}],
|
|
['recursive locals and parameters', 'void f(int n) {\nint saved = n;\nif (n > 0) {\nf(n - 1);\n}\nplayerMove(saved);\n}\n' + main('f(2);'), {moves: [0, 1, 2]}],
|
|
['early void return unwinds scope', 'void f() {\nint n = 8;\nreturn;\n}\n' + main('int n = 2;\nf();\nplayerMove(n);'), {moves: [2]}],
|
|
['continue unwinds body scope', main('int x = 9;\nfor (int i = 0; i < 2; i++) {\nint x = 2;\ncontinue;\n}\nplayerMove(x);'), {moves: [9]}],
|
|
['break does not increment', main('int i = 0;\nfor (i = 0; i < 2; i++) {\nbreak;\n}\nplayerMove(i);'), {moves: [0]}],
|
|
];
|
|
|
|
(async () => {
|
|
for (const [name, code, expected] of cases) {
|
|
try { await run(code, expected); } catch (error) {error.message = `${name}: ${error.message}`; throw error;}
|
|
console.log(`PASS ${name}`);
|
|
}
|
|
const strings = new CppCodeExecutor('', null);
|
|
await strings.executeSimpleStatement('string msg = "outer";');
|
|
await strings.withVariableScope(async () => {
|
|
await strings.executeSimpleStatement('string msg = "inner";');
|
|
assert.strictEqual(strings.variables.get('msg'), 'inner');
|
|
});
|
|
assert.strictEqual(strings.variables.get('msg'), 'outer');
|
|
console.log('PASS string block shadowing');
|
|
|
|
// Seed the interpreter's global environment to verify lexical lookup and writes.
|
|
const globalExecutor = await run('void f() {\ng += 1;\nplayerMove(g);\n}\n' + main('int g = 9;\nf();\nplayerMove(g);'), {moves: [2, 9]}, executor => executor.variables.set('g', 1));
|
|
assert.strictEqual(globalExecutor.variables.get('g'), 2);
|
|
console.log('PASS global writes survive function calls; caller locals are not visible');
|
|
|
|
for (const stop of [false, true]) {
|
|
const executor = new CppCodeExecutor('void f(int n) {\nfor (int i = 0; i < 2; i++) {\nplayerMove(i);\n}\n}\n' + main('f(2);'), null);
|
|
executor.variables.set('n', 9);
|
|
context.window = {isScriptRunOver: false, cppGameAPI: {playerMove: async () => {
|
|
if (stop) context.window.isScriptRunOver = true;
|
|
else throw new Error('game failure');
|
|
}}};
|
|
if (stop) await executor.run();
|
|
else await assert.rejects(executor.run(), /game failure/);
|
|
assert.strictEqual(executor.variables, executor.globalVariables);
|
|
assert.strictEqual(executor.variables.get('n'), 9);
|
|
assert.strictEqual(executor.variables.has('i'), false);
|
|
assert.strictEqual(executor.callStack.length, 0);
|
|
assert.strictEqual(executor.currentExecutionContext, null);
|
|
console.log(`PASS scope cleanup on ${stop ? 'stop' : 'error'}`);
|
|
}
|
|
|
|
const highlighted = [];
|
|
context.window = {isScriptRunOver: false, setHighlightedLine: line => highlighted.push(line)};
|
|
const manager = createCppExecutor();
|
|
for (const status of [1, 0]) {
|
|
assert.strictEqual(await manager.execute(main(`return ${status};`)), status);
|
|
assert.strictEqual(manager.currentExecutor, null);
|
|
assert.strictEqual(highlighted[highlighted.length - 1], null);
|
|
}
|
|
console.log('PASS editor executor manager resolves nonzero exit and can run again');
|
|
|
|
context.window.cppGameAPI = {playerMove: async () => {throw new Error('RETURN_INTERRUPT');}};
|
|
await assert.rejects(manager.execute('void f() {\nplayerMove(1);\n}\n' + main('f();')), /RETURN_INTERRUPT/);
|
|
assert.strictEqual(manager.currentExecutor, null);
|
|
assert.strictEqual(highlighted[highlighted.length - 1], null);
|
|
console.log('PASS real game errors are not mistaken for a return signal');
|
|
|
|
console.log(`${cases.length + 6} regression checks passed`);
|
|
})().catch(error => {console.error(error); process.exitCode = 1;});
|