From 1bf540efcf92a28e403699f6d28debb34395c809 Mon Sep 17 00:00:00 2001 From: dave caruso Date: Wed, 6 Dec 2023 22:09:43 -0800 Subject: [PATCH] feat: allow "bun:test" utilities at runtime (#7486) * feat: allow "bun:test" utilities at runtime * fsdafdsafd * yippee --- src/bun.js/module_loader.zig | 4 + src/bun.js/modules/BunObjectModule.cpp | 8 +- src/bun.js/modules/BunTestModule.h | 17 ++ src/bun.js/modules/NodeModuleModule.h | 1 + src/bun.js/modules/_NativeModule.h | 1 + src/bun.js/test/expect.zig | 13 +- src/bun.js/test/jest.zig | 176 +++++++++--------- src/js/node/repl.ts | 119 ++++++------ src/js_printer.zig | 2 +- .../js/node/module/node-module-module.test.js | 2 +- test/js/node/stubs.test.js | 7 +- 11 files changed, 195 insertions(+), 155 deletions(-) create mode 100644 src/bun.js/modules/BunTestModule.h diff --git a/src/bun.js/module_loader.zig b/src/bun.js/module_loader.zig index 65b780eb39..d8de853670 100644 --- a/src/bun.js/module_loader.zig +++ b/src/bun.js/module_loader.zig @@ -2200,6 +2200,7 @@ pub const ModuleLoader = struct { .@"node:util/types" => return jsSyntheticModule(.@"node:util/types", specifier), .@"node:constants" => return jsSyntheticModule(.@"node:constants", specifier), .@"bun:jsc" => return jsSyntheticModule(.@"bun:jsc", specifier), + .@"bun:test" => return jsSyntheticModule(.@"bun:test", specifier), // These are defined in src/js/* .@"bun:ffi" => return jsSyntheticModule(.@"bun:ffi", specifier), @@ -2377,6 +2378,7 @@ pub const HardcodedModule = enum { @"bun:ffi", @"bun:jsc", @"bun:main", + @"bun:test", // usually replaced by the transpiler but `await import("bun:" + "test")` has to work @"bun:sqlite", @"detect-libc", @"node:assert", @@ -2450,6 +2452,7 @@ pub const HardcodedModule = enum { .{ "bun:ffi", HardcodedModule.@"bun:ffi" }, .{ "bun:jsc", HardcodedModule.@"bun:jsc" }, .{ "bun:main", HardcodedModule.@"bun:main" }, + .{ "bun:test", HardcodedModule.@"bun:test" }, .{ "bun:sqlite", HardcodedModule.@"bun:sqlite" }, .{ "detect-libc", HardcodedModule.@"detect-libc" }, .{ "node-fetch", HardcodedModule.@"node-fetch" }, @@ -2658,6 +2661,7 @@ pub const HardcodedModule = enum { const bun_extra_alias_kvs = .{ .{ "bun", .{ .path = "bun", .tag = .bun } }, + .{ "bun:test", .{ .path = "bun:test", .tag = .bun_test } }, .{ "bun:ffi", .{ .path = "bun:ffi" } }, .{ "bun:jsc", .{ .path = "bun:jsc" } }, .{ "bun:sqlite", .{ .path = "bun:sqlite" } }, diff --git a/src/bun.js/modules/BunObjectModule.cpp b/src/bun.js/modules/BunObjectModule.cpp index 55f694fa16..57367dfb11 100644 --- a/src/bun.js/modules/BunObjectModule.cpp +++ b/src/bun.js/modules/BunObjectModule.cpp @@ -9,13 +9,11 @@ void generateNativeModule_BunObject(JSC::JSGlobalObject *lexicalGlobalObject, JSC::Identifier moduleKey, Vector &exportNames, JSC::MarkedArgumentBuffer &exportValues) { + // FIXME: this does not add each property as a top level export JSC::VM &vm = lexicalGlobalObject->vm(); - Zig::GlobalObject *globalObject = - reinterpret_cast(lexicalGlobalObject); + Zig::GlobalObject *globalObject = jsCast(lexicalGlobalObject); - JSObject *object = - globalObject->get(globalObject, Identifier::fromString(vm, "Bun"_s)) - .getObject(); + JSObject *object = globalObject->bunObject(); exportNames.append(vm.propertyNames->defaultKeyword); exportValues.append(object); diff --git a/src/bun.js/modules/BunTestModule.h b/src/bun.js/modules/BunTestModule.h new file mode 100644 index 0000000000..84687b6e93 --- /dev/null +++ b/src/bun.js/modules/BunTestModule.h @@ -0,0 +1,17 @@ + +namespace Zig { +void generateNativeModule_BunTest( + JSC::JSGlobalObject *lexicalGlobalObject, + JSC::Identifier moduleKey, + Vector &exportNames, + JSC::MarkedArgumentBuffer &exportValues) { + JSC::VM &vm = lexicalGlobalObject->vm(); + auto globalObject = jsCast(lexicalGlobalObject); + + JSObject *object = globalObject->lazyPreloadTestModuleObject(); + + exportNames.append(vm.propertyNames->defaultKeyword); + exportValues.append(object); +} + +} // namespace Zig \ No newline at end of file diff --git a/src/bun.js/modules/NodeModuleModule.h b/src/bun.js/modules/NodeModuleModule.h index 00872c64a5..00807521c3 100644 --- a/src/bun.js/modules/NodeModuleModule.h +++ b/src/bun.js/modules/NodeModuleModule.h @@ -35,6 +35,7 @@ static constexpr ASCIILiteral builtinModuleNames[] = { "bun:ffi"_s, "bun:jsc"_s, "bun:sqlite"_s, + "bun:test"_s, "bun:wrap"_s, "child_process"_s, "cluster"_s, diff --git a/src/bun.js/modules/_NativeModule.h b/src/bun.js/modules/_NativeModule.h index cd855a7a96..f21ab46305 100644 --- a/src/bun.js/modules/_NativeModule.h +++ b/src/bun.js/modules/_NativeModule.h @@ -25,6 +25,7 @@ #define BUN_FOREACH_NATIVE_MODULE(macro) \ macro("bun"_s, BunObject) \ + macro("bun:test"_s, BunTest) \ macro("bun:jsc"_s, BunJSC) \ macro("node:buffer"_s, NodeBuffer) \ macro("node:constants"_s, NodeConstants) \ diff --git a/src/bun.js/test/expect.zig b/src/bun.js/test/expect.zig index e30150c75b..2690487808 100644 --- a/src/bun.js/test/expect.zig +++ b/src/bun.js/test/expect.zig @@ -334,11 +334,14 @@ pub const Expect = struct { }; expect.* = .{ - .parent = if (Jest.runner.?.pending_test) |pending| - Expect.ParentScope{ .TestScope = Expect.TestScope{ - .describe = pending.describe, - .test_id = pending.test_id, - } } + .parent = if (Jest.runner) |runner| + if (runner.pending_test) |pending| + Expect.ParentScope{ .TestScope = Expect.TestScope{ + .describe = pending.describe, + .test_id = pending.test_id, + } } + else + Expect.ParentScope{ .global = {} } else Expect.ParentScope{ .global = {} }, }; diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index 8a87044df8..b3484142d9 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -273,6 +273,11 @@ pub const Jest = struct { globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame, ) callconv(.C) JSValue { + const the_runner = runner orelse { + globalThis.throw("Cannot use " ++ name ++ "() outside of the test runner. Run \"bun test\" to run tests.", .{}); + return .zero; + }; + const arguments = callframe.arguments(2); if (arguments.len < 1) { globalThis.throwNotEnoughArguments("callback", 1, arguments.len); @@ -291,7 +296,7 @@ pub const Jest = struct { } function.protect(); - @field(Jest.runner.?.global_callbacks, name).append( + @field(the_runner.global_callbacks, name).append( bun.default_allocator, function, ) catch unreachable; @@ -300,49 +305,23 @@ pub const Jest = struct { }.appendGlobalFunctionCallback; } - pub fn Bun__Jest__createTestPreloadObject(globalObject: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue { - JSC.markBinding(@src()); - - var global_hooks_object = JSC.JSValue.createEmptyObject(globalObject, 8); - global_hooks_object.ensureStillAlive(); - - const notSupportedHereFn = struct { - pub fn notSupportedHere( - globalThis: *JSC.JSGlobalObject, - _: *JSC.CallFrame, - ) callconv(.C) JSValue { - globalThis.throw("This function can only be used in a test.", .{}); - return .zero; - } - }.notSupportedHere; - const notSupportedHere = JSC.NewFunction(globalObject, null, 0, notSupportedHereFn, false); - notSupportedHere.ensureStillAlive(); - - inline for (.{ - "expect", - "describe", - "it", - "test", - }) |name| { - global_hooks_object.put(globalObject, ZigString.static(name), notSupportedHere); - } - - inline for (.{ "beforeAll", "beforeEach", "afterAll", "afterEach" }) |name| { - const function = JSC.NewFunction(globalObject, null, 1, globalHook(name), false); - function.ensureStillAlive(); - global_hooks_object.put(globalObject, ZigString.static(name), function); - } - - createMockObjects(globalObject, global_hooks_object); - return global_hooks_object; + pub fn Bun__Jest__createTestModuleObject(globalObject: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue { + return createTestModule(globalObject, false); } - pub fn Bun__Jest__createTestModuleObject(globalObject: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue { - JSC.markBinding(@src()); + pub fn Bun__Jest__createTestPreloadObject(globalObject: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue { + return createTestModule(globalObject, true); + } + + pub fn createTestModule(globalObject: *JSC.JSGlobalObject, comptime outside_of_test: bool) JSC.JSValue { + const ThisTestScope, const ThisDescribeScope = if (outside_of_test) + .{ WrappedTestScope, WrappedDescribeScope } + else + .{ TestScope, DescribeScope }; const module = JSC.JSValue.createEmptyObject(globalObject, 13); - const test_fn = JSC.NewFunction(globalObject, ZigString.static("test"), 2, TestScope.call, false); + const test_fn = JSC.NewFunction(globalObject, ZigString.static("test"), 2, ThisTestScope.call, false); module.put( globalObject, ZigString.static("test"), @@ -351,32 +330,32 @@ pub const Jest = struct { test_fn.put( globalObject, ZigString.static("only"), - JSC.NewFunction(globalObject, ZigString.static("only"), 2, TestScope.only, false), + JSC.NewFunction(globalObject, ZigString.static("only"), 2, ThisTestScope.only, false), ); test_fn.put( globalObject, ZigString.static("skip"), - JSC.NewFunction(globalObject, ZigString.static("skip"), 2, TestScope.skip, false), + JSC.NewFunction(globalObject, ZigString.static("skip"), 2, ThisTestScope.skip, false), ); test_fn.put( globalObject, ZigString.static("todo"), - JSC.NewFunction(globalObject, ZigString.static("todo"), 2, TestScope.todo, false), + JSC.NewFunction(globalObject, ZigString.static("todo"), 2, ThisTestScope.todo, false), ); test_fn.put( globalObject, ZigString.static("if"), - JSC.NewFunction(globalObject, ZigString.static("if"), 2, TestScope.callIf, false), + JSC.NewFunction(globalObject, ZigString.static("if"), 2, ThisTestScope.callIf, false), ); test_fn.put( globalObject, ZigString.static("skipIf"), - JSC.NewFunction(globalObject, ZigString.static("skipIf"), 2, TestScope.skipIf, false), + JSC.NewFunction(globalObject, ZigString.static("skipIf"), 2, ThisTestScope.skipIf, false), ); test_fn.put( globalObject, ZigString.static("each"), - JSC.NewFunction(globalObject, ZigString.static("each"), 2, TestScope.each, false), + JSC.NewFunction(globalObject, ZigString.static("each"), 2, ThisTestScope.each, false), ); module.put( @@ -384,36 +363,36 @@ pub const Jest = struct { ZigString.static("it"), test_fn, ); - const describe = JSC.NewFunction(globalObject, ZigString.static("describe"), 2, DescribeScope.call, false); + const describe = JSC.NewFunction(globalObject, ZigString.static("describe"), 2, ThisDescribeScope.call, false); describe.put( globalObject, ZigString.static("only"), - JSC.NewFunction(globalObject, ZigString.static("only"), 2, DescribeScope.only, false), + JSC.NewFunction(globalObject, ZigString.static("only"), 2, ThisDescribeScope.only, false), ); describe.put( globalObject, ZigString.static("skip"), - JSC.NewFunction(globalObject, ZigString.static("skip"), 2, DescribeScope.skip, false), + JSC.NewFunction(globalObject, ZigString.static("skip"), 2, ThisDescribeScope.skip, false), ); describe.put( globalObject, ZigString.static("todo"), - JSC.NewFunction(globalObject, ZigString.static("todo"), 2, DescribeScope.todo, false), + JSC.NewFunction(globalObject, ZigString.static("todo"), 2, ThisDescribeScope.todo, false), ); describe.put( globalObject, ZigString.static("if"), - JSC.NewFunction(globalObject, ZigString.static("if"), 2, DescribeScope.callIf, false), + JSC.NewFunction(globalObject, ZigString.static("if"), 2, ThisDescribeScope.callIf, false), ); describe.put( globalObject, ZigString.static("skipIf"), - JSC.NewFunction(globalObject, ZigString.static("skipIf"), 2, DescribeScope.skipIf, false), + JSC.NewFunction(globalObject, ZigString.static("skipIf"), 2, ThisDescribeScope.skipIf, false), ); describe.put( globalObject, ZigString.static("each"), - JSC.NewFunction(globalObject, ZigString.static("each"), 2, DescribeScope.each, false), + JSC.NewFunction(globalObject, ZigString.static("each"), 2, ThisDescribeScope.each, false), ); module.put( @@ -422,26 +401,22 @@ pub const Jest = struct { describe, ); - module.put( - globalObject, - ZigString.static("beforeAll"), - JSC.NewRuntimeFunction(globalObject, ZigString.static("beforeAll"), 1, DescribeScope.beforeAll, false, false), - ); - module.put( - globalObject, - ZigString.static("beforeEach"), - JSC.NewRuntimeFunction(globalObject, ZigString.static("beforeEach"), 1, DescribeScope.beforeEach, false, false), - ); - module.put( - globalObject, - ZigString.static("afterAll"), - JSC.NewRuntimeFunction(globalObject, ZigString.static("afterAll"), 1, DescribeScope.afterAll, false, false), - ); - module.put( - globalObject, - ZigString.static("afterEach"), - JSC.NewRuntimeFunction(globalObject, ZigString.static("afterEach"), 1, DescribeScope.afterEach, false, false), - ); + inline for (.{ "beforeAll", "beforeEach", "afterAll", "afterEach" }) |name| { + const function = if (outside_of_test) + JSC.NewFunction(globalObject, null, 1, globalHook(name), false) + else + JSC.NewRuntimeFunction( + globalObject, + ZigString.static(name), + 1, + @field(DescribeScope, name), + false, + false, + ); + module.put(globalObject, ZigString.static(name), function); + function.ensureStillAlive(); + } + module.put( globalObject, ZigString.static("expect"), @@ -523,12 +498,12 @@ pub const Jest = struct { globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame, ) callconv(.C) JSC.JSValue { - JSC.markBinding(@src()); + var vm = globalObject.bunVM(); + if (vm.is_in_preload or runner == null) { + return Bun__Jest__testPreloadObject(globalObject); + } + const arguments = callframe.arguments(2).slice(); - var runner_ = runner orelse { - globalObject.throw("Run \"bun test\" to run a test", .{}); - return .undefined; - }; if (arguments.len < 1 or !arguments[0].isString()) { globalObject.throw("Bun.jest() expects a string filename", .{}); @@ -542,14 +517,9 @@ pub const Jest = struct { globalObject.throw("Bun.jest() expects an absolute file path", .{}); return .undefined; } - var vm = globalObject.bunVM(); - if (vm.is_in_preload) { - return Bun__Jest__testPreloadObject(globalObject); - } var filepath = Fs.FileSystem.instance.filename_store.append([]const u8, slice) catch unreachable; - - var scope = runner_.getOrPutFile(filepath); + var scope = runner.?.getOrPutFile(filepath); scope.push(); return Bun__Jest__testModuleObject(globalObject); @@ -874,7 +844,7 @@ pub const DescribeScope = struct { pub threadlocal var active: ?*DescribeScope = null; - const CallbackFn = *const fn ( + const CallbackFn = fn ( *JSC.JSGlobalObject, *JSC.CallFrame, ) callconv(.C) JSC.JSValue; @@ -1242,6 +1212,44 @@ pub const DescribeScope = struct { }; +pub fn wrapTestFunction(comptime name: []const u8, comptime func: DescribeScope.CallbackFn) DescribeScope.CallbackFn { + return struct { + pub fn wrapped(globalThis: *JSGlobalObject, callframe: *CallFrame) callconv(.C) JSValue { + if (Jest.runner == null) { + globalThis.throw("Cannot use " ++ name ++ "() outside of the test runner. Run \"bun test\" to run tests.", .{}); + return .zero; + } + if (globalThis.bunVM().is_in_preload) { + globalThis.throw("Cannot use " ++ name ++ "() outside of a test file.", .{}); + return .zero; + } + return @call(.always_inline, func, .{ globalThis, callframe }); + } + }.wrapped; +} + +/// This wrapped scope as well as the wrapped describe scope is used when you load `bun:test` +/// outside of +pub const WrappedTestScope = struct { + pub const call = wrapTestFunction("test", TestScope.call); + pub const only = wrapTestFunction("test", TestScope.only); + pub const skip = wrapTestFunction("test", TestScope.skip); + pub const todo = wrapTestFunction("test", TestScope.todo); + pub const callIf = wrapTestFunction("test", TestScope.callIf); + pub const skipIf = wrapTestFunction("test", TestScope.skipIf); + pub const each = wrapTestFunction("test", TestScope.each); +}; + +pub const WrappedDescribeScope = struct { + pub const call = wrapTestFunction("describe", DescribeScope.call); + pub const only = wrapTestFunction("describe", DescribeScope.only); + pub const skip = wrapTestFunction("describe", DescribeScope.skip); + pub const todo = wrapTestFunction("describe", DescribeScope.todo); + pub const callIf = wrapTestFunction("describe", DescribeScope.callIf); + pub const skipIf = wrapTestFunction("describe", DescribeScope.skipIf); + pub const each = wrapTestFunction("describe", DescribeScope.each); +}; + pub const TestRunnerTask = struct { test_id: TestRunner.Test.ID, describe: *DescribeScope, diff --git a/src/js/node/repl.ts b/src/js/node/repl.ts index 90dbf50233..10ca90e391 100644 --- a/src/js/node/repl.ts +++ b/src/js/node/repl.ts @@ -18,6 +18,65 @@ function start() { throwNotImplemented("node:repl"); } +const builtinModules = [ + "bun", + "ffi", + "assert", + "assert/strict", + "async_hooks", + "buffer", + "child_process", + "cluster", + "console", + "constants", + "crypto", + "dgram", + "diagnostics_channel", + "dns", + "dns/promises", + "domain", + "events", + "fs", + "fs/promises", + "http", + "http2", + "https", + "inspector", + "inspector/promises", + "module", + "net", + "os", + "path", + "path/posix", + "path/win32", + "perf_hooks", + "process", + "punycode", + "querystring", + "readline", + "readline/promises", + "repl", + "stream", + "stream/consumers", + "stream/promises", + "stream/web", + "string_decoder", + "sys", + "timers", + "timers/promises", + "tls", + "trace_events", + "tty", + "url", + "util", + "util/types", + "v8", + "vm", + "wasi", + "worker_threads", + "zlib", +]; + export default { lines: [], context: globalThis, @@ -74,62 +133,6 @@ export default { }, }, ), - _builtinLibs: [ - "bun", - "ffi", - "assert", - "assert/strict", - "async_hooks", - "buffer", - "child_process", - "cluster", - "console", - "constants", - "crypto", - "dgram", - "diagnostics_channel", - "dns", - "dns/promises", - "domain", - "events", - "fs", - "fs/promises", - "http", - "http2", - "https", - "inspector", - "inspector/promises", - "module", - "net", - "os", - "path", - "path/posix", - "path/win32", - "perf_hooks", - "process", - "punycode", - "querystring", - "readline", - "readline/promises", - "repl", - "stream", - "stream/consumers", - "stream/promises", - "stream/web", - "string_decoder", - "sys", - "timers", - "timers/promises", - "tls", - "trace_events", - "tty", - "url", - "util", - "util/types", - "v8", - "vm", - "wasi", - "worker_threads", - "zlib", - ], + _builtinLibs: builtinModules, + builtinModules: builtinModules, }; diff --git a/src/js_printer.zig b/src/js_printer.zig index 56d644d8f9..19aa75939d 100644 --- a/src/js_printer.zig +++ b/src/js_printer.zig @@ -1017,7 +1017,7 @@ fn NewPrinter( } fn printBunJestImportStatement(p: *Printer, import: S.Import) void { - if (comptime !is_bun_platform) unreachable; + comptime std.debug.assert(is_bun_platform); switch (p.options.module_type) { .cjs => { diff --git a/test/js/node/module/node-module-module.test.js b/test/js/node/module/node-module-module.test.js index 5ac48d4263..c3d9a24be5 100644 --- a/test/js/node/module/node-module-module.test.js +++ b/test/js/node/module/node-module-module.test.js @@ -6,7 +6,7 @@ import path from "path"; test("builtinModules exists", () => { expect(Array.isArray(builtinModules)).toBe(true); - expect(builtinModules).toHaveLength(76); + expect(builtinModules).toHaveLength(77); }); test("isBuiltin() works", () => { diff --git a/test/js/node/stubs.test.js b/test/js/node/stubs.test.js index 057215c4f0..5830895d1a 100644 --- a/test/js/node/stubs.test.js +++ b/test/js/node/stubs.test.js @@ -1,4 +1,4 @@ -import { describe, expect, test } from "bun:test"; +import { expect, test } from "bun:test"; const weirdInternalSpecifiers = [ "_http_agent", @@ -93,3 +93,8 @@ for (let specifier of specifiers) { } }); } + +test("you can import bun:test", async () => { + const bunTest1 = await import("bun:test" + String("")); + const bunTest2 = require("bun:test" + String("")); +});