Files
bun.sh/test/js/node/vm/vm.test.ts

527 lines
13 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { createContext, runInContext, runInNewContext, runInThisContext, Script } from "node:vm";
function capture(_: any, _1?: any) {}
describe("runInContext()", () => {
testRunInContext({ fn: runInContext, isIsolated: true });
test("options can be a string", () => {
const context = createContext();
const result = runInContext("new Error().stack;", context, "test-filename.js");
expect(result).toContain("test-filename.js");
});
});
describe("runInNewContext()", () => {
testRunInContext({ fn: runInNewContext, isIsolated: true, isNew: true });
test("options can be a string", () => {
test("options can be a string", () => {
const result = runInNewContext("new Error().stack;", {}, "test-filename.js");
expect(result).toContain("test-filename.js");
});
});
});
describe("runInThisContext()", () => {
testRunInContext({ fn: runInThisContext });
test("options can be a string", () => {
const result = runInThisContext("new Error().stack;", "test-filename.js");
expect(result).toContain("test-filename.js");
});
});
describe("Script", () => {
describe("runInContext()", () => {
testRunInContext({
fn: (code, context, options) => {
const script = new Script(code, options);
return script.runInContext(context);
},
isIsolated: true,
});
});
describe("runInNewContext()", () => {
testRunInContext({
fn: (code, context, options) => {
const script = new Script(code, options);
return script.runInNewContext(context);
},
isIsolated: true,
isNew: true,
});
});
describe("runInThisContext()", () => {
testRunInContext({
fn: (code: string, options: any) => {
const script = new Script(code, options);
return script.runInThisContext();
},
});
});
test("can throw without new", () => {
// @ts-ignore
const result = () => Script();
expect(result).toThrow({
name: "TypeError",
message: "Class constructor Script cannot be invoked without 'new'",
});
});
});
type TestRunInContextArg =
| { fn: typeof runInContext; isIsolated: true; isNew?: boolean }
| { fn: typeof runInThisContext; isIsolated?: false; isNew?: boolean };
function testRunInContext({ fn, isIsolated, isNew }: TestRunInContextArg) {
test("can do nothing", () => {
const context = createContext({});
const result = fn("", context);
expect(result).toBeUndefined();
});
test("can return a value", () => {
const context = createContext({});
const result = fn("1 + 1;", context);
expect(result).toBe(2);
});
test("can return a complex value", () => {
const context = createContext({});
const result = fn("new Set([1, 2, 3]);", context);
expect(result).toStrictEqual(new Set([1, 2, 3]));
});
test("can return the last value", () => {
const context = createContext({});
const result = fn("1 + 1; 2 * 2; 3 / 3", context);
expect(result).toBe(1);
});
for (let View of [
ArrayBuffer,
SharedArrayBuffer,
Uint8Array,
Int8Array,
Uint16Array,
Int16Array,
Uint32Array,
Int32Array,
Float32Array,
Float64Array,
BigInt64Array,
BigUint64Array,
]) {
test(`new ${View.name}() in VM context doesn't crash`, () => {
const context = createContext({});
expect(fn(`new ${View.name}(2)`, context)).toHaveLength(2);
});
}
test("can return a function", () => {
const context = createContext({});
const result = fn("() => 'bar';", context);
expect(typeof result).toBe("function");
expect(result()).toBe("bar");
});
test("can throw a syntax error", () => {
const context = createContext({});
const result = () => fn("!?", context);
expect(result).toThrow({
name: "SyntaxError",
message: "Unexpected token '?'",
});
});
test("can throw an error", () => {
const context = createContext({});
const result = () => fn("throw new TypeError('Oops!');", context);
expect(result).toThrow({
name: "TypeError",
message: "Oops!",
});
});
test("can resolve a promise", async () => {
const context = createContext({});
const result = fn("Promise.resolve(true);", context);
expect(await result).toBe(true);
});
test("can reject a promise", () => {
const context = createContext({});
expect(async () => await fn("Promise.reject(new TypeError('Oops!'));", context)).toThrow({
name: "TypeError",
message: "Oops!",
});
});
test("can access `globalThis`", () => {
const context = createContext({});
const result = fn("typeof globalThis;", context);
expect(result).toBe("object");
});
test("cannot access local scope", () => {
var foo = "bar"; // intentionally unused
capture(foo, foo);
const context = createContext({});
const result = fn("typeof foo;", context);
expect(result).toBe("undefined");
});
if (isIsolated) {
test("can access context", () => {
const context = createContext({
foo: "bar",
fizz: (n: number) => "buzz".repeat(n),
});
const result = fn("foo + fizz(2);", context);
expect(result).toBe("barbuzzbuzz");
});
test("can modify context", () => {
const context = createContext({
baz: ["a", "b", "c"],
});
const result = fn("foo = 'baz'; delete baz[0];", context);
expect(context.foo).toBe("baz");
expect(context.baz).toEqual([undefined, "b", "c"]);
expect(result).toBe(true);
});
test("cannot access `process`", () => {
const context = createContext({});
const result = fn("typeof process;", context);
expect(result).toBe("undefined");
});
test("cannot access global scope", () => {
const prop = randomProp();
// @ts-expect-error
globalThis[prop] = "fizz";
try {
const context = createContext({});
const result = fn(`typeof ${prop};`, context);
expect(result).toBe("undefined");
} finally {
// @ts-expect-error
delete globalThis[prop];
}
});
test("can specify a filename", () => {
const context = createContext({});
const result = fn("new Error().stack;", context, {
filename: "foo.js",
});
expect(result).toContain("foo.js");
});
} else {
test("can access global context", () => {
const props = randomProps(2);
// @ts-expect-error
globalThis[props[0]] = "bar";
// @ts-expect-error
globalThis[props[1]] = (n: number) => "buzz".repeat(n);
try {
const result = fn(`${props[0]} + ${props[1]}(2);`);
expect(result).toBe("barbuzzbuzz");
} finally {
for (const prop of props) {
// @ts-expect-error
delete globalThis[prop];
}
}
});
test("can modify global context", () => {
const props = randomProps(3);
// @ts-expect-error
globalThis[props[0]] = ["a", "b", "c"];
// @ts-expect-error
globalThis[props[1]] = "initial value";
try {
const result = fn(`${props[1]} = 'baz'; ${props[2]} = 'bunny'; delete ${props[0]}[0];`);
// @ts-expect-error
expect(globalThis[props[1]]).toBe("baz");
// @ts-expect-error
expect(globalThis[props[2]]).toBe("bunny");
// @ts-expect-error
expect(globalThis[props[0]]).toEqual([undefined, "b", "c"]);
expect(result).toBe(true);
} finally {
for (const prop of props) {
// @ts-expect-error
delete globalThis[prop];
}
}
});
test("can access `process`", () => {
const result = fn("typeof process;");
expect(result).toBe("object");
});
test("can access this context", () => {
const prop = randomProp();
// @ts-expect-error
globalThis[prop] = "fizz";
try {
const result = fn(`${prop};`);
expect(result).toBe("fizz");
} finally {
// @ts-expect-error
delete globalThis[prop];
}
});
test.skip("can specify an error on SIGINT", () => {
const result = () =>
fn("process.kill(process.pid, 'SIGINT');", {
breakOnSigint: true,
});
// TODO: process.kill() is not implemented
expect(result).toThrow();
});
test("can specify a filename", () => {
const result = fn("new Error().stack;", {
filename: "foo.js",
});
expect(result).toContain("foo.js");
});
}
test.todo("can specify filename", () => {
//
});
test.todo("can specify lineOffset", () => {
//
});
test.todo("can specify columnOffset", () => {
//
});
test.todo("can specify displayErrors", () => {
//
});
test.todo("can specify timeout", () => {
//
});
test.todo("can specify breakOnSigint", () => {
//
});
test.todo("can specify cachedData", () => {
//
});
test.todo("can specify importModuleDynamically", () => {
//
});
// https://github.com/oven-sh/bun/issues/10885 .if(isNew == true)
test.todo("can specify contextName", () => {
//
});
// https://github.com/oven-sh/bun/issues/10885 .if(isNew == true)
test.todo("can specify contextOrigin", () => {
//
});
// https://github.com/oven-sh/bun/issues/10885 .if(isNew == true)
test.todo("can specify microtaskMode", () => {
//
});
}
function randomProp() {
return "prop" + crypto.randomUUID().replace(/-/g, "");
}
function randomProps(propsNumber = 0) {
const props = [];
for (let i = 0; i < propsNumber; i++) {
props.push(randomProp());
}
return props;
}
// https://github.com/oven-sh/bun/issues/13629
test("can extend generated globals & WebCore globals", async () => {
const vm = require("vm");
for (let j = 0; j < 100; j++) {
const context = vm.createContext({
URL,
urlProto: URL.prototype,
console,
Response,
});
const code = /*js*/ `
class ExtendedDOMGlobal extends URL {
constructor(url) {
super(url);
}
get searchParams() {
return super.searchParams;
}
}
class ExtendedExtendedDOMGlobal extends ExtendedDOMGlobal {
constructor(url) {
super(url);
}
get wowSuchGetter() {
return "wow such getter";
}
}
const response = new Response();
class ExtendedZigGeneratedClass extends Response {
constructor(body) {
super(body);
}
get ok() {
return super.ok;
}
get custom() {
return true;
}
}
class ExtendedExtendedZigGeneratedClass extends ExtendedZigGeneratedClass {
constructor(body) {
super(body);
}
get custom() {
return 42;
}
}
const resp = new ExtendedZigGeneratedClass("empty");
const resp2 = new ExtendedExtendedZigGeneratedClass("empty");
const url = new ExtendedDOMGlobal("https://example.com/path?foo=bar&baz=qux");
const url2 = new ExtendedExtendedDOMGlobal("https://example.com/path?foo=bar&baz=qux");
if (url.ok !== true) {
throw new Error("bad");
}
if (url2.wowSuchGetter !== "wow such getter") {
throw new Error("bad");
}
if (!response.ok) {
throw new Error("bad");
}
URL.prototype.ok = false;
if (url.ok !== false) {
throw new Error("bad");
}
url.searchParams.get("foo");
if (!resp.custom) {
throw new Error("expected getter");
}
if (resp2.custom !== 42) {
throw new Error("expected getter");
}
if (!resp2.ok) {
throw new Error("expected ok");
}
if (!(resp instanceof ExtendedZigGeneratedClass)) {
throw new Error("expected ExtendedZigGeneratedClass");
}
if (!(resp instanceof Response)) {
throw new Error("expected Response");
}
if (!(resp2 instanceof ExtendedExtendedZigGeneratedClass)) {
throw new Error("expected ExtendedExtendedZigGeneratedClass");
}
if (!(resp2 instanceof ExtendedZigGeneratedClass)) {
throw new Error("expected ExtendedZigGeneratedClass");
}
if (!(resp2 instanceof Response)) {
throw new Error("expected Response");
}
if (!resp.ok) {
throw new Error("expected ok");
}
resp.text().then((a) => {
if (a !== "empty") {
throw new Error("expected empty");
}
});
`;
URL.prototype.ok = true;
await vm.runInContext(code, context);
delete URL.prototype.ok;
}
});
test("can get sourceURL from eval inside node:vm", () => {
try {
runInNewContext(
`
throw new Error("hello");
//# sourceURL=hellohello.js
`,
{},
);
} catch (e: any) {
var err: Error = e;
}
expect(err!.stack!.replaceAll("\r\n", "\n").replaceAll(import.meta.path, "<this-url>")).toMatchInlineSnapshot(`
"Error: hello
at hellohello.js:2:16
at runInNewContext (unknown)
at <anonymous> (<this-url>:459:5)"
`);
});
test("can get sourceURL inside node:vm", () => {
const err = runInNewContext(
`
function hello() {
return Bun.inspect(new Error("hello"));
}
hello();
//# sourceURL=hellohello.js
`,
{ Bun },
);
expect(err.replaceAll("\r\n", "\n").replaceAll(import.meta.path, "<this-url>")).toMatchInlineSnapshot(`
"4 | return Bun.inspect(new Error("hello"));
^
error: hello
at hello (hellohello.js:4:24)
at hellohello.js:7:6
at <anonymous> (<this-url>:479:15)
"
`);
});
test("eval sourceURL is correct", () => {
const err = eval(
`
function hello() {
return Bun.inspect(new Error("hello"));
}
hello();
//# sourceURL=hellohello.js
`,
);
expect(err.replaceAll("\r\n", "\n").replaceAll(import.meta.path, "<this-url>")).toMatchInlineSnapshot(`
"4 | return Bun.inspect(new Error("hello"));
^
error: hello
at hello (hellohello.js:4:24)
at eval (hellohello.js:7:6)
at <anonymous> (<this-url>:505:15)
"
`);
});