aboutsummaryrefslogtreecommitdiff
path: root/src/bun.js
diff options
context:
space:
mode:
authorGravatar Jarred Sumner <jarred@jarredsumner.com> 2023-08-06 06:30:23 -0700
committerGravatar GitHub <noreply@github.com> 2023-08-06 06:30:23 -0700
commit14624454196370e08309d4f0b0463b494e4df9ca (patch)
tree538421bfffc3d804807a4ec70a1323fbcbe3416f /src/bun.js
parentecdf2ffa6c615d8a431c2919c0b9bdc4cbe2c4f0 (diff)
downloadbun-14624454196370e08309d4f0b0463b494e4df9ca.tar.gz
bun-14624454196370e08309d4f0b0463b494e4df9ca.tar.zst
bun-14624454196370e08309d4f0b0463b494e4df9ca.zip
Code coverage for `bun test` (#3975)
* WIP code coverage initial commit * almost works * one approach * Code Coverage * Update WebKit * it works but is not yet accurate * skip double ascii check * wrapper * it works but i'm not sure what to do about blocks * hide blocks for now * Update ZigSourceProvider.cpp * Create coverage.md * Update nav.ts --------- Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com>
Diffstat (limited to 'src/bun.js')
m---------src/bun.js/WebKit0
-rw-r--r--src/bun.js/bindings/CodeCoverage.cpp44
-rw-r--r--src/bun.js/bindings/InternalModuleRegistry.cpp16
-rw-r--r--src/bun.js/bindings/ZigSourceProvider.cpp41
-rw-r--r--src/bun.js/bindings/ZigSourceProvider.h4
-rw-r--r--src/bun.js/bindings/bindings.cpp9
-rw-r--r--src/bun.js/bindings/bindings.zig12
-rw-r--r--src/bun.js/bindings/headers.h1
-rw-r--r--src/bun.js/bindings/headers.zig1
-rw-r--r--src/bun.js/javascript.zig28
-rw-r--r--src/bun.js/modules/BunJSCModule.h60
-rw-r--r--src/bun.js/test/jest.zig1
12 files changed, 199 insertions, 18 deletions
diff --git a/src/bun.js/WebKit b/src/bun.js/WebKit
-Subproject 9d9172d3242b40f16fa980ead750dc9ab248065
+Subproject 74609640b2a7c5a1588b824f870d1b0ff91bfd8
diff --git a/src/bun.js/bindings/CodeCoverage.cpp b/src/bun.js/bindings/CodeCoverage.cpp
new file mode 100644
index 000000000..1cb3b6ba2
--- /dev/null
+++ b/src/bun.js/bindings/CodeCoverage.cpp
@@ -0,0 +1,44 @@
+#include "root.h"
+#include "ZigSourceProvider.h"
+#include <JavaScriptCore/ControlFlowProfiler.h>
+
+using namespace JSC;
+
+extern "C" bool CodeCoverage__withBlocksAndFunctions(
+ JSC::VM* vmPtr,
+ JSC::SourceID sourceID,
+ void* ctx,
+ bool ignoreSourceMap,
+ void (*blockCallback)(void* ctx, JSC::BasicBlockRange* range, size_t len, size_t functionOffset, bool ignoreSourceMap))
+{
+
+ VM& vm = *vmPtr;
+
+ auto basicBlocks = vm.controlFlowProfiler()->getBasicBlocksForSourceIDWithoutFunctionRange(
+ sourceID, vm);
+
+ if (basicBlocks.isEmpty()) {
+ blockCallback(ctx, nullptr, 0, 0, ignoreSourceMap);
+ return true;
+ }
+
+ size_t functionStartOffset = basicBlocks.size();
+
+ const Vector<std::tuple<bool, unsigned, unsigned>>& functionRanges = vm.functionHasExecutedCache()->getFunctionRanges(sourceID);
+
+ basicBlocks.reserveCapacity(functionRanges.size() + basicBlocks.size());
+
+ for (const auto& functionRange : functionRanges) {
+ BasicBlockRange range;
+ range.m_hasExecuted = std::get<0>(functionRange);
+ range.m_startOffset = static_cast<int>(std::get<1>(functionRange));
+ range.m_endOffset = static_cast<int>(std::get<2>(functionRange));
+ range.m_executionCount = range.m_hasExecuted
+ ? 1
+ : 0; // This is a hack. We don't actually count this.
+ basicBlocks.append(range);
+ }
+
+ blockCallback(ctx, basicBlocks.data(), basicBlocks.size(), functionStartOffset, ignoreSourceMap);
+ return true;
+}
diff --git a/src/bun.js/bindings/InternalModuleRegistry.cpp b/src/bun.js/bindings/InternalModuleRegistry.cpp
index e6b574d7b..841360502 100644
--- a/src/bun.js/bindings/InternalModuleRegistry.cpp
+++ b/src/bun.js/bindings/InternalModuleRegistry.cpp
@@ -12,6 +12,20 @@
namespace Bun {
+extern "C" bool BunTest__shouldGenerateCodeCoverage(BunString sourceURL);
+extern "C" void ByteRangeMapping__generate(BunString sourceURL, BunString code, int sourceID);
+
+static void maybeAddCodeCoverage(JSC::VM& vm, const JSC::SourceCode& code)
+{
+#ifdef BUN_DEBUG
+ bool isCodeCoverageEnabled = !!vm.controlFlowProfiler();
+ bool shouldGenerateCodeCoverage = isCodeCoverageEnabled && BunTest__shouldGenerateCodeCoverage(Bun::toString(code.provider()->sourceURL()));
+ if (shouldGenerateCodeCoverage) {
+ ByteRangeMapping__generate(Bun::toString(code.provider()->sourceURL()), Bun::toString(code.provider()->source().toStringWithoutCopying()), code.provider()->asID());
+ }
+#endif
+}
+
// The `INTERNAL_MODULE_REGISTRY_GENERATE` macro handles inlining code to compile and run a
// JS builtin that acts as a module. In debug mode, we use a different implementation that reads
// from the developer's filesystem. This allows reloading code without recompiling bindings.
@@ -20,7 +34,7 @@ namespace Bun {
auto throwScope = DECLARE_THROW_SCOPE(vm); \
auto&& origin = SourceOrigin(WTF::URL(makeString("builtin://"_s, moduleName))); \
SourceCode source = JSC::makeSource(SOURCE, origin, moduleName); \
- \
+ maybeAddCodeCoverage(vm, source); \
JSFunction* func \
= JSFunction::create( \
vm, \
diff --git a/src/bun.js/bindings/ZigSourceProvider.cpp b/src/bun.js/bindings/ZigSourceProvider.cpp
index 2c448b5a6..d11c748da 100644
--- a/src/bun.js/bindings/ZigSourceProvider.cpp
+++ b/src/bun.js/bindings/ZigSourceProvider.cpp
@@ -58,11 +58,48 @@ static SourceOrigin toSourceOrigin(const String& sourceURL, bool isBuiltin)
return SourceOrigin(WTF::URL::fileURLWithFileSystemPath(sourceURL));
}
+void forEachSourceProvider(const WTF::Function<void(JSC::SourceID)>& func)
+{
+ // if (sourceProviderMap == nullptr) {
+ // return;
+ // }
+
+ // for (auto& pair : *sourceProviderMap) {
+ // auto sourceProvider = pair.value;
+ // if (sourceProvider) {
+ // func(sourceProvider);
+ // }
+ // }
+}
+extern "C" int ByteRangeMapping__getSourceID(void* mappings, BunString sourceURL);
+extern "C" void* ByteRangeMapping__find(BunString sourceURL);
+void* sourceMappingForSourceURL(const WTF::String& sourceURL)
+{
+ return ByteRangeMapping__find(Bun::toString(sourceURL));
+}
+
+extern "C" void ByteRangeMapping__generate(BunString sourceURL, BunString code, int sourceID);
+
+JSC::SourceID sourceIDForSourceURL(const WTF::String& sourceURL)
+{
+ void* mappings = ByteRangeMapping__find(Bun::toString(sourceURL));
+ if (!mappings) {
+ return 0;
+ }
+
+ return ByteRangeMapping__getSourceID(mappings, Bun::toString(sourceURL));
+}
+
+extern "C" bool BunTest__shouldGenerateCodeCoverage(BunString sourceURL);
+
Ref<SourceProvider> SourceProvider::create(Zig::GlobalObject* globalObject, ResolvedSource resolvedSource, JSC::SourceProviderSourceType sourceType, bool isBuiltin)
{
auto stringImpl = Bun::toWTFString(resolvedSource.source_code);
auto sourceURLString = toStringCopy(resolvedSource.source_url);
+ bool isCodeCoverageEnabled = !!globalObject->vm().controlFlowProfiler();
+
+ bool shouldGenerateCodeCoverage = isCodeCoverageEnabled && !isBuiltin && BunTest__shouldGenerateCodeCoverage(Bun::toString(sourceURLString));
auto provider = adoptRef(*new SourceProvider(
globalObject->isThreadLocalDefaultGlobalObject ? globalObject : nullptr,
@@ -71,6 +108,10 @@ Ref<SourceProvider> SourceProvider::create(Zig::GlobalObject* globalObject, Reso
sourceURLString.impl(), TextPosition(),
sourceType));
+ if (shouldGenerateCodeCoverage) {
+ ByteRangeMapping__generate(Bun::toString(provider->sourceURL()), Bun::toString(provider->source().toStringWithoutCopying()), provider->asID());
+ }
+
return provider;
}
diff --git a/src/bun.js/bindings/ZigSourceProvider.h b/src/bun.js/bindings/ZigSourceProvider.h
index c189cc454..364e6ee23 100644
--- a/src/bun.js/bindings/ZigSourceProvider.h
+++ b/src/bun.js/bindings/ZigSourceProvider.h
@@ -22,6 +22,10 @@ namespace Zig {
class GlobalObject;
+void forEachSourceProvider(WTF::Function<void(JSC::SourceID)>);
+JSC::SourceID sourceIDForSourceURL(const WTF::String& sourceURL);
+void* sourceMappingForSourceURL(const WTF::String& sourceURL);
+
class SourceProvider final : public JSC::SourceProvider {
WTF_MAKE_FAST_ALLOCATED;
using Base = JSC::SourceProvider;
diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp
index b0a291c2e..201fc0959 100644
--- a/src/bun.js/bindings/bindings.cpp
+++ b/src/bun.js/bindings/bindings.cpp
@@ -4552,3 +4552,12 @@ CPP_DECL void JSC__JSMap__set(JSC__JSMap* map, JSC__JSGlobalObject* arg1, JSC__J
{
map->set(arg1, JSC::JSValue::decode(JSValue2), JSC::JSValue::decode(JSValue3));
}
+
+CPP_DECL void JSC__VM__setControlFlowProfiler(JSC__VM* vm, bool isEnabled)
+{
+ if (isEnabled) {
+ vm->enableControlFlowProfiler();
+ } else {
+ vm->disableControlFlowProfiler();
+ }
+} \ No newline at end of file
diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig
index 4f533b9d9..3b6116a0d 100644
--- a/src/bun.js/bindings/bindings.zig
+++ b/src/bun.js/bindings/bindings.zig
@@ -102,8 +102,9 @@ pub const ZigString = extern struct {
};
pub fn fromBytes(slice_: []const u8) ZigString {
- if (!strings.isAllASCII(slice_))
- return fromUTF8(slice_);
+ if (!strings.isAllASCII(slice_)) {
+ return initUTF8(slice_);
+ }
return init(slice_);
}
@@ -4978,6 +4979,7 @@ pub const VM = extern struct {
SmallHeap = 0,
LargeHeap = 1,
};
+
pub fn create(heap_type: HeapType) *VM {
return cppFn("create", .{@intFromEnum(heap_type)});
}
@@ -4986,6 +4988,10 @@ pub const VM = extern struct {
return cppFn("deinit", .{ vm, global_object });
}
+ pub fn setControlFlowProfiler(vm: *VM, enabled: bool) void {
+ return cppFn("setControlFlowProfiler", .{ vm, enabled });
+ }
+
pub fn isJITEnabled() bool {
return cppFn("isJITEnabled", .{});
}
@@ -5093,7 +5099,7 @@ pub const VM = extern struct {
return cppFn("blockBytesAllocated", .{vm});
}
- pub const Extern = [_][]const u8{ "collectAsync", "externalMemorySize", "blockBytesAllocated", "heapSize", "releaseWeakRefs", "throwError", "deferGC", "holdAPILock", "runGC", "generateHeapSnapshot", "isJITEnabled", "deleteAllCode", "create", "deinit", "setExecutionForbidden", "executionForbidden", "isEntered", "throwError", "drainMicrotasks", "whenIdle", "shrinkFootprint", "setExecutionTimeLimit", "clearExecutionTimeLimit" };
+ pub const Extern = [_][]const u8{ "setControlFlowProfiler", "collectAsync", "externalMemorySize", "blockBytesAllocated", "heapSize", "releaseWeakRefs", "throwError", "deferGC", "holdAPILock", "runGC", "generateHeapSnapshot", "isJITEnabled", "deleteAllCode", "create", "deinit", "setExecutionForbidden", "executionForbidden", "isEntered", "throwError", "drainMicrotasks", "whenIdle", "shrinkFootprint", "setExecutionTimeLimit", "clearExecutionTimeLimit" };
};
pub const ThrowScope = extern struct {
diff --git a/src/bun.js/bindings/headers.h b/src/bun.js/bindings/headers.h
index 05c708a48..9a6d1b72a 100644
--- a/src/bun.js/bindings/headers.h
+++ b/src/bun.js/bindings/headers.h
@@ -428,6 +428,7 @@ CPP_DECL bool JSC__VM__isEntered(JSC__VM* arg0);
CPP_DECL bool JSC__VM__isJITEnabled();
CPP_DECL void JSC__VM__releaseWeakRefs(JSC__VM* arg0);
CPP_DECL JSC__JSValue JSC__VM__runGC(JSC__VM* arg0, bool arg1);
+CPP_DECL void JSC__VM__setControlFlowProfiler(JSC__VM* arg0, bool arg1);
CPP_DECL void JSC__VM__setExecutionForbidden(JSC__VM* arg0, bool arg1);
CPP_DECL void JSC__VM__setExecutionTimeLimit(JSC__VM* arg0, double arg1);
CPP_DECL void JSC__VM__shrinkFootprint(JSC__VM* arg0);
diff --git a/src/bun.js/bindings/headers.zig b/src/bun.js/bindings/headers.zig
index fbca33a30..d39793c07 100644
--- a/src/bun.js/bindings/headers.zig
+++ b/src/bun.js/bindings/headers.zig
@@ -322,6 +322,7 @@ pub extern fn JSC__VM__isEntered(arg0: *bindings.VM) bool;
pub extern fn JSC__VM__isJITEnabled(...) bool;
pub extern fn JSC__VM__releaseWeakRefs(arg0: *bindings.VM) void;
pub extern fn JSC__VM__runGC(arg0: *bindings.VM, arg1: bool) JSC__JSValue;
+pub extern fn JSC__VM__setControlFlowProfiler(arg0: *bindings.VM, arg1: bool) void;
pub extern fn JSC__VM__setExecutionForbidden(arg0: *bindings.VM, arg1: bool) void;
pub extern fn JSC__VM__setExecutionTimeLimit(arg0: *bindings.VM, arg1: f64) void;
pub extern fn JSC__VM__shrinkFootprint(arg0: *bindings.VM) void;
diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig
index 62b00cf42..1c8d91d52 100644
--- a/src/bun.js/javascript.zig
+++ b/src/bun.js/javascript.zig
@@ -128,6 +128,7 @@ pub fn OpaqueWrap(comptime Context: type, comptime Function: fn (this: *Context)
pub const bun_file_import_path = "/node_modules.server.bun";
const SourceMap = @import("../sourcemap/sourcemap.zig");
+const ParsedSourceMap = SourceMap.Mapping.ParsedSourceMap;
const MappingList = SourceMap.Mapping.List;
pub const SavedSourceMap = struct {
@@ -138,7 +139,7 @@ pub const SavedSourceMap = struct {
data: [*]u8,
pub fn vlq(this: SavedMappings) []u8 {
- return this.data[16..this.len()];
+ return this.data[24..this.len()];
}
pub inline fn len(this: SavedMappings) usize {
@@ -149,12 +150,13 @@ pub const SavedSourceMap = struct {
default_allocator.free(this.data[0..this.len()]);
}
- pub fn toMapping(this: SavedMappings, allocator: Allocator, path: string) anyerror!MappingList {
+ pub fn toMapping(this: SavedMappings, allocator: Allocator, path: string) anyerror!ParsedSourceMap {
const result = SourceMap.Mapping.parse(
allocator,
- this.data[16..this.len()],
+ this.data[24..this.len()],
@as(usize, @bitCast(this.data[8..16].*)),
1,
+ @as(usize, @bitCast(this.data[16..24].*)),
);
switch (result) {
.fail => |fail| {
@@ -183,7 +185,7 @@ pub const SavedSourceMap = struct {
}
};
- pub const Value = TaggedPointerUnion(.{ MappingList, SavedMappings });
+ pub const Value = TaggedPointerUnion(.{ ParsedSourceMap, SavedMappings });
pub const HashTable = std.HashMap(u64, *anyopaque, IdentityContext(u64), 80);
/// This is a pointer to the map located on the VirtualMachine struct
@@ -203,8 +205,8 @@ pub const SavedSourceMap = struct {
var entry = try this.map.getOrPut(bun.hash(source.path.text));
if (entry.found_existing) {
var value = Value.from(entry.value_ptr.*);
- if (value.get(MappingList)) |source_map_| {
- var source_map: *MappingList = source_map_;
+ if (value.get(ParsedSourceMap)) |source_map_| {
+ var source_map: *ParsedSourceMap = source_map_;
source_map.deinit(default_allocator);
} else if (value.get(SavedMappings)) |saved_mappings| {
var saved = SavedMappings{ .data = @as([*]u8, @ptrCast(saved_mappings)) };
@@ -216,16 +218,16 @@ pub const SavedSourceMap = struct {
entry.value_ptr.* = Value.init(bun.cast(*SavedMappings, mappings.list.items.ptr)).ptr();
}
- pub fn get(this: *SavedSourceMap, path: string) ?MappingList {
+ pub fn get(this: *SavedSourceMap, path: string) ?ParsedSourceMap {
var mapping = this.map.getEntry(bun.hash(path)) orelse return null;
switch (Value.from(mapping.value_ptr.*).tag()) {
- (@field(Value.Tag, @typeName(MappingList))) => {
- return Value.from(mapping.value_ptr.*).as(MappingList).*;
+ Value.Tag.ParsedSourceMap => {
+ return Value.from(mapping.value_ptr.*).as(ParsedSourceMap).*;
},
Value.Tag.SavedMappings => {
- var saved = SavedMappings{ .data = @as([*]u8, @ptrCast(Value.from(mapping.value_ptr.*).as(MappingList))) };
+ var saved = SavedMappings{ .data = @as([*]u8, @ptrCast(Value.from(mapping.value_ptr.*).as(ParsedSourceMap))) };
defer saved.deinit();
- var result = default_allocator.create(MappingList) catch unreachable;
+ var result = default_allocator.create(ParsedSourceMap) catch unreachable;
result.* = saved.toMapping(default_allocator, path) catch {
_ = this.map.remove(mapping.key_ptr.*);
return null;
@@ -246,8 +248,8 @@ pub const SavedSourceMap = struct {
this.mutex.lock();
defer this.mutex.unlock();
- var mappings = this.get(path) orelse return null;
- return SourceMap.Mapping.find(mappings, line, column);
+ const parsed_mappings = this.get(path) orelse return null;
+ return SourceMap.Mapping.find(parsed_mappings.mappings, line, column);
}
};
const uws = @import("root").bun.uws;
diff --git a/src/bun.js/modules/BunJSCModule.h b/src/bun.js/modules/BunJSCModule.h
index d7548dcdf..73823e16e 100644
--- a/src/bun.js/modules/BunJSCModule.h
+++ b/src/bun.js/modules/BunJSCModule.h
@@ -28,14 +28,17 @@
#include "wtf/text/WTFString.h"
#include "Process.h"
-
+#include <JavaScriptCore/SourceProviderCache.h>
#if ENABLE(REMOTE_INSPECTOR)
#include "JavaScriptCore/RemoteInspectorServer.h"
#endif
#include "JSDOMConvertBase.h"
+#include "ZigSourceProvider.h"
#include "mimalloc.h"
+#include "JavaScriptCore/ControlFlowProfiler.h"
+
using namespace JSC;
using namespace WTF;
using namespace WebCore;
@@ -650,6 +653,60 @@ JSC_DEFINE_HOST_FUNCTION(functionDeserialize, (JSGlobalObject * globalObject,
RELEASE_AND_RETURN(throwScope, JSValue::encode(result));
}
+extern "C" EncodedJSValue ByteRangeMapping__findExecutedLines(
+ JSC::JSGlobalObject *, BunString sourceURL, BasicBlockRange *ranges,
+ size_t len, size_t functionOffset, bool ignoreSourceMap);
+
+JSC_DEFINE_HOST_FUNCTION(functionCodeCoverageForFile,
+ (JSGlobalObject * globalObject,
+ CallFrame *callFrame)) {
+ VM &vm = globalObject->vm();
+ auto throwScope = DECLARE_THROW_SCOPE(vm);
+
+ String fileName = callFrame->argument(0).toWTFString(globalObject);
+ RETURN_IF_EXCEPTION(throwScope, encodedJSValue());
+ bool ignoreSourceMap = callFrame->argument(1).toBoolean(globalObject);
+
+ auto sourceID = Zig::sourceIDForSourceURL(fileName);
+ if (!sourceID) {
+ throwException(globalObject, throwScope,
+ createError(globalObject, "No source for file"_s));
+ return JSValue::encode(jsUndefined());
+ }
+
+ auto basicBlocks =
+ vm.controlFlowProfiler()->getBasicBlocksForSourceIDWithoutFunctionRange(
+ sourceID, vm);
+
+ if (basicBlocks.isEmpty()) {
+ return JSC::JSValue::encode(
+ JSC::constructEmptyArray(globalObject, nullptr, 0));
+ }
+
+ size_t functionStartOffset = basicBlocks.size();
+
+ const Vector<std::tuple<bool, unsigned, unsigned>> &functionRanges =
+ vm.functionHasExecutedCache()->getFunctionRanges(sourceID);
+
+ basicBlocks.reserveCapacity(functionRanges.size() + basicBlocks.size());
+
+ for (const auto &functionRange : functionRanges) {
+ BasicBlockRange range;
+ range.m_hasExecuted = std::get<0>(functionRange);
+ range.m_startOffset = static_cast<int>(std::get<1>(functionRange));
+ range.m_endOffset = static_cast<int>(std::get<2>(functionRange));
+ range.m_executionCount =
+ range.m_hasExecuted
+ ? 1
+ : 0; // This is a hack. We don't actually count this.
+ basicBlocks.append(range);
+ }
+
+ return ByteRangeMapping__findExecutedLines(
+ globalObject, Bun::toString(fileName), basicBlocks.data(),
+ basicBlocks.size(), functionStartOffset, ignoreSourceMap);
+}
+
// clang-format off
/* Source for BunJSCModuleTable.lut.h
@begin BunJSCModuleTable
@@ -718,6 +775,7 @@ DEFINE_NATIVE_MODULE(BunJSC)
putNativeFn(Identifier::fromString(vm, "getProtectedObjects"_s), functionGetProtectedObjects);
putNativeFn(Identifier::fromString(vm, "generateHeapSnapshotForDebugging"_s), functionGenerateHeapSnapshotForDebugging);
putNativeFn(Identifier::fromString(vm, "profile"_s), functionRunProfiler);
+ putNativeFn(Identifier::fromString(vm, "codeCoverageForFile"_s), functionCodeCoverageForFile);
putNativeFn(Identifier::fromString(vm, "setTimeZone"_s), functionSetTimeZone);
putNativeFn(Identifier::fromString(vm, "serialize"_s), functionSerialize);
putNativeFn(Identifier::fromString(vm, "deserialize"_s), functionDeserialize);
diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig
index aacf671ce..8691e5a2d 100644
--- a/src/bun.js/test/jest.zig
+++ b/src/bun.js/test/jest.zig
@@ -89,6 +89,7 @@ pub const TestRunner = struct {
test_timeout_timer: ?*bun.uws.Timer = null,
last_test_timeout_timer_duration: u32 = 0,
active_test_for_timeout: ?TestRunner.Test.ID = null,
+ test_options: *const bun.CLI.Command.TestOptions = undefined,
global_callbacks: struct {
beforeAll: std.ArrayListUnmanaged(JSC.JSValue) = .{},