import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { test } from "node:test"; import { build } from "esbuild"; const compiled = await build({ entryPoints: [fileURLToPath(new URL("../src/player/python.ts", import.meta.url))], bundle: true, write: false, format: "esm", platform: "node", logLevel: "silent" }); const { Python, PyDict, userBuiltins, encodeValue, decodeValue, pyStr } = await import(`data:text/javascript;base64,${Buffer.from(compiled.outputFiles[0].text).toString("base64")}`); const ir = (name) => JSON.parse(readFileSync(new URL(`./fixtures/${name}.ir.json`, import.meta.url), "utf8")); function runtime() { const store = Object.create(null); const builtins = { any: (xs) => xs.some(Boolean), len: (x) => (x.map ? x.map.size : x.length), range: (n) => Array.from({ length: n }, (_, i) => i), max: Math.max, int: Number, str: pyStr, list: (xs = []) => [...xs], }; const scope = { get: (n) => (n in store ? store[n] : builtins[n]), has: (n) => n in store || n in builtins, set: (n, v) => { store[n] = v; }, del: (n) => { delete store[n]; }, }; const py = new Python(scope); Object.assign(builtins, userBuiltins(py)); return { py, store }; } test("user classes, functions, closures and lambdas behave like CPython", () => { const { py, store } = runtime(); py.exec(ir("classes")); assert.equal(store.result_brave, true); assert.equal(store.result_legend, true); assert.equal(store.result_missing, false); assert.equal(store.result_count, 2); assert.equal(store.result_str, "Perk(x)"); assert.equal(store.result_hp, 0); // property setter clamps assert.equal(store.result_role, "hero"); // **kwargs assert.equal(store.result_static, "Bob"); assert.equal(store.result_cls, true); // classmethod gets the class assert.equal(store.result_bump, 20); // *args / **kwargs call and `global` assert.equal(store.total, 20); assert.equal(store.result_fact, 720); assert.equal(store.result_closure, 2); // nonlocal assert.deepEqual(store.result_lambda, [3, 2, 1]); assert.deepEqual(store.result_isinstance, [true, false, true]); }); test("instances survive a save round trip once the class is defined again", () => { const { py, store } = runtime(); py.exec(ir("classes")); const saved = JSON.parse(JSON.stringify(encodeValue({ hero: store.hero, dict: new PyDict([["a", store.hero]]) }))); assert.equal(saved.hero.__inst__, "Hero"); assert.equal("Hero" in saved, false); const restored = decodeValue(saved); assert.equal(restored.hero.cls.name, "Hero"); store.hero = restored.hero; assert.equal(py.eval({ k: "call", f: { k: "attr", o: { k: "name", id: "hero" }, a: "has_perk" }, a: [{ k: "const", v: "fast" }], kw: {} }), true); }); test("the sandbox still refuses native members through user objects", () => { const { py, store } = runtime(); py.exec(ir("classes")); const hero = { k: "name", id: "hero" }; for (const key of ["constructor", "__proto__", "__class__", "__dict__", "prototype"]) { assert.equal(py.eval({ k: "attr", o: hero, a: key }), null); assert.throws(() => py.bind({ k: "attr", o: hero, a: key }, 1), /недоступ/); } assert.equal(Object.getPrototypeOf(store.hero.attrs), null); assert.equal({}.injected, undefined); }); test("unbounded recursion and bad calls fail with explicit errors", () => { const { py } = runtime(); py.exec([{ op: "def", name: "loop", params: [], decos: [], body: [{ op: "return", v: { k: "call", f: { k: "name", id: "loop" }, a: [], kw: {} } }] }]); assert.throws(() => py.eval({ k: "call", f: { k: "name", id: "loop" }, a: [], kw: {} }), /рекурси/); py.exec([{ op: "def", name: "one", params: [{ n: "x", d: null, kind: "p" }], decos: [], body: [] }]); assert.throws(() => py.eval({ k: "call", f: { k: "name", id: "one" }, a: [], kw: {} }), /не хватает/); assert.throws(() => py.eval({ k: "call", f: { k: "name", id: "one" }, a: [{ k: "const", v: 1 }, { k: "const", v: 2 }], kw: {} }), /слишком много/); }); test("try/except/else/finally, raise, loop else, assert and multi-generator comprehensions", () => { const { py, store } = runtime(); py.exec(ir("exceptions")); assert.deepEqual(store.log, ["zero", "fin"]); assert.equal(store.result_code, 7); assert.equal(store.result_key, "keyerror"); // dict[...] is strict inside try assert.equal(store.result_index, "indexerror"); assert.equal(store.result_value, "valueerror"); // int("abc") assert.equal(store.result_generic, true); assert.equal(store.result_zero, "zerodiv"); assert.equal(store.result_retry, 2); // return/continue pass through try assert.equal(store.result_forelse, "none"); assert.equal(store.result_whileelse, 30); assert.equal(store.result_reraise, "inner"); assert.deepEqual(store.result_pairs, [[1, 3], [1, 4], [2, 3], [2, 4]]); assert.deepEqual([...store.result_dictcomp.map.entries()], [[2, 20]]); assert.equal(store.result_assert, "boom"); }); test("lookups stay lenient outside try, uncaught exceptions are explicit errors", () => { const { py, store } = runtime(); py.exec([{ op: "set", t: { k: "name", id: "d" }, v: { k: "dict", v: [] } }]); assert.equal(py.eval({ k: "sub", o: { k: "name", id: "d" }, i: { k: "const", v: "missing" } }), null); assert.throws(() => py.exec([{ op: "raise", v: { k: "name", id: "ValueError" } }]), /ValueError/); assert.equal(py.strict, 0); void store; }); test("__hash__, __iter__, __getitem__ and __contains__ work on user classes", () => { const { py, store } = runtime(); py.exec(ir("dunders")); assert.equal(store.result_size, 1); assert.equal(store.result_value, 2); assert.deepEqual(store.result_loop, [6, 8, 10]); assert.equal(store.result_item, 4); assert.equal(store.result_in, true); assert.equal(store.result_out, false); }); test("generator functions hand out what they yield; iter and next walk a sequence", () => { const { py, store } = runtime(); py.exec(ir("dunders")); assert.deepEqual(store.result_found, [1, 2, 3, 4]); assert.equal(store.result_first, 7); assert.equal(store.result_second, 8); assert.equal(store.result_default, "done"); });