mirror of
https://github.com/oven-sh/bun
synced 2026-02-10 02:48:50 +00:00
456 lines
12 KiB
TypeScript
456 lines
12 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;
|
|
}
|
|
});
|