import {parse as acornParse} from 'acorn'; import {expect, test} from 'vitest'; import Sandbox from '@/astride/sandbox.js'; function parse(code, options = {}) { return acornParse(code, { ecmaVersion: 'latest', sourceType: 'module', ...options, }) } async function finish(sandbox) { let result; let i = 0; do { result = sandbox.step(); if (result.async) { await result.value; } } while (!result.done && ++i < 1000); return result; } test('declares variables', async () => { const sandbox = new Sandbox( await parse(` const scalar = true ? +1 : 32; const asyncScalar = await 2; const array = [3, 4, 5]; const asyncArray = await [6, 7, 8]; const object = {9: '10'}; const asyncObject = await {11: '12'}; `), ); await finish(sandbox); expect(sandbox.context) .to.deep.equal({ scalar: 1, asyncScalar: 2, array: [3, 4, 5], asyncArray: [6, 7, 8], object: {9: '10'}, asyncObject: {11: '12'}, }); }); test('scopes variables', async () => { const sandbox = new Sandbox( await parse(` const result = []; const scalar = 1; result.push(scalar); { const scalar = 2; result.push(scalar); } result.push(scalar); `), ); await finish(sandbox); expect(sandbox.context.result) .to.deep.equal([1, 2, 1]); }); test('returns last', async () => { const sandbox = new Sandbox( await parse(` foo = 32; { bar = 64; } `), ); ; expect(await finish(sandbox)) .to.deep.include({done: true, value: 64}); }); test('destructures variables', async () => { const sandbox = new Sandbox( await parse(` const [a, , c] = [1, 2, 3]; const {x: x1, y, z: {zz}} = {x: 4, y: 5, z: {zz: 6}}; const [d, e] = await [7, 8]; const {t, u: {uu}} = {t: 9, u: {uu: await 10}}; `), ); await finish(sandbox); expect(sandbox.context) .to.deep.equal({ a: 1, c: 3, x1: 4, y: 5, zz: 6, d: 7, e: 8, t: 9, uu: 10, }); }); test('runs arbitrary number of ops', async () => { const sandbox = new Sandbox( await parse(` const foo = []; for (let i = 0; i < 150; ++i) { foo.push(i); } `), ); sandbox.run(100); expect(sandbox.context.foo.length) .to.equal(100); sandbox.run(100); expect(sandbox.context.foo.length) .to.equal(150); }); test('evaluates conditional branches', async () => { const sandbox = new Sandbox( await parse(` let foo, bar; if (true) { foo = 1; } else { foo = 2; } if (await false) { bar = 1; } else { bar = 2; } if (await false) { bar = 1; } `), ); await finish(sandbox); expect(sandbox.context) .to.deep.equal({ foo: 1, bar: 2, }); }); test('evaluates loops', async () => { const sandbox = new Sandbox( await parse(` x = 0 y = 0 a = 0 b = 0 c = 0 for (let i = 0; i < 3; ++i) { x += 1; } for (let i = await 0; i < await 3; i = 1 + await i) { y += 1; } do { a += 1; } while (a < 3); do { b += 1; } while (await b < 3); while (c < 3) { c += 1; } `), ); await finish(sandbox); expect(sandbox.context) .to.deep.equal({ a: 3, b: 3, c: 3, x: 3, y: 3, }); }); test('evaluates undefined for nonexistent variables in scope', async () => { const sandbox = new Sandbox( await parse(` const x = y `), ); sandbox.run(); expect(sandbox.context) .to.deep.equal({ x: undefined, }); }); test('returns values at the top level', async () => { let sandbox; sandbox = new Sandbox( await parse(` x = 16 y = 4 return [x * 3, y * 3] x = 32 y = 8 `, {allowReturnOutsideFunction: true}), ); const {value} = sandbox.run(); expect(value) .to.deep.equal([48, 12]); expect(sandbox.context) .to.deep.equal({x: 16, y: 4}); sandbox = new Sandbox( await parse(` x = 16 y = 4 return x = 32 y = 8 `, {allowReturnOutsideFunction: true}), ); sandbox.run(); expect(sandbox.context) .to.deep.equal({x: 16, y: 4}); }); test('sets variables in global scope', async () => { const sandbox = new Sandbox( await parse(` x = y `), ); sandbox.run(); expect(sandbox.context) .to.deep.equal({ x: undefined, }); }); test('runs arbitrary number of ops', async () => { const sandbox = new Sandbox( await parse(` const foo = []; for (let i = 0; i < 1500; ++i) { foo.push(i); } `), ); sandbox.run(1000); expect(sandbox.context.foo.length) .to.equal(1000); sandbox.run(1000); expect(sandbox.context.foo.length) .to.equal(1500); expect(true) .to.be.true; }); test('clears for loop deferred context', async () => { const context = {x: 10}; const sandbox = new Sandbox( await parse(` let l = 0 for (let i = 0; i < x; ++i) { l += 1 } `), context, ); expect(sandbox.run()) .to.deep.include({ value: 10, }); context.x = 0 expect(sandbox.run()) .to.deep.include({ value: undefined, }); }); test('clears while loop deferred context', async () => { const context = {x: 10}; const sandbox = new Sandbox( await parse(` let l = 0 while (l < x) { l += 1 } `), context, ); expect(sandbox.run()) .to.deep.include({ value: 10, }); context.x = 0 expect(sandbox.run()) .to.deep.include({ value: undefined, }); });