mirror of
https://github.com/oven-sh/bun
synced 2026-02-03 07:28:53 +00:00
Compare commits
4 Commits
dylan/byte
...
jarred/iso
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe20b81dd0 | ||
|
|
157f9d4993 | ||
|
|
bf9beb923c | ||
|
|
f71a508601 |
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
177
src/bun.js/test/TestCleanupHandler.zig
Normal file
177
src/bun.js/test/TestCleanupHandler.zig
Normal 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;
|
||||
}
|
||||
};
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
15
src/cli.zig
15
src/cli.zig
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user