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, }) } 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'}; `), ); let result; do { result = sandbox.step(); if (result.value?.async) { await result.value.async; } } while (!result.done); 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); `), ); let result; do { result = sandbox.step(); if (result.value?.async) { await result.value.async; } } while (!result.done); expect(sandbox.context.result) .to.deep.equal([1, 2, 1]); }); 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}}; `), ); let result; do { result = sandbox.step(); if (result.value?.async) { await result.value.value; } } while (!result.done); 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 < 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('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; } `), ); let result; do { result = sandbox.step(); if (result.value?.async) { await result.value.value; } } while (!result.done); expect(sandbox.context) .to.deep.equal({ foo: 1, bar: 2, }); }); test('evaluates loops', async () => { const sandbox = new Sandbox( await parse(` let 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; } `), ); let result; do { result = sandbox.step(); if (result.value?.async) { await result.value.value; } } while (!result.done); 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: {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; });