Compare commits

...

4 Commits

Author SHA1 Message Date
Jarred Sumner
fe20b81dd0 Update TestCleanupHandler.zig 2024-04-12 17:42:52 -07:00
Jarred Sumner
157f9d4993 Update TestCleanupHandler.zig 2024-04-12 03:48:04 -07:00
Jarred Sumner
bf9beb923c a little DRYer 2024-04-12 03:47:32 -07:00
Jarred Sumner
f71a508601 Implement --isolate=lite in bun test 2024-04-12 03:42:33 -07:00
13 changed files with 484 additions and 88 deletions

View File

@@ -2814,12 +2814,13 @@ pub fn serve(
globalObject: *JSC.JSGlobalObject,
callframe: *JSC.CallFrame,
) callconv(.C) JSC.JSValue {
const vm = globalObject.bunVM();
const arguments = callframe.arguments(2).slice();
var config: JSC.API.ServerConfig = brk: {
var exception_ = [1]JSC.JSValueRef{null};
const exception = &exception_;
var args = JSC.Node.ArgumentsSlice.init(globalObject.bunVM(), arguments);
var args = JSC.Node.ArgumentsSlice.init(vm, arguments);
const config_ = JSC.API.ServerConfig.fromJS(globalObject.ptr(), &args, exception);
if (exception[0] != null) {
globalObject.throwValue(exception_[0].?.value());
@@ -2836,7 +2837,7 @@ pub fn serve(
var exception_value: *JSC.JSValue = undefined;
if (config.allow_hot) {
if (globalObject.bunVM().hotMap()) |hot| {
if (vm.hotMap()) |hot| {
if (config.id.len == 0) {
config.id = config.computeID(globalObject.allocator());
}
@@ -2889,10 +2890,13 @@ pub fn serve(
server.thisObject = obj;
if (config.allow_hot) {
if (globalObject.bunVM().hotMap()) |hot| {
if (vm.hotMap()) |hot| {
hot.insert(config.id, server);
}
}
if (vm.resourceCleaner()) |cleaner| {
cleaner.add(server);
}
return obj;
} else {
var server = JSC.API.HTTPSServer.init(config, globalObject.ptr());
@@ -2910,10 +2914,14 @@ pub fn serve(
server.thisObject = obj;
if (config.allow_hot) {
if (globalObject.bunVM().hotMap()) |hot| {
if (vm.hotMap()) |hot| {
hot.insert(config.id, server);
}
}
if (vm.resourceCleaner()) |cleaner| {
cleaner.add(server);
}
return obj;
}
} else {
@@ -2933,10 +2941,13 @@ pub fn serve(
server.thisObject = obj;
if (config.allow_hot) {
if (globalObject.bunVM().hotMap()) |hot| {
if (vm.hotMap()) |hot| {
hot.insert(config.id, server);
}
}
if (vm.resourceCleaner()) |cleaner| {
cleaner.add(server);
}
return obj;
} else {
var server = JSC.API.HTTPServer.init(config, globalObject.ptr());
@@ -2955,10 +2966,13 @@ pub fn serve(
server.thisObject = obj;
if (config.allow_hot) {
if (globalObject.bunVM().hotMap()) |hot| {
if (vm.hotMap()) |hot| {
hot.insert(config.id, server);
}
}
if (vm.resourceCleaner()) |cleaner| {
cleaner.add(server);
}
return obj;
}
}

View File

@@ -792,7 +792,8 @@ pub const Listener = struct {
const Socket = NewSocket(ssl);
bun.assert(ssl == listener.ssl);
var this_socket = listener.handlers.vm.allocator.create(Socket) catch @panic("Out of memory");
const vm = listener.handlers.vm;
var this_socket = vm.allocator.create(Socket) catch bun.outOfMemory();
this_socket.* = Socket{
.handlers = &listener.handlers,
.this_value = .zero,
@@ -800,6 +801,9 @@ pub const Listener = struct {
.protos = listener.protos,
.owned_protos = false,
};
if (vm.resourceCleaner()) |cleaner| {
cleaner.add(this_socket);
}
if (listener.strong_data.get()) |default_data| {
const globalObject = listener.handlers.globalObject;
Socket.dataSetCached(this_socket.getThisValue(globalObject), globalObject, default_data);
@@ -821,15 +825,14 @@ pub const Listener = struct {
// uws.us_socket_context_add_server_name
// }
pub fn stop(this: *Listener, _: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSValue {
const arguments = callframe.arguments(1);
pub fn doStop(this: *Listener, all: bool) void {
log("close", .{});
if (arguments.len > 0 and arguments.ptr[0].isBoolean() and arguments.ptr[0].toBoolean() and this.socket_context != null) {
if (all and this.socket_context != null) {
this.socket_context.?.close(this.ssl);
this.listener = null;
} else {
var listener = this.listener orelse return JSValue.jsUndefined();
var listener = this.listener orelse return;
this.listener = null;
listener.close(this.ssl);
}
@@ -843,8 +846,13 @@ pub const Listener = struct {
this.strong_self.clear();
this.strong_data.clear();
}
}
return JSValue.jsUndefined();
pub fn stop(this: *Listener, _: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSValue {
const arguments = callframe.arguments(1);
this.doStop(arguments.len > 0 and arguments.ptr[0].isBoolean() and arguments.ptr[0].toBoolean());
return .undefined;
}
pub fn finalize(this: *Listener) callconv(.C) void {
@@ -852,10 +860,18 @@ pub const Listener = struct {
this.deinit();
}
pub fn onCleanup(this: *Listener, _: *JSC.VirtualMachine) void {
this.doStop(true);
}
pub fn deinit(this: *Listener) void {
const vm = this.handlers.vm;
if (vm.resourceCleaner()) |cleaner| {
cleaner.remove(this);
}
this.strong_self.deinit();
this.strong_data.deinit();
this.poll_ref.unref(this.handlers.vm);
this.poll_ref.unref(vm);
bun.assert(this.listener == null);
bun.assert(this.handlers.active_connections == 0);
this.handlers.unprotect();
@@ -1294,9 +1310,19 @@ fn NewSocket(comptime ssl: bool) type {
this.handleConnectError(errno);
}
pub fn onCleanup(this: *This, _: *JSC.VirtualMachine) void {
if (this.is_active) {
// markInactive does .detached = true
this.markInactive();
}
}
pub fn markActive(this: *This) void {
if (!this.is_active) {
this.handlers.markActive();
if (this.handlers.vm.resourceCleaner()) |cleaner| {
cleaner.add(this);
}
this.is_active = true;
this.has_pending_activity.store(true, .Release);
}
@@ -1318,6 +1344,10 @@ fn NewSocket(comptime ssl: bool) type {
this.is_active = false;
const vm = this.handlers.vm;
if (vm.resourceCleaner()) |cleaner| {
cleaner.remove(this);
}
this.handlers.markInactive(ssl, this.socket.context(), this.wrapped);
this.poll_ref.unref(vm);
this.has_pending_activity.store(false, .Release);
@@ -1977,6 +2007,7 @@ fn NewSocket(comptime ssl: bool) type {
pub fn finalize(this: *This) callconv(.C) void {
log("finalize() {d}", .{@intFromPtr(this)});
if (!this.detached) {
this.detached = true;
if (!this.socket.isClosed()) {

View File

@@ -651,6 +651,14 @@ pub const Subprocess = struct {
this.updateHasPendingActivity();
}
pub fn onCleanup(this: *Subprocess, _: *JSC.VirtualMachine) void {
log("onCleanupInTest({*})", .{this});
_ = this.tryKill(1);
this.closeIO(.stdin);
this.closeIO(.stdout);
this.closeIO(.stderr);
}
pub fn doSend(this: *Subprocess, global: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue {
const ipc_data = &(this.ipc_data orelse {
if (this.hasExited()) {
@@ -1484,8 +1492,12 @@ pub const Subprocess = struct {
// access it after it's been freed We cannot call any methods which
// access GC'd values during the finalizer
this.this_jsvalue = .zero;
const vm = JSC.VirtualMachine.get();
if (vm.resourceCleaner()) |cleaner| {
cleaner.remove(this);
}
bun.assert(!this.hasPendingActivity() or JSC.VirtualMachine.get().isShuttingDown());
bun.assert(!this.hasPendingActivity() or vm.isShuttingDown());
this.finalizeStreams();
this.process.detach();
@@ -2148,15 +2160,24 @@ pub const Subprocess = struct {
should_close_memfd = false;
if (comptime !is_sync) {
if (jsc_vm.resourceCleaner()) |cleaner| {
if (!subprocess.hasExited()) {
cleaner.add(subprocess);
}
}
return out;
}
if (comptime is_sync) {
switch (subprocess.process.watch(jsc_vm)) {
.result => {},
.err => {
subprocess.process.wait(true);
},
switch (subprocess.process.watch(jsc_vm)) {
.result => {},
.err => {
subprocess.process.wait(true);
},
}
if (jsc_vm.resourceCleaner()) |cleaner| {
if (!subprocess.hasExited()) {
cleaner.add(subprocess);
}
}

View File

@@ -5577,6 +5577,9 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp
pub fn finalize(this: *ThisServer) callconv(.C) void {
httplog("finalize", .{});
this.flags.has_js_deinited = true;
if (this.vm.resourceCleaner()) |cleaner| {
cleaner.remove(this);
}
this.deinitIfWeCan();
}
@@ -5632,6 +5635,10 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp
if (this.flags.deinit_scheduled)
return;
this.flags.deinit_scheduled = true;
const vm = this.vm;
if (vm.resourceCleaner()) |cleaner| {
cleaner.remove(this);
}
httplog("scheduleDeinit", .{});
if (!this.flags.terminated) {
@@ -5641,7 +5648,7 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp
const task = bun.default_allocator.create(JSC.AnyTask) catch unreachable;
task.* = JSC.AnyTask.New(ThisServer, deinit).init(this);
this.vm.enqueueTask(JSC.Task.init(task));
vm.enqueueTask(JSC.Task.init(task));
}
pub fn deinit(this: *ThisServer) void {
@@ -5799,13 +5806,16 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp
return;
}
pub fn onCleanup(this: *ThisServer, _: *JSC.VirtualMachine) void {
this.stop(false);
}
pub fn onListen(this: *ThisServer, socket: ?*App.ListenSocket) void {
if (socket == null) {
return this.onListenFailed();
}
this.listener = socket;
this.vm.event_loop_handle = Async.Loop.get();
if (!ssl_enabled_)
this.vm.addListeningSocketForWatchMode(socket.?.socket().fd());
}

View File

@@ -105,37 +105,6 @@ extern "C" JSC::EncodedJSValue JSMock__jsSetSystemTime(JSC::JSGlobalObject* glob
uint64_t JSMockModule::s_nextInvocationId = 0;
// This is taken from JSWeakSet
// We only want to hold onto the list of active spies which haven't already been collected
// So we use a WeakSet
// Unlike using WeakSet from JS, we are able to iterate through the WeakSet.
class ActiveSpySet final : public WeakMapImpl<WeakMapBucket<WeakMapBucketDataKey>> {
public:
using Base = WeakMapImpl<WeakMapBucket<WeakMapBucketDataKey>>;
DECLARE_EXPORT_INFO;
static Structure* createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype)
{
return Structure::create(vm, globalObject, prototype, TypeInfo(JSWeakSetType, StructureFlags), info());
}
static ActiveSpySet* create(VM& vm, Structure* structure)
{
ActiveSpySet* instance = new (NotNull, allocateCell<ActiveSpySet>(vm)) ActiveSpySet(vm, structure);
instance->finishCreation(vm);
return instance;
}
private:
ActiveSpySet(VM& vm, Structure* structure)
: Base(vm, structure)
{
}
};
static_assert(std::is_final<ActiveSpySet>::value, "Required for JSType based casting");
const ClassInfo ActiveSpySet::s_info = { "ActiveSpySet"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(ActiveSpySet) };
class JSMockImplementation final : public JSNonFinalObject {
public:
@@ -450,6 +419,70 @@ void JSMockFunction::visitChildrenImpl(JSCell* cell, Visitor& visitor)
}
DEFINE_VISIT_CHILDREN(JSMockFunction);
// This is taken from JSWeakSet
// We only want to hold onto the list of active spies which haven't already been collected
// So we use a WeakSet
// Unlike using WeakSet from JS, we are able to iterate through the WeakSet.
class ActiveSpySet final : public WeakMapImpl<WeakMapBucket<WeakMapBucketDataKey>> {
public:
using Base = WeakMapImpl<WeakMapBucket<WeakMapBucketDataKey>>;
DECLARE_EXPORT_INFO;
static Structure* createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype)
{
return Structure::create(vm, globalObject, prototype, TypeInfo(JSWeakSetType, StructureFlags), info());
}
static ActiveSpySet* create(VM& vm, Structure* structure)
{
ActiveSpySet* instance = new (NotNull, allocateCell<ActiveSpySet>(vm)) ActiveSpySet(vm, structure);
instance->finishCreation(vm);
return instance;
}
void clearAll() {
MarkedArgumentBuffer active;
this->takeSnapshot(active);
size_t size = active.size();
for (size_t i = 0; i < size; ++i) {
JSValue spy = active.at(i);
if (!spy.isObject())
continue;
auto* spyObject = jsCast<JSMockFunction*>(spy);
spyObject->clearSpy();
}
}
void restoreAll() {
MarkedArgumentBuffer active;
this->takeSnapshot(active);
size_t size = active.size();
for (size_t i = 0; i < size; ++i) {
JSValue spy = active.at(i);
if (!spy.isObject())
continue;
auto* spyObject = jsCast<JSMockFunction*>(spy);
spyObject->clear();
}
}
private:
ActiveSpySet(VM& vm, Structure* structure)
: Base(vm, structure)
{
}
};
static_assert(std::is_final<ActiveSpySet>::value, "Required for JSType based casting");
const ClassInfo ActiveSpySet::s_info = { "ActiveSpySet"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(ActiveSpySet) };
static void pushImpl(JSMockFunction* fn, JSGlobalObject* jsGlobalObject, JSMockImplementation::Kind kind, JSValue value)
{
Zig::GlobalObject* globalObject = jsCast<Zig::GlobalObject*>(jsGlobalObject);
@@ -567,18 +600,7 @@ extern "C" void JSMock__resetSpies(Zig::GlobalObject* globalObject)
auto spiesValue = globalObject->mockModule.activeSpies.get();
ActiveSpySet* activeSpies = jsCast<ActiveSpySet*>(spiesValue);
MarkedArgumentBuffer active;
activeSpies->takeSnapshot(active);
size_t size = active.size();
for (size_t i = 0; i < size; ++i) {
JSValue spy = active.at(i);
if (!spy.isObject())
continue;
auto* spyObject = jsCast<JSMockFunction*>(spy);
spyObject->clearSpy();
}
activeSpies->clearAll();
globalObject->mockModule.activeSpies.clear();
}
@@ -596,20 +618,7 @@ extern "C" void JSMock__clearAllMocks(Zig::GlobalObject* globalObject)
auto spiesValue = globalObject->mockModule.activeMocks.get();
ActiveSpySet* activeSpies = jsCast<ActiveSpySet*>(spiesValue);
MarkedArgumentBuffer active;
activeSpies->takeSnapshot(active);
size_t size = active.size();
for (size_t i = 0; i < size; ++i) {
JSValue spy = active.at(i);
if (!spy.isObject())
continue;
auto* spyObject = jsCast<JSMockFunction*>(spy);
// seems similar to what we do in JSMock__resetSpies,
// but we actually only clear calls, context, instances and results
spyObject->clear();
}
activeSpies->restoreAll();
}
extern "C" JSC::EncodedJSValue JSMock__jsClearAllMocks(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callframe)

View File

@@ -559,6 +559,11 @@ pub const VirtualMachine = struct {
after_event_loop_callback_ctx: ?*anyopaque = null,
after_event_loop_callback: ?OpaqueCallback = null,
__test_cleaner: JSC.TestCleanupHandler = .{},
// This pointer is only set when the handler is not null
test_cleaner: ?*JSC.TestCleanupHandler = null,
is_test_cleaner_enabled: bool = false,
/// The arguments used to launch the process _after_ the script name and bun and any flags applied to Bun
/// "bun run foo --bar"
/// ["--bar"]
@@ -883,6 +888,16 @@ pub const VirtualMachine = struct {
};
}
pub fn ensureTestCleaner(this: *VirtualMachine) void {
if (this.test_cleaner == null) {
this.test_cleaner = &this.__test_cleaner;
}
}
pub inline fn resourceCleaner(this: *VirtualMachine) ?*JSC.TestCleanupHandler {
return this.test_cleaner;
}
pub inline fn rareData(this: *VirtualMachine) *JSC.RareData {
return this.rare_data orelse brk: {
this.rare_data = this.allocator.create(JSC.RareData) catch unreachable;
@@ -2261,6 +2276,10 @@ pub const VirtualMachine = struct {
}
if (!this.bundler.options.disable_transpilation) {
const cleaner = this.test_cleaner;
this.test_cleaner = null;
defer this.test_cleaner = cleaner;
if (try this.loadPreloads()) |promise| {
JSC.JSValue.fromCell(promise).ensureStillAlive();
JSC.JSValue.fromCell(promise).protect();
@@ -2293,6 +2312,9 @@ pub const VirtualMachine = struct {
}
if (!this.bundler.options.disable_transpilation) {
const cleaner = this.test_cleaner;
this.test_cleaner = null;
defer this.test_cleaner = cleaner;
if (try this.loadPreloads()) |promise| {
JSC.JSValue.fromCell(promise).ensureStillAlive();
this.pending_internal_promise = promise;
@@ -3776,3 +3798,5 @@ pub fn NewHotReloader(comptime Ctx: type, comptime EventLoopType: type, comptime
}
pub export var isBunTest: bool = false;
pub const TestCleanupHandler = @import("./test/TestCleanupHandler.zig").TestCleanupHandler;

View File

@@ -0,0 +1,177 @@
const std = @import("std");
const bun = @import("root").bun;
const default_allocator = bun.default_allocator;
const string = bun.string;
const MutableString = bun.MutableString;
const strings = bun.strings;
const Output = bun.Output;
const jest = bun.JSC.Jest;
const Jest = jest.Jest;
const TestRunner = jest.TestRunner;
const DescribeScope = jest.DescribeScope;
const JSC = bun.JSC;
const VirtualMachine = JSC.VirtualMachine;
const JSGlobalObject = JSC.JSGlobalObject;
const JSValue = JSC.JSValue;
const JSInternalPromise = JSC.JSInternalPromise;
const JSPromise = JSC.JSPromise;
const JSType = JSValue.JSType;
const JSError = JSC.JSError;
const JSObject = JSC.JSObject;
const CallFrame = JSC.CallFrame;
const ZigString = JSC.ZigString;
const Environment = bun.Environment;
const Cleanable = packed struct {
ptr: Type = Type.Null,
pub fn init(ptr: anytype) Cleanable {
return Cleanable{ .ptr = Type.init(ptr) };
}
const Subprocess = bun.JSC.Subprocess;
const TLSSocket = bun.JSC.API.TLSSocket;
const TCPSocket = bun.JSC.API.TCPSocket;
const Listener = bun.JSC.API.Listener;
const HTTPServer = JSC.API.HTTPServer;
const HTTPSServer = JSC.API.HTTPSServer;
const DebugHTTPServer = JSC.API.DebugHTTPServer;
const DebugHTTPSServer = JSC.API.DebugHTTPSServer;
const ShellSubprocess = bun.shell.ShellSubprocess;
pub const Type = bun.TaggedPointerUnion(.{
Subprocess,
TLSSocket,
TCPSocket,
Listener,
HTTPServer,
HTTPSServer,
DebugHTTPServer,
DebugHTTPSServer,
ShellSubprocess,
});
const Tag = Type.Tag;
const name = bun.meta.typeName;
pub fn run(this: *Cleanable, vm: *JSC.VirtualMachine) void {
switch (this.ptr.tag()) {
inline @field(Tag, name(Subprocess)),
@field(Tag, name(TLSSocket)),
@field(Tag, name(TCPSocket)),
@field(Tag, name(Listener)),
@field(Tag, name(HTTPServer)),
@field(Tag, name(HTTPSServer)),
@field(Tag, name(DebugHTTPServer)),
@field(Tag, name(DebugHTTPSServer)),
@field(Tag, name(ShellSubprocess)),
=> |tag| {
const resource = this.ptr.as(
Type.typeFromTag(
@intFromEnum(
@field(
Tag,
@tagName(tag),
),
),
),
);
this.ptr = Type.Null;
resource.onCleanup(vm);
},
else => {
bun.assert(false);
},
}
}
};
pub const TestCleanupHandler = struct {
cleanables: std.AutoArrayHashMapUnmanaged(Cleanable, u32) = .{},
generation: u32 = 0,
state: State = .none,
const State = enum {
none,
waiting,
running,
};
pub usingnamespace bun.New(@This());
pub fn add(this: *TestCleanupHandler, ptr: anytype) void {
_ = this.cleanables.getOrPutValue(bun.default_allocator, Cleanable.init(ptr), this.generation) catch bun.outOfMemory();
}
pub fn remove(this: *TestCleanupHandler, ptr: anytype) void {
const cleanable = Cleanable.init(ptr);
_ = this.cleanables.swapRemove(cleanable);
}
pub fn runAllAfter(this: *TestCleanupHandler, target: u32, vm: *JSC.VirtualMachine) void {
vm.test_cleaner = null;
const cleanables = this.cleanables.keys();
const generations = this.cleanables.values();
var i: usize = 0;
for (generations, cleanables, 0..) |gen, *cleanable, idx| {
if (target <= gen) {
if (!cleanable.ptr.isNull())
cleanable.run(vm);
} else {
i = idx;
}
}
this.cleanables.shrinkRetainingCapacity(i);
}
pub fn runAll(this: *TestCleanupHandler, vm: *JSC.VirtualMachine) void {
const cleanables = this.cleanables;
this.cleanables = .{};
const ptrs = cleanables.keys();
for (ptrs) |*cleanable| {
if (!cleanable.ptr.isNull())
cleanable.run(vm);
}
}
pub fn run(this: *TestCleanupHandler, expected_generation: u32, vm: *JSC.VirtualMachine) void {
bun.assert(this.state != .running); // must not be re-entrant.
this.state = .running;
const cleanables = this.cleanables.keys();
const generations = this.cleanables.values();
vm.test_cleaner = null;
var i: usize = 0;
var last_i: usize = 0;
while (i < this.cleanables.count()) {
const generation = generations[i];
if (generation != expected_generation) {
i += 1;
last_i = i;
continue;
}
if (!cleanables[i].ptr.isNull()) {
cleanables[i].run(vm);
}
i += 1;
}
this.cleanables.shrinkRetainingCapacity(last_i);
}
pub fn beginCycle(this: *TestCleanupHandler, vm: *JSC.VirtualMachine) u32 {
bun.assert(this.state != .running);
this.state = .waiting;
vm.test_cleaner = this;
return this.generation;
}
pub fn endCycle(this: *TestCleanupHandler, generation: u32, vm: *JSC.VirtualMachine) void {
this.run(generation, vm);
this.state = .none;
if (generation == this.generation)
this.generation +%= 1;
}
};

View File

@@ -1254,6 +1254,7 @@ pub const TestRunnerTask = struct {
source_file_path: string = "",
needs_before_each: bool = true,
ref: JSC.Ref = JSC.Ref.init(),
test_cleanup_generation_number: u32 = 0,
done_callback_state: AsyncState = .none,
promise_state: AsyncState = .none,
@@ -1309,10 +1310,10 @@ pub const TestRunnerTask = struct {
const tag = if (!describe.shouldEvaluateScope()) describe.tag else test_.tag;
switch (tag) {
.todo => {
this.processTestResult(globalThis, .{ .todo = {} }, test_, test_id, describe);
this.processTestResult(globalThis, .{ .todo = {} }, test_, test_id, describe, jsc_vm);
},
.skip => {
this.processTestResult(globalThis, .{ .skip = {} }, test_, test_id, describe);
this.processTestResult(globalThis, .{ .skip = {} }, test_, test_id, describe, jsc_vm);
},
else => {},
}
@@ -1334,7 +1335,12 @@ pub const TestRunnerTask = struct {
}
this.sync_state = .pending;
if (jsc_vm.is_test_cleaner_enabled) {
jsc_vm.ensureTestCleaner();
if (jsc_vm.resourceCleaner()) |cleaner| {
this.test_cleanup_generation_number = cleaner.beginCycle(jsc_vm);
}
}
var result = TestScope.run(&test_, this);
// rejected promises should fail the test
@@ -1427,15 +1433,17 @@ pub const TestRunnerTask = struct {
var describe = this.describe;
describe.tests.items[test_id] = test_;
const vm = this.globalThis.bunVM();
if (comptime from == .timeout) {
const err = this.globalThis.createErrorInstance("Test {} timed out after {d}ms", .{ bun.fmt.quote(test_.label), test_.timeout_millis });
this.globalThis.bunVM().onUnhandledError(this.globalThis, err);
vm.onUnhandledError(this.globalThis, err);
}
processTestResult(this, this.globalThis, result, test_, test_id, describe);
processTestResult(this, this.globalThis, result, test_, test_id, describe, vm);
}
fn processTestResult(this: *TestRunnerTask, globalThis: *JSC.JSGlobalObject, result: Result, test_: TestScope, test_id: u32, describe: *DescribeScope) void {
fn processTestResult(this: *TestRunnerTask, globalThis: *JSC.JSGlobalObject, result: Result, test_: TestScope, test_id: u32, describe: *DescribeScope, vm: *JSC.VirtualMachine) void {
switch (result.forceTODO(test_.tag == .todo)) {
.pass => |count| Jest.runner.?.reportPass(
test_id,
@@ -1477,6 +1485,9 @@ pub const TestRunnerTask = struct {
},
.pending => @panic("Unexpected pending test"),
}
if (vm.resourceCleaner()) |cleaner| {
cleaner.endCycle(this.test_cleanup_generation_number, vm);
}
describe.onTestComplete(globalThis, test_id, result == .skip);
Jest.runner.?.runNextTest();
}

View File

@@ -240,6 +240,7 @@ pub const Arguments = struct {
const test_only_params = [_]ParamType{
clap.parseParam("--timeout <NUMBER> Set the per-test timeout in milliseconds, default is 5000.") catch unreachable,
clap.parseParam("--update-snapshots Update snapshot files") catch unreachable,
clap.parseParam("--isolate <STR> 'light' or 'none' (default). Experimental. Automatically cleanup pending I/O after each test.") catch unreachable,
clap.parseParam("--rerun-each <NUMBER> Re-run each test file <NUMBER> times, helps catch certain bugs") catch unreachable,
clap.parseParam("--only Only run tests that are marked with \"test.only()\"") catch unreachable,
clap.parseParam("--todo Include tests that are marked with \"test.todo()\"") catch unreachable,
@@ -438,6 +439,17 @@ pub const Arguments = struct {
}
}
if (args.option("--isolate")) |isolate| {
if (strings.eqlComptime(isolate, "lite")) {
ctx.test_options.isolate = .lite;
} else if (strings.eqlComptime(isolate, "none")) {
ctx.test_options.isolate = .none;
} else {
Output.prettyErrorln("<r><red>error<r>: Invalid isolate mode: \"{s}\", must be \"lite\" or \"none\"", .{isolate});
Global.exit(1);
}
}
if (!ctx.test_options.coverage.enabled) {
ctx.test_options.coverage.enabled = args.flag("--coverage");
}
@@ -1113,8 +1125,11 @@ pub const Command = struct {
run_todo: bool = false,
only: bool = false,
bail: u32 = 0,
isolate: Isolate = .none,
coverage: TestCommand.CodeCoverageOptions = .{},
test_filter_regex: ?*RegularExpression = null,
pub const Isolate = enum { none, lite };
};
pub const Debugger = union(enum) {

View File

@@ -87,6 +87,7 @@ pub const CommandLineReporter = struct {
failures_to_repeat_buf: std.ArrayListUnmanaged(u8) = .{},
skips_to_repeat_buf: std.ArrayListUnmanaged(u8) = .{},
todos_to_repeat_buf: std.ArrayListUnmanaged(u8) = .{},
current_cleanup_generation_id: u32 = 0,
pub const Summary = struct {
pass: u32 = 0,
@@ -659,7 +660,9 @@ pub const TestCommand = struct {
vm.preload = ctx.preloads;
vm.bundler.options.rewrite_jest_for_tests = true;
vm.bundler.options.env.behavior = .load_all_without_inlining;
if (ctx.test_options.isolate == .lite) {
vm.is_test_cleaner_enabled = true;
}
const node_env_entry = try env_loader.map.getOrPutWithoutValue("NODE_ENV");
if (!node_env_entry.found_existing) {
node_env_entry.key_ptr.* = try env_loader.allocator.dupe(u8, node_env_entry.key_ptr.*);
@@ -934,6 +937,12 @@ pub const TestCommand = struct {
Output.prettyError("\n", .{});
Output.flush();
if (vm.is_test_cleaner_enabled) {
if (vm.resourceCleaner()) |cleaner| {
cleaner.runAllAfter(0, vm);
}
}
if (vm.hot_reload == .watch) {
vm.eventLoop().tickPossiblyForever();
@@ -1029,6 +1038,8 @@ pub const TestCommand = struct {
const repeat_count = reporter.repeat_count;
var repeat_index: u32 = 0;
const is_test_cleaner_enabled = vm.is_test_cleaner_enabled;
while (repeat_index < repeat_count) : (repeat_index += 1) {
if (repeat_count > 1) {
Output.prettyErrorln("<r>\n{s}{s}: <d>(run #{d})<r>\n", .{ file_prefix, file_title, repeat_index + 1 });
@@ -1037,6 +1048,24 @@ pub const TestCommand = struct {
}
Output.flush();
var initial_cleanup_generation_id: u32 = reporter.current_cleanup_generation_id;
if (is_test_cleaner_enabled) {
vm.ensureTestCleaner();
initial_cleanup_generation_id = vm.resourceCleaner().?.beginCycle(vm);
reporter.current_cleanup_generation_id = initial_cleanup_generation_id;
}
errdefer {
if (is_test_cleaner_enabled) {
const id = reporter.current_cleanup_generation_id;
if (id != std.math.maxInt(u32) and id == initial_cleanup_generation_id) {
reporter.current_cleanup_generation_id = std.math.maxInt(u32);
vm.ensureTestCleaner();
vm.resourceCleaner().?.runAllAfter(id, vm);
}
}
}
var promise = try vm.loadEntryPointForTestRunner(file_path);
reporter.summary.files += 1;
@@ -1045,6 +1074,15 @@ pub const TestCommand = struct {
vm.onUnhandledError(vm.global, promise.result(vm.global.vm()));
reporter.summary.fail += 1;
if (is_test_cleaner_enabled) {
const id = reporter.current_cleanup_generation_id;
if (id == initial_cleanup_generation_id and id != std.math.maxInt(u32)) {
reporter.current_cleanup_generation_id = std.math.maxInt(u32);
vm.ensureTestCleaner();
vm.resourceCleaner().?.runAllAfter(id, vm);
}
}
if (reporter.jest.bail == reporter.summary.fail) {
reporter.printSummary();
Output.prettyError("\nBailed out after {d} failure{s}<r>\n", .{ reporter.jest.bail, if (reporter.jest.bail == 1) "" else "s" });
@@ -1118,6 +1156,15 @@ pub const TestCommand = struct {
vm.global.deleteModuleRegistryEntry(&entry);
}
if (is_test_cleaner_enabled) {
const id = reporter.current_cleanup_generation_id;
if (id == initial_cleanup_generation_id and id != std.math.maxInt(u32)) {
reporter.current_cleanup_generation_id = std.math.maxInt(u32);
vm.ensureTestCleaner();
vm.resourceCleaner().?.runAllAfter(id, vm);
}
}
if (Output.is_github_action) {
Output.prettyErrorln("<r>\n::endgroup::\n", .{});
Output.flush();

View File

@@ -507,6 +507,15 @@ pub const ShellSubprocess = struct {
// }
}
pub fn onCleanup(this: *@This(), _: *JSC.VirtualMachine) void {
if (this.hasExited()) {
return;
}
_ = this.tryKill(0);
this.unref(true);
}
pub fn hasKilled(this: *const @This()) bool {
return this.process.hasKilled();
}
@@ -556,6 +565,11 @@ pub const ShellSubprocess = struct {
// This must only be run once per Subprocess
pub fn finalizeSync(this: *@This()) void {
if (this.event_loop == .js) {
if (this.event_loop.js.virtual_machine.resourceCleaner()) |cleaner| {
cleaner.remove(this);
}
}
this.closeProcess();
// this.closeIO(.stdin);
@@ -901,6 +915,14 @@ pub const ShellSubprocess = struct {
// process has already exited
// https://cs.github.com/libuv/libuv/blob/b00d1bd225b602570baee82a6152eaa823a84fa6/src/unix/process.c#L1007
subprocess.wait(subprocess.flags.is_sync);
} else {
if (!subprocess.hasExited()) {
if (event_loop == .js) {
if (event_loop.js.virtual_machine.resourceCleaner()) |cleaner| {
cleaner.add(subprocess);
}
}
}
}
}

View File

@@ -100,7 +100,7 @@ pub fn TaggedPointerUnion(comptime Types: anytype) type {
const TagType: type = result.tag_type;
return struct {
return packed struct {
pub const Tag = TagType;
pub const TagInt = TagSize;
pub const type_map: TypeMap(Types) = result.ty_map;

View File

@@ -1,5 +1,5 @@
import { file, gc, Serve, serve, Server } from "bun";
import { afterEach, describe, it, expect, afterAll } from "bun:test";
import { afterEach, describe, it, expect, afterAll, beforeAll } from "bun:test";
import { readFileSync, writeFileSync } from "fs";
import { join, resolve } from "path";
import { bunExe, bunEnv, dumpStats } from "harness";
@@ -19,6 +19,21 @@ afterEach(() => {
const count = 200;
let server: Server | undefined;
beforeAll(() => {
try {
server = serve({
fetch() {
return new Response();
},
port: 0,
});
} catch (e: any) {
console.log("catch:", e);
if (e?.message !== `Failed to start server `) {
throw e;
}
}
});
async function runTest({ port, ...serverOptions }: Serve<any>, test: (server: Server) => Promise<void> | void) {
if (server) {