mirror of
https://github.com/oven-sh/bun
synced 2026-02-03 07:28:53 +00:00
Compare commits
6 Commits
dylan/pyth
...
jarred/byt
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bfe7e711eb | ||
|
|
dc03f0283c | ||
|
|
8195aa4c96 | ||
|
|
ad638a6bea | ||
|
|
22176cb9bb | ||
|
|
cf3caba3dc |
@@ -447,6 +447,8 @@ pub const SocketConfig = struct {
|
||||
}
|
||||
};
|
||||
|
||||
const UnixOrHost = uws.UnixOrHost;
|
||||
|
||||
pub const Listener = struct {
|
||||
pub const log = Output.scoped(.Listener, false);
|
||||
|
||||
@@ -481,46 +483,6 @@ pub const Listener = struct {
|
||||
return true;
|
||||
}
|
||||
|
||||
const UnixOrHost = union(enum) {
|
||||
unix: []const u8,
|
||||
host: struct {
|
||||
host: []const u8,
|
||||
port: u16,
|
||||
},
|
||||
fd: bun.FileDescriptor,
|
||||
|
||||
pub fn clone(this: UnixOrHost) UnixOrHost {
|
||||
switch (this) {
|
||||
.unix => |u| {
|
||||
return .{
|
||||
.unix = (bun.default_allocator.dupe(u8, u) catch bun.outOfMemory()),
|
||||
};
|
||||
},
|
||||
.host => |h| {
|
||||
return .{
|
||||
.host = .{
|
||||
.host = (bun.default_allocator.dupe(u8, h.host) catch bun.outOfMemory()),
|
||||
.port = this.host.port,
|
||||
},
|
||||
};
|
||||
},
|
||||
.fd => |f| return .{ .fd = f },
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deinit(this: UnixOrHost) void {
|
||||
switch (this) {
|
||||
.unix => |u| {
|
||||
bun.default_allocator.free(u);
|
||||
},
|
||||
.host => |h| {
|
||||
bun.default_allocator.free(h.host);
|
||||
},
|
||||
.fd => {}, // this is an integer
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub fn reload(this: *Listener, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSValue {
|
||||
const args = callframe.arguments(1);
|
||||
|
||||
@@ -594,7 +556,7 @@ pub const Listener = struct {
|
||||
|
||||
const socket_context = uws.us_create_bun_socket_context(
|
||||
@intFromBool(ssl_enabled),
|
||||
uws.Loop.get(),
|
||||
vm.uwsLoop(),
|
||||
@sizeOf(usize),
|
||||
ctx_opts,
|
||||
) orelse {
|
||||
@@ -656,7 +618,7 @@ pub const Listener = struct {
|
||||
);
|
||||
}
|
||||
|
||||
var connection: Listener.UnixOrHost = if (port) |port_| .{
|
||||
var connection: UnixOrHost = if (port) |port_| .{
|
||||
.host = .{ .host = (hostname_or_unix.cloneIfNeeded(bun.default_allocator) catch bun.outOfMemory()).slice(), .port = port_ },
|
||||
} else .{
|
||||
.unix = (hostname_or_unix.cloneIfNeeded(bun.default_allocator) catch bun.outOfMemory()).slice(),
|
||||
@@ -993,7 +955,7 @@ pub const Listener = struct {
|
||||
return .zero;
|
||||
};
|
||||
|
||||
const connection: Listener.UnixOrHost = blk: {
|
||||
const connection: UnixOrHost = blk: {
|
||||
if (opts.getTruthy(globalObject, "fd")) |fd_| {
|
||||
if (fd_.isNumber()) {
|
||||
const fd = fd_.asFileDescriptor();
|
||||
@@ -1152,7 +1114,7 @@ fn NewSocket(comptime ssl: bool) type {
|
||||
this_value: JSC.JSValue = .zero,
|
||||
poll_ref: Async.KeepAlive = Async.KeepAlive.init(),
|
||||
last_4: [4]u8 = .{ 0, 0, 0, 0 },
|
||||
connection: ?Listener.UnixOrHost = null,
|
||||
connection: ?UnixOrHost = null,
|
||||
protos: ?[]const u8,
|
||||
server_name: ?[]const u8 = null,
|
||||
|
||||
@@ -1189,7 +1151,7 @@ fn NewSocket(comptime ssl: bool) type {
|
||||
return this.has_pending_activity.load(.acquire);
|
||||
}
|
||||
|
||||
pub fn doConnect(this: *This, connection: Listener.UnixOrHost) !void {
|
||||
pub fn doConnect(this: *This, connection: UnixOrHost) !void {
|
||||
bun.assert(this.socket_context != null);
|
||||
switch (connection) {
|
||||
.host => |c| {
|
||||
|
||||
@@ -523,6 +523,7 @@ pub const RuntimeTranspilerStore = struct {
|
||||
.dont_bundle_twice = true,
|
||||
.allow_commonjs = true,
|
||||
.inject_jest_globals = bundler.options.rewrite_jest_for_tests and is_main,
|
||||
.inline_loc_for_tests = bundler.options.has_byte_range_filter_for_tests and is_main,
|
||||
.set_breakpoint_on_first_line = vm.debugger != null and
|
||||
vm.debugger.?.set_breakpoint_on_first_line and
|
||||
is_main and
|
||||
@@ -1664,6 +1665,7 @@ pub const ModuleLoader = struct {
|
||||
.dont_bundle_twice = true,
|
||||
.allow_commonjs = true,
|
||||
.inject_jest_globals = jsc_vm.bundler.options.rewrite_jest_for_tests and is_main,
|
||||
.inline_loc_for_tests = jsc_vm.bundler.options.has_byte_range_filter_for_tests and is_main,
|
||||
.keep_json_and_toml_as_one_statement = true,
|
||||
.set_breakpoint_on_first_line = is_main and
|
||||
jsc_vm.debugger != null and
|
||||
|
||||
@@ -67,6 +67,7 @@ pub const TestRunner = struct {
|
||||
run_todo: bool = false,
|
||||
last_file: u64 = 0,
|
||||
bail: u32 = 0,
|
||||
byte_range_filter: []const logger.Range = &.{},
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
callback: *Callback = undefined,
|
||||
@@ -106,6 +107,26 @@ pub const TestRunner = struct {
|
||||
|
||||
pub const Drainer = JSC.AnyTask.New(TestRunner, drain);
|
||||
|
||||
pub fn isOutsideByteRangeFilter(this: *const TestRunner, byte_range: logger.Range) bool {
|
||||
if (byte_range.len == 0 or this.byte_range_filter.len == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const start_test_range = byte_range.loc.start;
|
||||
const end_test_range = byte_range.end().start;
|
||||
for (this.byte_range_filter) |range| {
|
||||
const user_provided_start = range.loc.start;
|
||||
const user_provided_end = range.end().start;
|
||||
|
||||
// Check if the ranges overlap
|
||||
if (start_test_range <= user_provided_end and end_test_range >= user_provided_start) {
|
||||
return false; // Ranges overlap
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn onTestTimeout(this: *TestRunner, now: *const bun.timespec, vm: *VirtualMachine) void {
|
||||
_ = vm; // autofix
|
||||
this.event_loop_timer.state = .FIRED;
|
||||
@@ -170,6 +191,12 @@ pub const TestRunner = struct {
|
||||
if (this.only) {
|
||||
return;
|
||||
}
|
||||
|
||||
// byte range filter overrides only
|
||||
if (this.byte_range_filter.len > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.only = true;
|
||||
|
||||
const list = this.queue.readableSlice(0);
|
||||
@@ -185,7 +212,7 @@ pub const TestRunner = struct {
|
||||
|
||||
pub const Callback = struct {
|
||||
pub const OnUpdateCount = *const fn (this: *Callback, delta: u32, total: u32) void;
|
||||
pub const OnTestStart = *const fn (this: *Callback, test_id: Test.ID) void;
|
||||
pub const OnTestStart = *const fn (this: *Callback, test_id: Test.ID, file: string, label: string, byte_range: logger.Range, parent: ?*DescribeScope) void;
|
||||
pub const OnTestUpdate = *const fn (this: *Callback, test_id: Test.ID, file: string, label: string, expectations: u32, elapsed_ns: u64, parent: ?*DescribeScope) void;
|
||||
onUpdateCount: OnUpdateCount,
|
||||
onTestStart: OnTestStart,
|
||||
@@ -195,6 +222,10 @@ pub const TestRunner = struct {
|
||||
onTestTodo: OnTestUpdate,
|
||||
};
|
||||
|
||||
pub fn reportStart(this: *TestRunner, test_id: Test.ID, file: string, label: string, byte_range: logger.Range, parent: ?*DescribeScope) void {
|
||||
this.callback.onTestStart(this.callback, test_id, file, label, byte_range, parent);
|
||||
}
|
||||
|
||||
pub fn reportPass(this: *TestRunner, test_id: Test.ID, file: string, label: string, expectations: u32, elapsed_ns: u64, parent: ?*DescribeScope) void {
|
||||
this.tests.items(.status)[test_id] = .pass;
|
||||
this.callback.onTestPass(this.callback, test_id, file, label, expectations, elapsed_ns, parent);
|
||||
@@ -272,6 +303,7 @@ pub const TestRunner = struct {
|
||||
|
||||
pub const Jest = struct {
|
||||
pub var runner: ?*TestRunner = null;
|
||||
pub var is_byte_range_filter_enabled: bool = false;
|
||||
|
||||
fn globalHook(comptime name: string) JSC.JSHostFunctionType {
|
||||
return struct {
|
||||
@@ -590,6 +622,8 @@ pub const TestScope = struct {
|
||||
tag: Tag = .pass,
|
||||
snapshot_count: usize = 0,
|
||||
|
||||
byte_range: logger.Range = .{},
|
||||
|
||||
// null if the test does not set a timeout
|
||||
timeout_millis: u32 = std.math.maxInt(u32),
|
||||
|
||||
@@ -831,6 +865,8 @@ pub const DescribeScope = struct {
|
||||
done: bool = false,
|
||||
skip_count: u32 = 0,
|
||||
tag: Tag = .pass,
|
||||
byte_range: logger.Range = .{},
|
||||
reported: bool = false,
|
||||
|
||||
fn isWithinOnlyScope(this: *const DescribeScope) bool {
|
||||
if (this.tag == .only) return true;
|
||||
@@ -1380,13 +1416,15 @@ pub const TestRunnerTask = struct {
|
||||
var test_: TestScope = this.describe.tests.items[test_id];
|
||||
describe.current_test_id = test_id;
|
||||
|
||||
if (test_.func == .zero or !describe.shouldEvaluateScope() or (test_.tag != .only and Jest.runner.?.only)) {
|
||||
if (test_.func == .zero or !describe.shouldEvaluateScope() or (test_.tag != .only and Jest.runner.?.only) or Jest.runner.?.isOutsideByteRangeFilter(test_.byte_range)) {
|
||||
const tag = if (!describe.shouldEvaluateScope()) describe.tag else test_.tag;
|
||||
switch (tag) {
|
||||
.todo => {
|
||||
Jest.runner.?.callback.onTestStart(Jest.runner.?.callback, test_id, this.source_file_path, test_.label, test_.byte_range, describe);
|
||||
this.processTestResult(globalThis, .{ .todo = {} }, test_, test_id, describe);
|
||||
},
|
||||
.skip => {
|
||||
Jest.runner.?.callback.onTestStart(Jest.runner.?.callback, test_id, this.source_file_path, test_.label, test_.byte_range, describe);
|
||||
this.processTestResult(globalThis, .{ .skip = {} }, test_, test_id, describe);
|
||||
},
|
||||
else => {},
|
||||
@@ -1397,6 +1435,7 @@ pub const TestRunnerTask = struct {
|
||||
|
||||
jsc_vm.onUnhandledRejectionCtx = this;
|
||||
jsc_vm.onUnhandledRejection = onUnhandledRejection;
|
||||
Jest.runner.?.callback.onTestStart(Jest.runner.?.callback, test_id, this.source_file_path, test_.label, test_.byte_range, describe);
|
||||
|
||||
if (this.needs_before_each) {
|
||||
this.needs_before_each = false;
|
||||
@@ -1410,7 +1449,6 @@ pub const TestRunnerTask = struct {
|
||||
}
|
||||
|
||||
this.sync_state = .pending;
|
||||
|
||||
var result = TestScope.run(&test_, this);
|
||||
|
||||
if (this.describe.tests.items.len > test_id) {
|
||||
@@ -1689,18 +1727,15 @@ inline fn createScope(
|
||||
comptime tag: Tag,
|
||||
) JSValue {
|
||||
const this = callframe.this();
|
||||
const arguments = callframe.arguments(3);
|
||||
const args = arguments.slice();
|
||||
const arguments = callframe.arguments(4);
|
||||
var description, var function, var options, var test_range_value = arguments.ptr;
|
||||
const args_len = arguments.len;
|
||||
|
||||
if (args.len == 0) {
|
||||
if (args_len == 0) {
|
||||
globalThis.throwPretty("{s} expects a description or function", .{signature});
|
||||
return .zero;
|
||||
}
|
||||
|
||||
var description = args[0];
|
||||
var function = if (args.len > 1) args[1] else .zero;
|
||||
var options = if (args.len > 2) args[2] else .zero;
|
||||
|
||||
if (description.isEmptyOrUndefinedOrNull() or !description.isString()) {
|
||||
function = description;
|
||||
description = .zero;
|
||||
@@ -1713,9 +1748,33 @@ inline fn createScope(
|
||||
}
|
||||
}
|
||||
|
||||
var test_range = logger.Range.None;
|
||||
|
||||
if (comptime tag == .pass or tag == .only) {
|
||||
// Handle the byte ranges inserted by the transpiler.
|
||||
// Let's not run any of this if it's not enabled.
|
||||
if (Jest.is_byte_range_filter_enabled) {
|
||||
if (options.isArray() and args_len > 2) {
|
||||
test_range_value = options;
|
||||
options = .zero;
|
||||
}
|
||||
|
||||
if (test_range_value.isArray() and test_range_value.getLength(globalThis) == 2) {
|
||||
test_range = logger.Range{
|
||||
.loc = .{ .start = test_range_value.getDirectIndex(globalThis, 0).coerceToInt32(globalThis) },
|
||||
.len = test_range_value.getDirectIndex(globalThis, 1).coerceToInt32(globalThis),
|
||||
};
|
||||
}
|
||||
|
||||
if (globalThis.hasException()) {
|
||||
return .zero;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var timeout_ms: u32 = std.math.maxInt(u32);
|
||||
if (options.isNumber()) {
|
||||
timeout_ms = @as(u32, @intCast(@max(args[2].coerce(i32, globalThis), 0)));
|
||||
timeout_ms = @as(u32, @intCast(@max(options.coerce(i32, globalThis), 0)));
|
||||
} else if (options.isObject()) {
|
||||
if (options.get(globalThis, "timeout")) |timeout| {
|
||||
if (!timeout.isNumber()) {
|
||||
@@ -1802,6 +1861,7 @@ inline fn createScope(
|
||||
.func_arg = function_args,
|
||||
.func_has_callback = has_callback,
|
||||
.timeout_millis = timeout_ms,
|
||||
.byte_range = test_range,
|
||||
}) catch unreachable;
|
||||
} else {
|
||||
var scope = allocator.create(DescribeScope) catch unreachable;
|
||||
@@ -1810,6 +1870,7 @@ inline fn createScope(
|
||||
.parent = parent,
|
||||
.file_id = parent.file_id,
|
||||
.tag = tag_to_use,
|
||||
.byte_range = test_range,
|
||||
};
|
||||
|
||||
return scope.run(globalThis, function, &.{});
|
||||
@@ -2067,7 +2128,7 @@ fn eachBind(
|
||||
if (Jest.runner.?.filter_regex) |regex| {
|
||||
var buffer: bun.MutableString = Jest.runner.?.filter_buffer;
|
||||
buffer.reset();
|
||||
appendParentLabel(&buffer, parent) catch @panic("Bun ran out of memory while filtering tests");
|
||||
appendParentLabel(&buffer, parent) catch bun.outOfMemory();
|
||||
buffer.append(formattedLabel) catch unreachable;
|
||||
const str = bun.String.fromBytes(buffer.toOwnedSliceLeaky());
|
||||
is_skip = !regex.matches(str);
|
||||
@@ -2118,7 +2179,7 @@ inline fn createEach(
|
||||
comptime signature: string,
|
||||
comptime is_test: bool,
|
||||
) JSValue {
|
||||
const arguments = callframe.arguments(1);
|
||||
const arguments = callframe.arguments(2);
|
||||
const args = arguments.slice();
|
||||
|
||||
if (args.len == 0) {
|
||||
|
||||
@@ -1279,6 +1279,7 @@ pub const Bundler = struct {
|
||||
virtual_source: ?*const logger.Source = null,
|
||||
replace_exports: runtime.Runtime.Features.ReplaceableExport.Map = .{},
|
||||
inject_jest_globals: bool = false,
|
||||
inline_loc_for_tests: bool = false,
|
||||
set_breakpoint_on_first_line: bool = false,
|
||||
emit_decorator_metadata: bool = false,
|
||||
remove_cjs_module_wrapper: bool = false,
|
||||
@@ -1433,6 +1434,7 @@ pub const Bundler = struct {
|
||||
|
||||
opts.features.jsx_optimization_hoist = bundler.options.jsx_optimization_hoist orelse opts.features.jsx_optimization_inline;
|
||||
opts.features.inject_jest_globals = this_parse.inject_jest_globals;
|
||||
opts.features.inline_loc_for_tests = this_parse.inline_loc_for_tests;
|
||||
opts.features.minify_syntax = bundler.options.minify_syntax;
|
||||
opts.features.minify_identifiers = bundler.options.minify_identifiers;
|
||||
opts.features.dead_code_elimination = bundler.options.dead_code_elimination;
|
||||
|
||||
@@ -276,6 +276,7 @@ pub const Arguments = struct {
|
||||
clap.parseParam("--coverage-dir <STR> Directory for coverage files. Defaults to 'coverage'.") catch unreachable,
|
||||
clap.parseParam("--bail <NUMBER>? Exit the test suite after <NUMBER> failures. If you do not specify a number, it defaults to 1.") catch unreachable,
|
||||
clap.parseParam("-t, --test-name-pattern <STR> Run only tests with a name that matches the given regex.") catch unreachable,
|
||||
clap.parseParam("--listen <STR> Listen for test results on a socket") catch unreachable,
|
||||
};
|
||||
pub const test_params = test_only_params ++ runtime_params_ ++ transpiler_params_ ++ base_params_;
|
||||
|
||||
@@ -480,6 +481,13 @@ pub const Arguments = struct {
|
||||
}
|
||||
}
|
||||
|
||||
if (args.option("--listen")) |listen_address| {
|
||||
ctx.test_options.listen_address = bun.uws.UnixOrHost.parse(listen_address) orelse {
|
||||
Output.prettyErrorln("<r><red>error<r>: --listen received invalid address: \"{s}\". Must be either a unix:path, fd:file_descriptor_number, or host:port pair", .{listen_address});
|
||||
Global.exit(1);
|
||||
};
|
||||
}
|
||||
|
||||
if (args.option("--coverage-dir")) |dir| {
|
||||
ctx.test_options.coverage.reports_directory = dir;
|
||||
}
|
||||
@@ -1242,6 +1250,7 @@ pub const Command = struct {
|
||||
bail: u32 = 0,
|
||||
coverage: TestCommand.CodeCoverageOptions = .{},
|
||||
test_filter_regex: ?*RegularExpression = null,
|
||||
listen_address: ?bun.uws.UnixOrHost = null,
|
||||
};
|
||||
|
||||
pub const Debugger = union(enum) {
|
||||
|
||||
@@ -76,6 +76,321 @@ fn writeTestStatusLine(comptime status: @Type(.EnumLiteral), writer: anytype) vo
|
||||
writer.print(fmtStatusTextLine(status, false), .{}) catch unreachable;
|
||||
}
|
||||
|
||||
pub const SocketReporter = struct {
|
||||
socket: uws.SocketTCP,
|
||||
command_line: *CommandLineReporter,
|
||||
private_buffer_struct_do_not_use: std.ArrayListUnmanaged(u8) = .{},
|
||||
sent_offset: u32 = 0,
|
||||
connection_status: ConnectionStatus = .pending,
|
||||
socket_context: ?*uws.SocketContext = null,
|
||||
buffer: *std.ArrayListUnmanaged(u8) = undefined,
|
||||
|
||||
const ConnectionStatus = enum {
|
||||
pending,
|
||||
connected,
|
||||
disconnected,
|
||||
last_write_failed,
|
||||
};
|
||||
|
||||
pub usingnamespace bun.New(@This());
|
||||
|
||||
pub fn connect(reporter: *CommandLineReporter, address: *const uws.UnixOrHost) ?*SocketReporter {
|
||||
const context_options = uws.us_bun_socket_context_options_t{};
|
||||
|
||||
const socket_context: *uws.SocketContext = uws.us_create_bun_socket_context(0, uws.Loop.get(), @sizeOf(usize), context_options) orelse @panic("Failed to create socket context");
|
||||
var socket_reporter = SocketReporter.new(.{
|
||||
.command_line = reporter,
|
||||
.socket = undefined,
|
||||
.socket_context = socket_context,
|
||||
});
|
||||
socket_reporter.buffer = &socket_reporter.private_buffer_struct_do_not_use;
|
||||
|
||||
uws.SocketTCP.configure(socket_context, true, *SocketReporter, struct {
|
||||
pub const onOpen = SocketReporter.onOpen;
|
||||
pub const onClose = SocketReporter.onClose;
|
||||
pub const onData = SocketReporter.onData;
|
||||
pub const onWritable = SocketReporter.onWritable;
|
||||
pub const onTimeout = SocketReporter.onTimeout;
|
||||
pub const onLongTimeout = SocketReporter.onLongTimeout;
|
||||
pub const onConnectError = SocketReporter.onConnectError;
|
||||
pub const onEnd = SocketReporter.onEnd;
|
||||
pub const onHandshake = SocketReporter.onHandshake;
|
||||
});
|
||||
debug("connect({})", .{address.*});
|
||||
socket_reporter.socket = uws.SocketTCP.connectToAddress(address, socket_context, SocketReporter, socket_reporter, "socket") catch |err| {
|
||||
Output.err(err, "Failed to connect to test socket reporter", .{});
|
||||
return null;
|
||||
};
|
||||
|
||||
return socket_reporter;
|
||||
}
|
||||
|
||||
pub const Protocol = struct {
|
||||
pub const TestStart = struct {
|
||||
id: Test.ID,
|
||||
parent_id: u32 = std.math.maxInt(u32),
|
||||
module_id: u32,
|
||||
byte_range: logger.Range,
|
||||
label: string,
|
||||
|
||||
pub fn write(this: *const TestStart, writer: WriterContext) !void {
|
||||
try writer.writeInt(this.id);
|
||||
try writer.writeInt(this.parent_id);
|
||||
try writer.writeInt(this.module_id);
|
||||
try writer.writeInt(@intCast(@max(this.byte_range.loc.start, 0)));
|
||||
try writer.writeInt(@intCast(@max(this.byte_range.len, 0)));
|
||||
try writer.writeSlice(u8, this.label);
|
||||
}
|
||||
};
|
||||
|
||||
pub const TestEnd = struct {
|
||||
id: Test.ID,
|
||||
status: TestRunner.Test.Status,
|
||||
duration_ms: u32 = 0,
|
||||
expectation_count: u32 = 0,
|
||||
|
||||
pub fn write(this: *const TestEnd, writer: WriterContext) !void {
|
||||
try writer.writeInt(this.id);
|
||||
try writer.writeInt(@intFromEnum(this.status));
|
||||
try writer.writeInt(this.duration_ms);
|
||||
try writer.writeInt(this.expectation_count);
|
||||
}
|
||||
};
|
||||
|
||||
pub const ModuleStart = struct {
|
||||
id: u32,
|
||||
path: string,
|
||||
|
||||
pub fn write(this: *const ModuleStart, writer: WriterContext) !void {
|
||||
try writer.writeInt(this.id);
|
||||
try writer.writeSlice(u8, this.path);
|
||||
}
|
||||
};
|
||||
|
||||
pub const CoverageReport = struct {
|
||||
files: []CoverageFileReport,
|
||||
|
||||
pub fn write(this: *const CoverageReport, writer: WriterContext) !void {
|
||||
try writer.writeSlice(CoverageFileReport, this.files);
|
||||
}
|
||||
};
|
||||
|
||||
pub const CoverageFileReport = struct {
|
||||
file_path: string,
|
||||
line_ranges: []u32,
|
||||
function_ranges: []u32,
|
||||
|
||||
pub fn write(this: *const CoverageFileReport, writer: WriterContext) !void {
|
||||
try writer.writeSlice(u8, this.file_path);
|
||||
try writer.writeSlice(u32, this.line_ranges);
|
||||
try writer.writeSlice(u32, this.function_ranges);
|
||||
}
|
||||
};
|
||||
|
||||
pub const Message = union(Tag) {
|
||||
TestStart: TestStart,
|
||||
TestEnd: TestEnd,
|
||||
ModuleStart: ModuleStart,
|
||||
CoverageReport: CoverageReport,
|
||||
CoverageFileReport: CoverageFileReport,
|
||||
|
||||
pub fn write(this: *const Message, writer: WriterContext) !void {
|
||||
const byte_length_counter = try writer.byteLengthCounter();
|
||||
try writer.writeInt(@intFromEnum(@as(Tag, this.*)));
|
||||
switch (this.*) {
|
||||
inline else => |*msg| {
|
||||
try msg.write(writer);
|
||||
},
|
||||
}
|
||||
byte_length_counter.commit(writer);
|
||||
}
|
||||
|
||||
pub const Tag = enum(u32) {
|
||||
TestStart,
|
||||
TestEnd,
|
||||
ModuleStart,
|
||||
CoverageReport,
|
||||
CoverageFileReport,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
pub fn hasPendingMessages(this: *const SocketReporter) bool {
|
||||
return this.pendingData().len > 0 and switch (this.connection_status) {
|
||||
.connected => true,
|
||||
.last_write_failed => true,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn write(this: *SocketReporter, data: []const u8) !usize {
|
||||
try this.buffer.appendSlice(bun.default_allocator, data);
|
||||
return data.len;
|
||||
}
|
||||
|
||||
fn pendingData(this: *const SocketReporter) []u8 {
|
||||
return this.buffer.items[this.sent_offset..];
|
||||
}
|
||||
|
||||
pub fn flush(this: *SocketReporter) void {
|
||||
if (this.connection_status == .disconnected or this.connection_status == .pending or this.pendingData().len == 0) {
|
||||
return;
|
||||
}
|
||||
const wrote = this.socket.write(this.pendingData(), false);
|
||||
debug("flush({d})", .{this.pendingData().len});
|
||||
if (wrote > 0) {
|
||||
this.sent_offset += @intCast(wrote);
|
||||
if (this.pendingData().len == 0) {
|
||||
this.sent_offset = 0;
|
||||
this.buffer.items.len = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn writeMessage(this: *SocketReporter, message: Protocol.Message) void {
|
||||
debug("writeMessage({s})", .{@tagName(message)});
|
||||
message.write(WriterContext{ .ctx = this }) catch bun.outOfMemory();
|
||||
if (this.connection_status == .connected) {
|
||||
this.flush();
|
||||
}
|
||||
}
|
||||
|
||||
pub const WriterContext = struct {
|
||||
ctx: *SocketReporter,
|
||||
|
||||
const ByteLengthCounter = struct {
|
||||
offset: u32 = 0,
|
||||
|
||||
pub fn get(writer: WriterContext) !ByteLengthCounter {
|
||||
const offset = writer.ctx.buffer.items.len;
|
||||
try writer.writeInt(@as(u32, 0));
|
||||
return .{ .offset = @truncate(offset) };
|
||||
}
|
||||
|
||||
pub fn commit(this: ByteLengthCounter, writer: WriterContext) void {
|
||||
const offset = @as(u32, this.offset);
|
||||
const length: u32 = @truncate(writer.ctx.buffer.items.len -| @as(usize, offset));
|
||||
writer.ctx.buffer.items[offset .. offset + 4][0..4].* = @as([4]u8, @bitCast(length));
|
||||
}
|
||||
};
|
||||
|
||||
pub fn write(this: WriterContext, data: []const u8) !usize {
|
||||
return try this.ctx.write(data);
|
||||
}
|
||||
|
||||
pub fn byteLengthCounter(this: WriterContext) !ByteLengthCounter {
|
||||
return try ByteLengthCounter.get(this);
|
||||
}
|
||||
|
||||
pub fn writeInt(this: WriterContext, value: u32) !void {
|
||||
_ = try this.write(&std.mem.toBytes(value));
|
||||
}
|
||||
|
||||
pub fn writeSlice(this: WriterContext, comptime T: type, value: []const T) !void {
|
||||
try this.writeInt(@truncate(value.len));
|
||||
if (T == u8) {
|
||||
_ = try this.write(value);
|
||||
} else {
|
||||
_ = try this.write(std.mem.sliceAsBytes(value));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub fn onOpen(
|
||||
this: *SocketReporter,
|
||||
socket: uws.SocketTCP,
|
||||
) void {
|
||||
debug("onOpen()", .{});
|
||||
this.connection_status = .connected;
|
||||
this.socket = socket;
|
||||
this.flush();
|
||||
}
|
||||
|
||||
pub fn onEnd(
|
||||
this: *SocketReporter,
|
||||
socket: uws.SocketTCP,
|
||||
) void {
|
||||
debug("onEnd()", .{});
|
||||
_ = this; // autofix
|
||||
socket.close(.failure);
|
||||
}
|
||||
|
||||
pub fn onHandshake(
|
||||
_: *SocketReporter,
|
||||
_: uws.SocketTCP,
|
||||
_: i32,
|
||||
_: uws.us_bun_verify_error_t,
|
||||
) void {
|
||||
// not implemented.
|
||||
}
|
||||
|
||||
pub fn onClose(
|
||||
this: *SocketReporter,
|
||||
socket: uws.SocketTCP,
|
||||
err: c_int,
|
||||
data: ?*anyopaque,
|
||||
) void {
|
||||
debug("onClose()", .{});
|
||||
_ = socket; // autofix
|
||||
_ = err; // autofix
|
||||
_ = data; // autofix
|
||||
this.connection_status = .disconnected;
|
||||
this.buffer.deinit(bun.default_allocator);
|
||||
}
|
||||
|
||||
pub fn onData(
|
||||
this: *SocketReporter,
|
||||
socket: uws.SocketTCP,
|
||||
data: []const u8,
|
||||
) void {
|
||||
_ = this; // autofix
|
||||
_ = socket; // autofix
|
||||
_ = data; // autofix
|
||||
// do nothing, for now.
|
||||
}
|
||||
|
||||
pub fn onWritable(
|
||||
this: *SocketReporter,
|
||||
socket: uws.SocketTCP,
|
||||
) void {
|
||||
_ = socket; // autofix
|
||||
this.connection_status = .connected;
|
||||
this.flush();
|
||||
}
|
||||
pub fn onTimeout(
|
||||
this: *SocketReporter,
|
||||
socket: uws.SocketTCP,
|
||||
) void {
|
||||
_ = this; // autofix
|
||||
_ = socket; // autofix
|
||||
// do nothing
|
||||
}
|
||||
|
||||
pub fn onLongTimeout(
|
||||
this: *SocketReporter,
|
||||
socket: uws.SocketTCP,
|
||||
) void {
|
||||
_ = this; // autofix
|
||||
_ = socket; // autofix
|
||||
// do nothing
|
||||
}
|
||||
|
||||
pub fn onConnectError(
|
||||
this: *SocketReporter,
|
||||
socket: uws.SocketTCP,
|
||||
errno: c_int,
|
||||
) void {
|
||||
_ = errno; // autofix
|
||||
_ = socket; // autofix
|
||||
this.connection_status = .disconnected;
|
||||
this.buffer.clearAndFree(bun.default_allocator);
|
||||
this.sent_offset = 0;
|
||||
Output.err(error.TestReporterConnection, "Failed to connect to test socket reporter", .{});
|
||||
}
|
||||
};
|
||||
|
||||
const debug = Output.scoped(.TestCommand, false);
|
||||
|
||||
pub const CommandLineReporter = struct {
|
||||
jest: TestRunner,
|
||||
callback: TestRunner.Callback,
|
||||
@@ -88,6 +403,8 @@ pub const CommandLineReporter = struct {
|
||||
skips_to_repeat_buf: std.ArrayListUnmanaged(u8) = .{},
|
||||
todos_to_repeat_buf: std.ArrayListUnmanaged(u8) = .{},
|
||||
|
||||
socket: ?*SocketReporter = null,
|
||||
|
||||
pub const Summary = struct {
|
||||
pass: u32 = 0,
|
||||
expectations: u32 = 0,
|
||||
@@ -108,7 +425,23 @@ pub const CommandLineReporter = struct {
|
||||
|
||||
pub fn handleUpdateCount(_: *TestRunner.Callback, _: u32, _: u32) void {}
|
||||
|
||||
pub fn handleTestStart(_: *TestRunner.Callback, _: Test.ID) void {}
|
||||
pub fn handleTestStart(cb: *TestRunner.Callback, test_id: Test.ID, file: string, label: string, byte_range: logger.Range, parent: ?*jest.DescribeScope) void {
|
||||
_ = file; // autofix
|
||||
const this: *CommandLineReporter = @fieldParentPtr("callback", cb);
|
||||
if (this.socket) |socket| {
|
||||
socket.writeMessage(
|
||||
.{
|
||||
.TestStart = .{
|
||||
.id = test_id,
|
||||
.label = label,
|
||||
.parent_id = if (parent) |p| p.test_id_start else std.math.maxInt(u32),
|
||||
.module_id = if (parent) |p| p.file_id else std.math.maxInt(u32),
|
||||
.byte_range = byte_range,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn printTestLine(label: string, elapsed_ns: u64, parent: ?*jest.DescribeScope, comptime skip: bool, writer: anytype) void {
|
||||
var scopes_stack = std.BoundedArray(*jest.DescribeScope, 64).init(0) catch unreachable;
|
||||
@@ -168,13 +501,25 @@ pub const CommandLineReporter = struct {
|
||||
}
|
||||
|
||||
pub fn handleTestPass(cb: *TestRunner.Callback, id: Test.ID, _: string, label: string, expectations: u32, elapsed_ns: u64, parent: ?*jest.DescribeScope) void {
|
||||
var this: *CommandLineReporter = @fieldParentPtr("callback", cb);
|
||||
if (this.socket) |socket| {
|
||||
socket.writeMessage(
|
||||
.{
|
||||
.TestEnd = .{
|
||||
.id = id,
|
||||
.status = TestRunner.Test.Status.pass,
|
||||
.duration_ms = @truncate(elapsed_ns / std.time.ns_per_ms),
|
||||
.expectation_count = expectations,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const writer_ = Output.errorWriter();
|
||||
var buffered_writer = std.io.bufferedWriter(writer_);
|
||||
var writer = buffered_writer.writer();
|
||||
defer buffered_writer.flush() catch unreachable;
|
||||
|
||||
var this: *CommandLineReporter = @fieldParentPtr("callback", cb);
|
||||
|
||||
writeTestStatusLine(.pass, &writer);
|
||||
|
||||
printTestLine(label, elapsed_ns, parent, false, writer);
|
||||
@@ -187,6 +532,18 @@ pub const CommandLineReporter = struct {
|
||||
pub fn handleTestFail(cb: *TestRunner.Callback, id: Test.ID, _: string, label: string, expectations: u32, elapsed_ns: u64, parent: ?*jest.DescribeScope) void {
|
||||
var writer_ = Output.errorWriter();
|
||||
var this: *CommandLineReporter = @fieldParentPtr("callback", cb);
|
||||
if (this.socket) |socket| {
|
||||
socket.writeMessage(
|
||||
.{
|
||||
.TestEnd = .{
|
||||
.id = id,
|
||||
.status = TestRunner.Test.Status.fail,
|
||||
.duration_ms = @truncate(elapsed_ns / std.time.ns_per_ms),
|
||||
.expectation_count = expectations,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// when the tests fail, we want to repeat the failures at the end
|
||||
// so that you can see them better when there are lots of tests that ran
|
||||
@@ -220,6 +577,18 @@ pub const CommandLineReporter = struct {
|
||||
pub fn handleTestSkip(cb: *TestRunner.Callback, id: Test.ID, _: string, label: string, expectations: u32, elapsed_ns: u64, parent: ?*jest.DescribeScope) void {
|
||||
var writer_ = Output.errorWriter();
|
||||
var this: *CommandLineReporter = @fieldParentPtr("callback", cb);
|
||||
if (this.socket) |socket| {
|
||||
socket.writeMessage(
|
||||
.{
|
||||
.TestEnd = .{
|
||||
.id = id,
|
||||
.status = TestRunner.Test.Status.skip,
|
||||
.duration_ms = @truncate(0),
|
||||
.expectation_count = 0,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// If you do it.only, don't report the skipped tests because its pretty noisy
|
||||
if (jest.Jest.runner != null and !jest.Jest.runner.?.only) {
|
||||
@@ -261,6 +630,19 @@ pub const CommandLineReporter = struct {
|
||||
this.summary.todo += 1;
|
||||
this.summary.expectations += expectations;
|
||||
this.jest.tests.items(.status)[id] = TestRunner.Test.Status.todo;
|
||||
|
||||
if (this.socket) |socket| {
|
||||
socket.writeMessage(
|
||||
.{
|
||||
.TestEnd = .{
|
||||
.id = id,
|
||||
.status = TestRunner.Test.Status.todo,
|
||||
.duration_ms = @truncate(0),
|
||||
.expectation_count = 0,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn printSummary(this: *CommandLineReporter) void {
|
||||
@@ -723,6 +1105,51 @@ pub const TestCommand = struct {
|
||||
lcov: bool,
|
||||
};
|
||||
|
||||
const TestFilePath = struct {
|
||||
path: []const u8 = "",
|
||||
byte_ranges: std.ArrayListUnmanaged(logger.Range) = .{},
|
||||
|
||||
pub fn slice(this: *const TestFilePath) string {
|
||||
return this.path;
|
||||
}
|
||||
|
||||
pub fn update(this: *TestFilePath, input: []const u8) !void {
|
||||
var remaining = input;
|
||||
while (remaining.len > 0) {
|
||||
const end_index = strings.indexOfChar(remaining, ':') orelse return error.InvalidRange;
|
||||
const start_buffer = remaining[0..end_index];
|
||||
const next_i = strings.indexOf(remaining, "::") orelse remaining.len;
|
||||
const end_buffer = remaining[@min(end_index + 1, remaining.len)..next_i];
|
||||
|
||||
const start = std.fmt.parseInt(i32, start_buffer, 10) catch {
|
||||
Output.err(error.InvalidByteRange, "Invalid start byte range passed to bun test filter: {s}", .{remaining});
|
||||
Global.exit(1);
|
||||
};
|
||||
|
||||
const len = std.fmt.parseInt(i32, end_buffer, 10) catch {
|
||||
Output.err(error.InvalidByteRange, "Invalid end range passed to bun test filter: {s}", .{remaining});
|
||||
Global.exit(1);
|
||||
};
|
||||
try this.byte_ranges.append(bun.default_allocator, .{
|
||||
.loc = .{ .start = start },
|
||||
.len = len,
|
||||
});
|
||||
remaining = remaining[@min(next_i + 2, remaining.len)..];
|
||||
}
|
||||
}
|
||||
};
|
||||
const PathsOrFiles = union(enum) {
|
||||
paths: []const PathString,
|
||||
files: []const TestFilePath,
|
||||
|
||||
pub fn isEmpty(this: *const PathsOrFiles) bool {
|
||||
return switch (this.*) {
|
||||
.paths => |paths| paths.len == 0,
|
||||
.files => |files| files.len == 0,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub fn exec(ctx: Command.Context) !void {
|
||||
if (comptime is_bindgen) unreachable;
|
||||
|
||||
@@ -843,10 +1270,11 @@ pub const TestCommand = struct {
|
||||
_ = vm.global.setTimeZone(&JSC.ZigString.init(TZ_NAME));
|
||||
}
|
||||
|
||||
var results = try std.ArrayList(PathString).initCapacity(ctx.allocator, ctx.positionals.len);
|
||||
var results = bun.StringArrayHashMap(TestFilePath).init(ctx.allocator);
|
||||
try results.ensureTotalCapacity(ctx.positionals.len);
|
||||
defer results.deinit();
|
||||
|
||||
const test_files, const search_count = scan: {
|
||||
const test_files: PathsOrFiles, const search_count = scan: {
|
||||
if (for (ctx.positionals) |arg| {
|
||||
if (std.fs.path.isAbsolute(arg) or
|
||||
strings.startsWith(arg, "./") or
|
||||
@@ -855,10 +1283,22 @@ pub const TestCommand = struct {
|
||||
strings.startsWith(arg, "..\\")))) break true;
|
||||
} else false) {
|
||||
// One of the files is a filepath. Instead of treating the arguments as filters, treat them as filepaths
|
||||
for (ctx.positionals[1..]) |arg| {
|
||||
results.appendAssumeCapacity(PathString.init(arg));
|
||||
for (ctx.positionals[1..]) |arg_| {
|
||||
const range_index = strings.indexOf(arg_, "::");
|
||||
const path = if (range_index) |index| arg_[0..index] else arg_;
|
||||
var gpe = results.getOrPutAssumeCapacity(path);
|
||||
if (!gpe.found_existing) {
|
||||
gpe.value_ptr.* = TestFilePath{
|
||||
.path = path,
|
||||
.byte_ranges = .{},
|
||||
};
|
||||
}
|
||||
|
||||
if (range_index != null) {
|
||||
try gpe.value_ptr.update(arg_[@min(range_index.? + 2, arg_.len)..]);
|
||||
}
|
||||
}
|
||||
break :scan .{ results.items, 0 };
|
||||
break :scan .{ .{ .files = results.values() }, 0 };
|
||||
}
|
||||
|
||||
// Treat arguments as filters and scan the codebase
|
||||
@@ -880,13 +1320,13 @@ pub const TestCommand = struct {
|
||||
ctx.allocator.free(i);
|
||||
ctx.allocator.free(filter_names_normalized);
|
||||
};
|
||||
|
||||
var scanner_results = std.ArrayList(PathString).init(bun.default_allocator);
|
||||
var scanner = Scanner{
|
||||
.dirs_to_scan = Scanner.Fifo.init(ctx.allocator),
|
||||
.options = &vm.bundler.options,
|
||||
.fs = vm.bundler.fs,
|
||||
.filter_names = filter_names_normalized,
|
||||
.results = &results,
|
||||
.results = &scanner_results,
|
||||
};
|
||||
const dir_to_scan = brk: {
|
||||
if (ctx.debug.test_directory.len > 0) {
|
||||
@@ -899,10 +1339,10 @@ pub const TestCommand = struct {
|
||||
scanner.scan(dir_to_scan);
|
||||
scanner.dirs_to_scan.deinit();
|
||||
|
||||
break :scan .{ scanner.results.items, scanner.search_count };
|
||||
break :scan .{ .{ .paths = scanner.results.items }, scanner.search_count };
|
||||
};
|
||||
|
||||
if (test_files.len > 0) {
|
||||
if (!test_files.isEmpty()) {
|
||||
vm.hot_reload = ctx.debug.hot_reload;
|
||||
|
||||
switch (vm.hot_reload) {
|
||||
@@ -912,7 +1352,7 @@ pub const TestCommand = struct {
|
||||
}
|
||||
|
||||
// vm.bundler.fs.fs.readDirectory(_dir: string, _handle: ?std.fs.Dir)
|
||||
runAllTests(reporter, vm, test_files, ctx.allocator);
|
||||
runAllTests(ctx, reporter, vm, test_files);
|
||||
}
|
||||
|
||||
try jest.Jest.runner.?.snapshots.writeSnapshotFile();
|
||||
@@ -954,7 +1394,7 @@ pub const TestCommand = struct {
|
||||
|
||||
Output.flush();
|
||||
|
||||
if (test_files.len == 0) {
|
||||
if (test_files.isEmpty()) {
|
||||
if (ctx.positionals.len == 0) {
|
||||
Output.prettyErrorln(
|
||||
\\<yellow>No tests found!<r>
|
||||
@@ -1102,6 +1542,30 @@ pub const TestCommand = struct {
|
||||
}
|
||||
}
|
||||
|
||||
if (reporter.socket) |socket| {
|
||||
// wait for a maximum of 1 second if there are any pending messages
|
||||
if (socket.hasPendingMessages()) {
|
||||
const start = bun.timespec.now();
|
||||
const loop = bun.uws.Loop.get();
|
||||
loop.ref();
|
||||
debug("waiting for socket messages", .{});
|
||||
while (socket.hasPendingMessages()) {
|
||||
socket.flush();
|
||||
vm.eventLoop().autoTick();
|
||||
|
||||
if (bun.timespec.now().duration(&start).ms() > 1000) {
|
||||
debug("timeout waiting for socket messages", .{});
|
||||
break;
|
||||
}
|
||||
}
|
||||
loop.unref();
|
||||
}
|
||||
|
||||
if (socket.connection_status == .connected) {
|
||||
socket.socket.close(.normal);
|
||||
}
|
||||
}
|
||||
|
||||
if (reporter.summary.fail > 0 or (coverage.enabled and coverage.fractions.failing and coverage.fail_on_low_coverage)) {
|
||||
Global.exit(1);
|
||||
} else if (reporter.jest.unhandled_errors_between_tests > 0) {
|
||||
@@ -1110,32 +1574,33 @@ pub const TestCommand = struct {
|
||||
}
|
||||
|
||||
pub fn runAllTests(
|
||||
ctx: Command.Context,
|
||||
reporter_: *CommandLineReporter,
|
||||
vm_: *JSC.VirtualMachine,
|
||||
files_: []const PathString,
|
||||
allocator_: std.mem.Allocator,
|
||||
files_: PathsOrFiles,
|
||||
) void {
|
||||
const Context = struct {
|
||||
reporter: *CommandLineReporter,
|
||||
vm: *JSC.VirtualMachine,
|
||||
files: []const PathString,
|
||||
allocator: std.mem.Allocator,
|
||||
files: PathsOrFiles,
|
||||
pub fn begin(this: *@This()) void {
|
||||
const reporter = this.reporter;
|
||||
const vm = this.vm;
|
||||
var files = this.files;
|
||||
const allocator = this.allocator;
|
||||
bun.assert(files.len > 0);
|
||||
const paths_or_files = this.files;
|
||||
|
||||
if (files.len > 1) {
|
||||
for (files[0 .. files.len - 1]) |file_name| {
|
||||
TestCommand.run(reporter, vm, file_name.slice(), allocator, false) catch {};
|
||||
reporter.jest.default_timeout_override = std.math.maxInt(u32);
|
||||
Global.mimalloc_cleanup(false);
|
||||
}
|
||||
switch (paths_or_files) {
|
||||
inline else => |files| {
|
||||
if (files.len > 1) {
|
||||
for (files[0 .. files.len - 1]) |file_name| {
|
||||
TestCommand.run(reporter, vm, file_name.slice(), if (comptime @TypeOf(files) == []const PathString) &.{} else file_name.byte_ranges.items, false) catch {};
|
||||
reporter.jest.default_timeout_override = std.math.maxInt(u32);
|
||||
Global.mimalloc_cleanup(false);
|
||||
}
|
||||
}
|
||||
|
||||
TestCommand.run(reporter, vm, files[files.len - 1].slice(), if (comptime @TypeOf(files) == []const PathString) &.{} else files[files.len - 1].byte_ranges.items, true) catch {};
|
||||
},
|
||||
}
|
||||
|
||||
TestCommand.run(reporter, vm, files[files.len - 1].slice(), allocator, true) catch {};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1143,8 +1608,18 @@ pub const TestCommand = struct {
|
||||
vm_.eventLoop().ensureWaker();
|
||||
vm_.arena = &arena;
|
||||
vm_.allocator = arena.allocator();
|
||||
var ctx = Context{ .reporter = reporter_, .vm = vm_, .files = files_, .allocator = allocator_ };
|
||||
vm_.runWithAPILock(Context, &ctx, Context.begin);
|
||||
vm_.bundler.options.has_byte_range_filter_for_tests = files_ == .files;
|
||||
vm_.bundler.resolver.opts.has_byte_range_filter_for_tests = files_ == .files;
|
||||
jest.Jest.is_byte_range_filter_enabled = files_ == .files;
|
||||
|
||||
if (ctx.test_options.listen_address) |*address| {
|
||||
if (SocketReporter.connect(reporter_, address)) |socket| {
|
||||
reporter_.socket = socket;
|
||||
}
|
||||
}
|
||||
|
||||
var test_runner_ctx = Context{ .reporter = reporter_, .vm = vm_, .files = files_ };
|
||||
vm_.runWithAPILock(Context, &test_runner_ctx, Context.begin);
|
||||
}
|
||||
|
||||
fn timerNoop(_: *uws.Timer) callconv(.C) void {}
|
||||
@@ -1153,7 +1628,7 @@ pub const TestCommand = struct {
|
||||
reporter: *CommandLineReporter,
|
||||
vm: *JSC.VirtualMachine,
|
||||
file_name: string,
|
||||
_: std.mem.Allocator,
|
||||
byte_ranges: []const logger.Range,
|
||||
is_last: bool,
|
||||
) !void {
|
||||
defer {
|
||||
@@ -1180,6 +1655,7 @@ pub const TestCommand = struct {
|
||||
const file_start = reporter.jest.files.len;
|
||||
const resolution = try vm.bundler.resolveEntryPoint(file_name);
|
||||
vm.clearEntryPoint();
|
||||
reporter.jest.byte_range_filter = byte_ranges;
|
||||
|
||||
const file_path = resolution.path_pair.primary.text;
|
||||
const file_title = bun.path.relative(FileSystem.instance.top_level_dir, file_path);
|
||||
@@ -1235,14 +1711,27 @@ pub const TestCommand = struct {
|
||||
const file_end = reporter.jest.files.len;
|
||||
|
||||
for (file_start..file_end) |module_id| {
|
||||
const initial_ran_count = reporter.summary.pass + reporter.summary.fail;
|
||||
const module: *jest.DescribeScope = reporter.jest.files.items(.module_scope)[module_id];
|
||||
|
||||
if (reporter.socket) |socket| {
|
||||
socket.writeMessage(
|
||||
.{
|
||||
.ModuleStart = .{
|
||||
.id = @truncate(module_id),
|
||||
.path = file_path,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
vm.onUnhandledRejectionCtx = null;
|
||||
vm.onUnhandledRejection = jest.TestRunnerTask.onUnhandledRejection;
|
||||
module.runTests(vm.global);
|
||||
vm.eventLoop().tick();
|
||||
|
||||
var prev_unhandled_count = vm.unhandled_error_counter;
|
||||
|
||||
while (vm.active_tasks > 0) : (vm.eventLoop().flushImmediateQueue()) {
|
||||
if (!jest.Jest.runner.?.has_pending_tests) {
|
||||
jest.Jest.runner.?.drain();
|
||||
@@ -1265,6 +1754,13 @@ pub const TestCommand = struct {
|
||||
|
||||
vm.eventLoop().flushImmediateQueue();
|
||||
|
||||
const end_ran_count = reporter.summary.pass + reporter.summary.fail;
|
||||
if (end_ran_count != initial_ran_count) {
|
||||
if (module.runCallback(vm.global, .afterAll)) |err| {
|
||||
_ = vm.uncaughtException(vm.global, err, true);
|
||||
}
|
||||
}
|
||||
|
||||
switch (vm.aggressive_garbage_collection) {
|
||||
.none => {},
|
||||
.mild => {
|
||||
|
||||
110
src/deps/uws.zig
110
src/deps/uws.zig
@@ -613,6 +613,27 @@ pub fn NewSocketHandler(comptime is_ssl: bool) type {
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn connectToAddress(
|
||||
address: *const UnixOrHost,
|
||||
socket_ctx: *SocketContext,
|
||||
comptime Context: type,
|
||||
ctx: *Context,
|
||||
comptime socket_field_name: []const u8,
|
||||
) !ThisSocket {
|
||||
switch (address.*) {
|
||||
.unix => |path| {
|
||||
return try connectUnixAnon(path, socket_ctx, ctx);
|
||||
},
|
||||
.host => |host| {
|
||||
_ = try connectPtr(host.host, host.port, socket_ctx, Context, ctx, socket_field_name);
|
||||
return @field(ctx, socket_field_name);
|
||||
},
|
||||
.fd => |fd_| {
|
||||
return fromFd(socket_ctx, fd_, Context, ctx, socket_field_name) orelse error.FailedToOpenSocket;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn connect(
|
||||
host: []const u8,
|
||||
port: i32,
|
||||
@@ -1440,7 +1461,7 @@ pub extern fn us_socket_context_remove_server_name(ssl: i32, context: ?*SocketCo
|
||||
extern fn us_socket_context_on_server_name(ssl: i32, context: ?*SocketContext, cb: ?*const fn (?*SocketContext, [*c]const u8) callconv(.C) void) void;
|
||||
extern fn us_socket_context_get_native_handle(ssl: i32, context: ?*SocketContext) ?*anyopaque;
|
||||
pub extern fn us_create_socket_context(ssl: i32, loop: ?*Loop, ext_size: i32, options: us_socket_context_options_t) ?*SocketContext;
|
||||
pub extern fn us_create_bun_socket_context(ssl: i32, loop: ?*Loop, ext_size: i32, options: us_bun_socket_context_options_t) ?*SocketContext;
|
||||
pub extern fn us_create_bun_socket_context(ssl: i32, loop: *Loop, ext_size: i32, options: us_bun_socket_context_options_t) ?*SocketContext;
|
||||
pub extern fn us_bun_socket_context_add_server_name(ssl: i32, context: ?*SocketContext, hostname_pattern: [*c]const u8, options: us_bun_socket_context_options_t, ?*anyopaque) void;
|
||||
pub extern fn us_socket_context_free(ssl: i32, context: ?*SocketContext) void;
|
||||
pub extern fn us_socket_context_ref(ssl: i32, context: ?*SocketContext) void;
|
||||
@@ -3307,4 +3328,91 @@ pub fn onThreadExit() void {
|
||||
bun_clear_loop_at_thread_exit();
|
||||
}
|
||||
|
||||
pub const UnixOrHost = union(enum) {
|
||||
unix: []const u8,
|
||||
host: struct {
|
||||
host: []const u8,
|
||||
port: u16,
|
||||
},
|
||||
fd: bun.FileDescriptor,
|
||||
|
||||
pub fn format(this: UnixOrHost, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void {
|
||||
_ = fmt; // autofix
|
||||
_ = options; // autofix
|
||||
switch (this) {
|
||||
.unix => |u| {
|
||||
try writer.print("unix:{s}", .{u});
|
||||
},
|
||||
.host => |h| {
|
||||
try writer.print("{s}:{d}", .{ h.host, h.port });
|
||||
},
|
||||
.fd => |f| {
|
||||
try writer.print("fd:{}", .{f});
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(str: []const u8) ?UnixOrHost {
|
||||
if (str.len == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (bun.strings.hasPrefixComptime(str, "unix:") and str.len > 5) {
|
||||
return .{ .unix = str[5..] };
|
||||
}
|
||||
|
||||
if (bun.strings.hasPrefixComptime(str, "fd:")) {
|
||||
return .{ .fd = bun.toFD(std.fmt.parseInt(i32, str[3..], 10) catch return null) };
|
||||
}
|
||||
|
||||
if (bun.strings.indexOfChar(str, ':')) |i| {
|
||||
const hostname = str[0..i];
|
||||
if (i + 1 >= str.len) {
|
||||
return null;
|
||||
}
|
||||
const port_string = str[i + 1 ..];
|
||||
return .{
|
||||
.host = .{
|
||||
.host = hostname,
|
||||
.port = std.fmt.parseInt(u16, port_string, 10) catch return null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn clone(this: UnixOrHost) UnixOrHost {
|
||||
switch (this) {
|
||||
.unix => |u| {
|
||||
return .{
|
||||
.unix = (bun.default_allocator.dupe(u8, u) catch bun.outOfMemory()),
|
||||
};
|
||||
},
|
||||
.host => |h| {
|
||||
return .{
|
||||
.host = .{
|
||||
.host = (bun.default_allocator.dupe(u8, h.host) catch bun.outOfMemory()),
|
||||
.port = this.host.port,
|
||||
},
|
||||
};
|
||||
},
|
||||
.fd => |f| return .{ .fd = f },
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deinit(this: UnixOrHost) void {
|
||||
switch (this) {
|
||||
.unix => |u| {
|
||||
bun.default_allocator.free(u);
|
||||
},
|
||||
.host => |h| {
|
||||
bun.default_allocator.free(h.host);
|
||||
},
|
||||
.fd => {}, // this is an integer
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
extern fn uws_app_clear_routes(ssl_flag: c_int, app: *uws_app_t) void;
|
||||
|
||||
|
||||
@@ -1686,6 +1686,15 @@ pub const E = struct {
|
||||
was_originally_identifier: bool = false,
|
||||
};
|
||||
|
||||
pub const LocationIdentifier = struct {
|
||||
ref: Ref = Ref.None,
|
||||
|
||||
/// If true, this was originally an identifier expression such as "foo". If
|
||||
/// false, this could potentially have been a member access expression such
|
||||
/// as "ns.foo" off of an imported namespace object.
|
||||
was_originally_identifier: bool = false,
|
||||
};
|
||||
|
||||
/// This is a dot expression on exports, such as `exports.<ref>`. It is given
|
||||
/// it's own AST node to allow CommonJS unwrapping, in which this can just be
|
||||
/// the identifier in the Ref
|
||||
@@ -3876,6 +3885,17 @@ pub const Expr = struct {
|
||||
},
|
||||
};
|
||||
},
|
||||
E.LocationIdentifier => {
|
||||
return Expr{
|
||||
.loc = loc,
|
||||
.data = Data{
|
||||
.e_location_identifier = .{
|
||||
.ref = st.ref,
|
||||
.was_originally_identifier = st.was_originally_identifier,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
E.ImportIdentifier => {
|
||||
return Expr{
|
||||
.loc = loc,
|
||||
@@ -4266,6 +4286,15 @@ pub const Expr = struct {
|
||||
},
|
||||
};
|
||||
},
|
||||
E.LocationIdentifier => return Expr{
|
||||
.loc = loc,
|
||||
.data = Data{
|
||||
.e_location_identifier = .{
|
||||
.ref = st.ref,
|
||||
.was_originally_identifier = st.was_originally_identifier,
|
||||
},
|
||||
},
|
||||
},
|
||||
E.ImportIdentifier => {
|
||||
return Expr{
|
||||
.loc = loc,
|
||||
@@ -4449,8 +4478,7 @@ pub const Expr = struct {
|
||||
|
||||
pub fn isRef(this: Expr, ref: Ref) bool {
|
||||
return switch (this.data) {
|
||||
.e_import_identifier => |import_identifier| import_identifier.ref.eql(ref),
|
||||
.e_identifier => |ident| ident.ref.eql(ref),
|
||||
inline .e_identifier, .e_import_identifier, .e_location_identifier => |ident| ident.ref.eql(ref),
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
@@ -4478,6 +4506,8 @@ pub const Expr = struct {
|
||||
e_import,
|
||||
e_identifier,
|
||||
e_import_identifier,
|
||||
e_location_identifier,
|
||||
e_location_dot,
|
||||
e_private_identifier,
|
||||
e_commonjs_export_identifier,
|
||||
e_module_dot_exports,
|
||||
@@ -4544,6 +4574,8 @@ pub const Expr = struct {
|
||||
.e_arrow => writer.writeAll("arrow"),
|
||||
.e_identifier => writer.writeAll("identifier"),
|
||||
.e_import_identifier => writer.writeAll("import identifier"),
|
||||
.e_location_identifier => writer.writeAll("location identifier"),
|
||||
.e_location_dot => writer.writeAll("location dot"),
|
||||
.e_private_identifier => writer.writeAll("#privateIdentifier"),
|
||||
.e_jsx_element => writer.writeAll("<jsx>"),
|
||||
.e_missing => writer.writeAll("<missing>"),
|
||||
@@ -5173,6 +5205,23 @@ pub const Expr = struct {
|
||||
|
||||
e_identifier: E.Identifier,
|
||||
e_import_identifier: E.ImportIdentifier,
|
||||
|
||||
// These are used by the printer to insert the byte ranges of the source
|
||||
// locations at print time when calling the functions as an extra argument.
|
||||
//
|
||||
// Currently, this is only used by bun:test when calling:
|
||||
// - test(label, callback, [start, end])
|
||||
// - test.only(label, callback, [start, end])
|
||||
// - test.each(label, callback, [start, end])
|
||||
// - describe(label, callback, [start, end])
|
||||
// - describe.only(label, callback, [start, end])
|
||||
// - describe.each(label, callback, [start, end])
|
||||
//
|
||||
// We might use it later to implement toMatchInlineSnapshot.
|
||||
//
|
||||
e_location_identifier: E.LocationIdentifier,
|
||||
e_location_dot: *E.Dot,
|
||||
|
||||
e_private_identifier: E.PrivateIdentifier,
|
||||
e_commonjs_export_identifier: E.CommonJSExportIdentifier,
|
||||
e_module_dot_exports,
|
||||
@@ -6003,6 +6052,7 @@ pub const Expr = struct {
|
||||
|
||||
.e_identifier,
|
||||
.e_import_identifier,
|
||||
.e_location_identifier,
|
||||
.e_private_identifier,
|
||||
.e_commonjs_export_identifier,
|
||||
=> error.@"Cannot convert identifier to JS. Try a statically-known value",
|
||||
|
||||
@@ -4923,6 +4923,34 @@ const Jest = struct {
|
||||
beforeAll: Ref = Ref.None,
|
||||
afterAll: Ref = Ref.None,
|
||||
jest: Ref = Ref.None,
|
||||
|
||||
const BunTestField = enum {
|
||||
expect,
|
||||
describe,
|
||||
it,
|
||||
@"test",
|
||||
beforeEach,
|
||||
afterEach,
|
||||
beforeAll,
|
||||
afterAll,
|
||||
jest,
|
||||
};
|
||||
|
||||
const map = bun.ComptimeEnumMap(BunTestField);
|
||||
|
||||
pub fn setFromClauseItems(this: *Jest, items: []const js_ast.ClauseItem) void {
|
||||
for (items) |item| {
|
||||
if (map.get(item.alias)) |field| {
|
||||
switch (field) {
|
||||
inline else => |tag| @field(this, @tagName(tag)) = item.name.ref.?,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shouldBecomeLocationIdentifierForTests(this: *const Jest, ref: Ref) bool {
|
||||
return this.it.eql(ref) or this.describe.eql(ref) or this.@"test".eql(ref);
|
||||
}
|
||||
};
|
||||
|
||||
// workaround for https://github.com/ziglang/zig/issues/10903
|
||||
@@ -6149,6 +6177,11 @@ fn NewParser_(
|
||||
}
|
||||
}
|
||||
|
||||
// Handle functions which need to have their source location tracked at runtime
|
||||
if (p.options.features.inline_loc_for_tests and (p.jest.shouldBecomeLocationIdentifierForTests(ref))) {
|
||||
return p.newLocationIdentifier(ref, opts.was_originally_identifier, loc);
|
||||
}
|
||||
|
||||
// Substitute an EImportIdentifier now if this is an import item
|
||||
if (p.is_import_item.contains(ref)) {
|
||||
return p.newExpr(
|
||||
@@ -6222,6 +6255,12 @@ fn NewParser_(
|
||||
};
|
||||
}
|
||||
|
||||
fn newLocationIdentifier(p: *P, ref: Ref, was_originally_identifier: bool, loc: logger.Loc) Expr {
|
||||
// This function exists entirely for @setCold(true);
|
||||
@setCold(true);
|
||||
return p.newExpr(E.LocationIdentifier{ .ref = ref, .was_originally_identifier = was_originally_identifier }, loc);
|
||||
}
|
||||
|
||||
pub fn generateImportStmt(
|
||||
p: *P,
|
||||
import_path: string,
|
||||
@@ -6874,15 +6913,11 @@ fn NewParser_(
|
||||
p.filename_ref = try p.declareCommonJSSymbol(.unbound, "__filename");
|
||||
|
||||
if (p.options.features.inject_jest_globals) {
|
||||
p.jest.describe = try p.declareCommonJSSymbol(.unbound, "describe");
|
||||
p.jest.@"test" = try p.declareCommonJSSymbol(.unbound, "test");
|
||||
p.jest.jest = try p.declareCommonJSSymbol(.unbound, "jest");
|
||||
p.jest.it = try p.declareCommonJSSymbol(.unbound, "it");
|
||||
p.jest.expect = try p.declareCommonJSSymbol(.unbound, "expect");
|
||||
p.jest.beforeEach = try p.declareCommonJSSymbol(.unbound, "beforeEach");
|
||||
p.jest.afterEach = try p.declareCommonJSSymbol(.unbound, "afterEach");
|
||||
p.jest.beforeAll = try p.declareCommonJSSymbol(.unbound, "beforeAll");
|
||||
p.jest.afterAll = try p.declareCommonJSSymbol(.unbound, "afterAll");
|
||||
inline for (comptime std.meta.fieldNames(Jest)) |field_name| {
|
||||
if (@field(p.jest, field_name).isNull()) {
|
||||
@field(p.jest, field_name) = try p.declareCommonJSSymbol(.unbound, field_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (p.options.features.hot_module_reloading) {
|
||||
@@ -9290,11 +9325,25 @@ fn NewParser_(
|
||||
try p.validateImportType(path.import_tag, &stmt);
|
||||
}
|
||||
|
||||
if (comptime !only_scan_imports_and_do_not_visit) {
|
||||
if (p.options.features.inline_loc_for_tests and strings.eqlComptime(path.text, "bun:test")) {
|
||||
p.configureBunTestImport(stmt.items, stmt.import_record_index);
|
||||
}
|
||||
}
|
||||
|
||||
// Track the items for this namespace
|
||||
try p.import_items_for_namespace.put(p.allocator, stmt.namespace_ref, item_refs);
|
||||
return p.s(stmt, loc);
|
||||
}
|
||||
|
||||
// This function mostly exists for @setCold(true).
|
||||
fn configureBunTestImport(p: *P, clause_items: []const js_ast.ClauseItem, record_index: u32) void {
|
||||
@setCold(true);
|
||||
|
||||
p.import_records.items[record_index].tag = .bun_test;
|
||||
p.jest.setFromClauseItems(clause_items);
|
||||
}
|
||||
|
||||
fn validateImportType(p: *P, import_tag: ImportRecord.Tag, stmt: *S.Import) !void {
|
||||
@setCold(true);
|
||||
|
||||
@@ -9618,7 +9667,7 @@ fn NewParser_(
|
||||
}
|
||||
}
|
||||
},
|
||||
.e_identifier => |ident| {
|
||||
inline .e_location_identifier, .e_identifier => |ident| {
|
||||
return LocRef{ .loc = loc, .ref = ident.ref };
|
||||
},
|
||||
.e_import_identifier => |ident| {
|
||||
@@ -15808,13 +15857,7 @@ fn NewParser_(
|
||||
|
||||
const key = brk: {
|
||||
switch (expr.data) {
|
||||
.e_import_identifier => |ident| {
|
||||
break :brk p.newExpr(E.String{ .data = p.loadNameFromRef(ident.ref) }, expr.loc);
|
||||
},
|
||||
.e_commonjs_export_identifier => |ident| {
|
||||
break :brk p.newExpr(E.String{ .data = p.loadNameFromRef(ident.ref) }, expr.loc);
|
||||
},
|
||||
.e_identifier => |ident| {
|
||||
inline .e_location_identifier, .e_identifier, .e_commonjs_export_identifier, .e_import_identifier => |ident| {
|
||||
break :brk p.newExpr(E.String{ .data = p.loadNameFromRef(ident.ref) }, expr.loc);
|
||||
},
|
||||
.e_dot => |dot| {
|
||||
@@ -18779,9 +18822,9 @@ fn NewParser_(
|
||||
// just not module.exports = { bar: function() {} }
|
||||
// just not module.exports = { bar() {} }
|
||||
switch (prop.value.?.data) {
|
||||
.e_commonjs_export_identifier, .e_import_identifier, .e_identifier => false,
|
||||
.e_location_identifier, .e_commonjs_export_identifier, .e_import_identifier, .e_identifier => false,
|
||||
.e_call => |call| switch (call.target.data) {
|
||||
.e_commonjs_export_identifier, .e_import_identifier, .e_identifier => false,
|
||||
.e_location_identifier, .e_commonjs_export_identifier, .e_import_identifier, .e_identifier => false,
|
||||
else => |call_target| !@as(Expr.Tag, call_target).isPrimitiveLiteral(),
|
||||
},
|
||||
else => !prop.value.?.isPrimitiveLiteral(),
|
||||
@@ -18968,6 +19011,24 @@ fn NewParser_(
|
||||
}
|
||||
}
|
||||
},
|
||||
.e_location_identifier => |id| {
|
||||
// support:
|
||||
// - test.each
|
||||
// - test.only
|
||||
// - describe.only
|
||||
// - describe.each
|
||||
if (identifier_opts.is_call_target and (id.ref.eql(p.jest.@"test") or id.ref.eql(p.jest.describe) or id.ref.eql(p.jest.it)) and ((strings.eqlComptime(name, "each") or strings.eqlComptime(name, "only")))) {
|
||||
var out = Expr.init(E.Dot, E.Dot{
|
||||
.name = name,
|
||||
.name_loc = name_loc,
|
||||
.target = target,
|
||||
}, loc);
|
||||
out.data = .{
|
||||
.e_location_dot = out.data.e_dot,
|
||||
};
|
||||
return out;
|
||||
}
|
||||
},
|
||||
.e_object => |obj| {
|
||||
if (comptime FeatureFlags.inline_properties_in_transpiler) {
|
||||
if (p.options.features.minify_syntax) {
|
||||
|
||||
@@ -2493,6 +2493,7 @@ fn NewPrinter(
|
||||
}
|
||||
// We only want to generate an unbound eval() in CommonJS
|
||||
p.call_target = e.target.data;
|
||||
const is_loc_identifier = is_bun_platform and (e.target.data == .e_location_identifier or e.target.data == .e_location_dot);
|
||||
|
||||
const is_unbound_eval = (!e.is_direct_eval and
|
||||
p.isUnboundEvalIdentifier(e.target) and
|
||||
@@ -2524,6 +2525,24 @@ fn NewPrinter(
|
||||
if (e.close_paren_loc.start > expr.loc.start) {
|
||||
p.addSourceMapping(e.close_paren_loc);
|
||||
}
|
||||
|
||||
// Append [start, length] to the end of the call
|
||||
// Used by bun:test.
|
||||
if (comptime is_bun_platform) {
|
||||
if (is_loc_identifier) {
|
||||
if (args.len > 0) {
|
||||
p.print(",");
|
||||
p.printSpace();
|
||||
}
|
||||
|
||||
p.print(("["));
|
||||
p.printNumber(@floatFromInt(e.target.loc.start), level);
|
||||
p.print(",");
|
||||
p.printNumber(@floatFromInt(e.close_paren_loc.start - e.target.loc.start), level);
|
||||
p.print("]");
|
||||
}
|
||||
}
|
||||
|
||||
p.print(")");
|
||||
if (wrap) {
|
||||
p.print(")");
|
||||
@@ -2656,7 +2675,7 @@ fn NewPrinter(
|
||||
);
|
||||
}
|
||||
},
|
||||
.e_dot => |e| {
|
||||
.e_location_dot, .e_dot => |e| {
|
||||
const isOptionalChain = e.optional_chain == .start;
|
||||
|
||||
var wrap = false;
|
||||
@@ -3088,6 +3107,22 @@ fn NewPrinter(
|
||||
p.print(")");
|
||||
}
|
||||
},
|
||||
.e_location_identifier => |e| {
|
||||
// Pretend this is an import identifier
|
||||
p.printExpr(
|
||||
Expr{
|
||||
.data = .{
|
||||
.e_import_identifier = .{
|
||||
.ref = e.ref,
|
||||
.was_originally_identifier = e.was_originally_identifier,
|
||||
},
|
||||
},
|
||||
.loc = expr.loc,
|
||||
},
|
||||
level,
|
||||
flags,
|
||||
);
|
||||
},
|
||||
.e_import_identifier => |e| {
|
||||
// Potentially use a property access instead of an identifier
|
||||
var didPrint = false;
|
||||
|
||||
@@ -1493,6 +1493,10 @@ pub const BundleOptions = struct {
|
||||
|
||||
rewrite_jest_for_tests: bool = false,
|
||||
|
||||
/// Appends the byte range of the source location to the test
|
||||
/// In the parser, this is called `inline_loc_for_tests`
|
||||
has_byte_range_filter_for_tests: bool = false,
|
||||
|
||||
macro_remap: MacroRemap = MacroRemap{},
|
||||
no_macros: bool = false,
|
||||
|
||||
|
||||
@@ -251,6 +251,8 @@ pub const Runtime = struct {
|
||||
|
||||
replace_exports: ReplaceableExport.Map = .{},
|
||||
|
||||
inline_loc_for_tests: bool = false,
|
||||
|
||||
dont_bundle_twice: bool = false,
|
||||
|
||||
/// This is a list of packages which even when require() is used, we will
|
||||
|
||||
67
test/js/bun/test/TestReporterProtocol.test.ts
Normal file
67
test/js/bun/test/TestReporterProtocol.test.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { listenOnSocket, Message, MessageType, ModuleStart, TestStatus } from "./TestReporterProtocol";
|
||||
import { test, expect } from "bun:test";
|
||||
import { createServer, type Socket } from "node:net";
|
||||
import path from "node:path";
|
||||
import { bunEnv, bunExe } from "harness";
|
||||
|
||||
test("listenOnSocket", async () => {
|
||||
const { resolve, promise } = Promise.withResolvers<Socket>();
|
||||
const server = createServer(socket => {
|
||||
resolve(socket);
|
||||
}).listen(0, "127.0.0.1");
|
||||
|
||||
const testPath = path.join(__dirname, "simple-test-fixture.ts");
|
||||
const { address: host, port } = server.address();
|
||||
const { stdout, exited } = Bun.spawn({
|
||||
cmd: [bunExe(), "test", testPath, "--listen=" + `${host}:${port}`],
|
||||
env: { ...bunEnv, "BUN_DEBUG": "out.log", "BUN_DEBUG_ALL": "1", "BUN_DEBUG_QUIET_LOGS": undefined },
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
});
|
||||
|
||||
const messages: Message[] = [];
|
||||
const socket = await promise;
|
||||
const getIterator = await listenOnSocket(socket);
|
||||
for await (const message of getIterator()) {
|
||||
messages.push(message);
|
||||
}
|
||||
expect(messages).toEqual([
|
||||
{
|
||||
tag: MessageType.ModuleStart,
|
||||
path: testPath,
|
||||
id: 0,
|
||||
},
|
||||
{
|
||||
tag: MessageType.TestStart,
|
||||
id: 0,
|
||||
label: "should pass",
|
||||
byteOffset: expect.any(Number),
|
||||
byteLength: expect.any(Number),
|
||||
parent_id: 0,
|
||||
module_id: 0,
|
||||
},
|
||||
{
|
||||
tag: MessageType.TestEnd,
|
||||
id: 0,
|
||||
duration_ms: expect.any(Number),
|
||||
expectation_count: 1,
|
||||
status: TestStatus.pass,
|
||||
},
|
||||
{
|
||||
tag: MessageType.TestStart,
|
||||
id: 1,
|
||||
label: "should fail",
|
||||
byteOffset: expect.any(Number),
|
||||
byteLength: expect.any(Number),
|
||||
parent_id: 0,
|
||||
module_id: 0,
|
||||
},
|
||||
{
|
||||
tag: MessageType.TestEnd,
|
||||
id: 1,
|
||||
duration_ms: expect.any(Number),
|
||||
expectation_count: 1,
|
||||
status: TestStatus.fail,
|
||||
},
|
||||
]);
|
||||
});
|
||||
228
test/js/bun/test/TestReporterProtocol.ts
Normal file
228
test/js/bun/test/TestReporterProtocol.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
// All ints are little endian unsigned 32 bit integers
|
||||
// There are only uint32 integers and UTF-8 strings
|
||||
// Each message starts with the byte length of the message
|
||||
// Then, the message type, which is a 4 byte unsigned integer (as all ints are)
|
||||
// Then, the message data
|
||||
import { Socket } from "node:net";
|
||||
export const enum MessageType {
|
||||
TestStart,
|
||||
TestEnd,
|
||||
ModuleStart,
|
||||
CoverageReport,
|
||||
CoverageFileReport,
|
||||
}
|
||||
|
||||
export const enum TestStatus {
|
||||
pending,
|
||||
pass,
|
||||
fail,
|
||||
skip,
|
||||
todo,
|
||||
fail_because_todo_passed,
|
||||
fail_because_expected_has_assertions,
|
||||
fail_because_expected_assertion_count,
|
||||
}
|
||||
|
||||
function readString(data: Buffer, offset: number) {
|
||||
const length = data.readUint32LE(offset);
|
||||
offset += 4;
|
||||
return data.toString("utf8", offset, offset + length);
|
||||
}
|
||||
|
||||
export class ModuleStart {
|
||||
id: number;
|
||||
path: string;
|
||||
|
||||
constructor(id: number, path: string) {
|
||||
this.id = id;
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
tag: MessageType.ModuleStart = MessageType.ModuleStart;
|
||||
|
||||
static decode(data: Buffer, offset: number): ModuleStart {
|
||||
const id = data.readUint32LE(offset);
|
||||
offset += 4;
|
||||
const path = readString(data, offset);
|
||||
const result = new ModuleStart(id, path);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export class TestStart {
|
||||
id: number;
|
||||
parent_id: number;
|
||||
module_id: number;
|
||||
byteOffset: number;
|
||||
byteLength: number;
|
||||
label: string;
|
||||
|
||||
constructor(id: number, parent_id: number, module_id: number, byteOffset: number, byteLength: number, label: string) {
|
||||
this.id = id;
|
||||
this.parent_id = parent_id;
|
||||
this.module_id = module_id;
|
||||
this.byteOffset = byteOffset;
|
||||
this.byteLength = byteLength;
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
tag: MessageType.TestStart = MessageType.TestStart;
|
||||
|
||||
static decode(data: Buffer, offset: number) {
|
||||
const id = data.readUint32LE(offset);
|
||||
offset += 4;
|
||||
const parent_id = data.readUint32LE(offset);
|
||||
offset += 4;
|
||||
const module_id = data.readUint32LE(offset);
|
||||
offset += 4;
|
||||
const byteOffset = data.readUint32LE(offset);
|
||||
offset += 4;
|
||||
const byteLength = data.readUint32LE(offset);
|
||||
offset += 4;
|
||||
const label = readString(data, offset);
|
||||
return new TestStart(id, parent_id, module_id, byteOffset, byteLength, label);
|
||||
}
|
||||
}
|
||||
|
||||
export class TestEnd {
|
||||
id: number;
|
||||
status: TestStatus;
|
||||
duration_ms: number;
|
||||
expectation_count: number;
|
||||
|
||||
constructor(id: number, status: TestStatus, duration_ms: number, expectation_count: number) {
|
||||
this.id = id;
|
||||
this.status = status;
|
||||
this.duration_ms = duration_ms;
|
||||
this.expectation_count = expectation_count;
|
||||
}
|
||||
|
||||
tag: MessageType.TestEnd = MessageType.TestEnd;
|
||||
|
||||
static decode(data: Buffer, offset: number) {
|
||||
return new TestEnd(
|
||||
data.readUint32LE(0 + offset),
|
||||
data.readUint32LE(4 + offset),
|
||||
data.readUint32LE(8 + offset),
|
||||
data.readUint32LE(12 + offset),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class Decoder {
|
||||
read(data: Buffer, offset: number, length: number): TestStart | TestEnd | ModuleStart | undefined {
|
||||
if (length < 4) {
|
||||
throw new Error("Not enough data to read. Must have at least 4 bytes.");
|
||||
}
|
||||
|
||||
const tag = data.readUint32LE(offset);
|
||||
switch (tag) {
|
||||
case MessageType.TestStart:
|
||||
return TestStart.decode(data, offset + 4);
|
||||
case MessageType.TestEnd:
|
||||
return TestEnd.decode(data, offset + 4);
|
||||
case MessageType.ModuleStart:
|
||||
return ModuleStart.decode(data, offset + 4);
|
||||
default: {
|
||||
throw new Error(`Unknown message type: ${tag}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function listenOnSocket(socket: Socket) {
|
||||
const decoder = new Decoder();
|
||||
let promise, resolve, reject;
|
||||
let buffer = Buffer.alloc(16 * 1024);
|
||||
let bufferLength = 0;
|
||||
let read = 0;
|
||||
|
||||
promise = new Promise((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
|
||||
const resumeFn = () => {
|
||||
drain();
|
||||
if (queue.length > 0) {
|
||||
resolve(queue);
|
||||
}
|
||||
};
|
||||
|
||||
function onData(data: Buffer) {
|
||||
data.copy(buffer, bufferLength);
|
||||
bufferLength += data.length;
|
||||
resumeFn();
|
||||
}
|
||||
|
||||
socket.on("data", onData);
|
||||
|
||||
function onClose() {
|
||||
socket.off("data", onData);
|
||||
isClosed = true;
|
||||
resolve();
|
||||
}
|
||||
|
||||
socket.once("close", onClose);
|
||||
socket.once("end", onClose);
|
||||
|
||||
var queue: Message[] = [];
|
||||
var isClosed = false;
|
||||
|
||||
function drainOne() {
|
||||
const readable = bufferLength - read;
|
||||
if (readable < 8) return;
|
||||
|
||||
const messageLength = buffer.readUint32LE(read);
|
||||
const offset = read + 4;
|
||||
if (readable < messageLength) return;
|
||||
read += messageLength;
|
||||
if (read >= bufferLength) {
|
||||
buffer.copyWithin(0, read, bufferLength);
|
||||
read = 0;
|
||||
bufferLength = 0;
|
||||
}
|
||||
return decoder.read(buffer, offset, messageLength);
|
||||
}
|
||||
|
||||
function drain() {
|
||||
while (!isClosed) {
|
||||
const message = drainOne();
|
||||
if (message) {
|
||||
queue.push(message);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return async function* () {
|
||||
while (true) {
|
||||
await promise;
|
||||
promise = new Promise((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
const messages = queue;
|
||||
queue = [];
|
||||
if (messages.length === 0) {
|
||||
return;
|
||||
}
|
||||
yield* messages;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export class CoverageReport {
|
||||
constructor(public data: Buffer) {}
|
||||
|
||||
tag: MessageType.CoverageReport = MessageType.CoverageReport;
|
||||
}
|
||||
|
||||
export class CoverageFileReport {
|
||||
constructor(public data: Buffer) {}
|
||||
|
||||
tag: MessageType.CoverageFileReport = MessageType.CoverageFileReport;
|
||||
}
|
||||
|
||||
export type Message = TestStart | TestEnd | ModuleStart | CoverageReport | CoverageFileReport;
|
||||
53
test/js/bun/test/bun-byte-range-fixture.ts
generated
Normal file
53
test/js/bun/test/bun-byte-range-fixture.ts
generated
Normal file
@@ -0,0 +1,53 @@
|
||||
import { expect, test, describe, beforeEach, beforeAll, afterAll, afterEach } from "bun:test";
|
||||
|
||||
beforeAll(() => {
|
||||
console.log("beforeAll");
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
console.log("afterAll");
|
||||
});
|
||||
|
||||
test("<!-- <Test [0]> -->", () => {
|
||||
console.log("Test #1 ran");
|
||||
});
|
||||
|
||||
test("<!-- <Test [1]> -->", () => {
|
||||
console.log("Test #2 ran");
|
||||
});
|
||||
|
||||
describe("<!-- <Describe [0]> -->", () => {
|
||||
beforeEach(() => {
|
||||
console.log("beforeEach");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
console.log("afterEach");
|
||||
});
|
||||
|
||||
test("<!-- <Test In Describe [0]> -->", () => {
|
||||
console.log("Test #3 ran");
|
||||
});
|
||||
/// --- Before Test#2InDescribe
|
||||
|
||||
test("<!-- <Test In Describe [1]> -->", () => {
|
||||
console.log("Test #4 ran");
|
||||
});
|
||||
|
||||
// --- Before Test#3InDescribe
|
||||
|
||||
test("<!-- <Test In Describe [2]> -->", () => {
|
||||
console.log("Test #5 ran");
|
||||
});
|
||||
});
|
||||
|
||||
// --- Before test.only
|
||||
test.only("<!-- <Test [5]> -->", () => {
|
||||
console.log("Test #6 ran");
|
||||
});
|
||||
|
||||
// After test.only
|
||||
|
||||
test("<!-- <Test [6]> -->", () => {
|
||||
console.log("Test #7 ran");
|
||||
});
|
||||
214
test/js/bun/test/bun-byte-range.test.ts
Normal file
214
test/js/bun/test/bun-byte-range.test.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
import { expect, test, describe } from "bun:test";
|
||||
import "harness";
|
||||
import path from "path";
|
||||
import { readFileSync } from "fs";
|
||||
import { spawnSync } from "bun";
|
||||
import { bunExe, bunEnv } from "harness";
|
||||
|
||||
const fixture = readFileSync(path.join(import.meta.dir, "bun-byte-range-fixture.ts"), "utf8");
|
||||
|
||||
function runTest(startMarker: string, endMarker: string, expectedOutput: string[]) {
|
||||
const startRange = fixture.indexOf(startMarker);
|
||||
const endRange = fixture.indexOf(endMarker, startRange + startMarker.length);
|
||||
const length = endRange - startRange;
|
||||
const byteRange = `${startRange}:${length + endMarker.length}`;
|
||||
const rangedPath = path.join(import.meta.dir, "bun-byte-range-fixture.ts") + "::" + byteRange;
|
||||
|
||||
const { stdout, exitCode } = spawnSync({
|
||||
cmd: [bunExe(), "test", rangedPath],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "inherit",
|
||||
});
|
||||
|
||||
const text = stdout.toString().trim().split("\n");
|
||||
expect(text).toEqual(expectedOutput);
|
||||
expect(exitCode).toBe(0);
|
||||
}
|
||||
|
||||
function runTestMultipleMarkers(markers: Array<[string, string]>, expectedOutput: string[]) {
|
||||
const ranges = markers.map(([startMarker, endMarker]) => {
|
||||
const startRange = fixture.indexOf(startMarker);
|
||||
const endRange = fixture.indexOf(endMarker, startRange + startMarker.length);
|
||||
const length = endRange - startRange;
|
||||
return `${startRange}:${length + endMarker.length}`;
|
||||
});
|
||||
const rangedPath = path.join(import.meta.dir, "bun-byte-range-fixture.ts") + "::" + ranges.join("::");
|
||||
console.log({ rangedPath });
|
||||
const { stdout, exitCode } = spawnSync({
|
||||
cmd: [bunExe(), "test", rangedPath],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "inherit",
|
||||
});
|
||||
|
||||
const text = stdout.toString().trim().split("\n");
|
||||
expect(text).toEqual(expectedOutput);
|
||||
expect(exitCode).toBe(0);
|
||||
}
|
||||
|
||||
describe("single byte range filter", () => {
|
||||
test("Test #1 and #2", () => {
|
||||
runTest("<!-- <Test [0]> -->", "<!-- <Test [1]> -->", ["beforeAll", "Test #1 ran", "Test #2 ran", "afterAll"]);
|
||||
});
|
||||
|
||||
test("Test #1", () => {
|
||||
runTest("<!-- <Test [0]> -->", "Test #1 ran", ["beforeAll", "Test #1 ran", "afterAll"]);
|
||||
});
|
||||
|
||||
test("Test #2", () => {
|
||||
runTest("<!-- <Test [1]> -->", "<!-- <Describe [0]> -->", ["beforeAll", "Test #2 ran", "afterAll"]);
|
||||
});
|
||||
|
||||
describe("Describe block tests", () => {
|
||||
test("all tests in Describe block", () => {
|
||||
runTest("<!-- <Describe [0]> -->", "// --- Before test.only", [
|
||||
"beforeAll",
|
||||
"beforeEach",
|
||||
"Test #3 ran",
|
||||
"afterEach",
|
||||
"beforeEach",
|
||||
"Test #4 ran",
|
||||
"afterEach",
|
||||
"beforeEach",
|
||||
"Test #5 ran",
|
||||
"afterEach",
|
||||
"afterAll",
|
||||
]);
|
||||
});
|
||||
|
||||
test("Test #3 in Describe block", () => {
|
||||
runTest("<!-- <Test In Describe [0]> -->", "/// --- Before Test#2InDescribe", [
|
||||
"beforeAll",
|
||||
"beforeEach",
|
||||
"Test #3 ran",
|
||||
"afterEach",
|
||||
"afterAll",
|
||||
]);
|
||||
});
|
||||
|
||||
test("Test #4 in Describe block", () => {
|
||||
runTest("<!-- <Test In Describe [1]> -->", "--- Before Test#3InDescribe", [
|
||||
"beforeAll",
|
||||
"beforeEach",
|
||||
"Test #4 ran",
|
||||
"afterEach",
|
||||
"afterAll",
|
||||
]);
|
||||
});
|
||||
|
||||
test("Test #5 in Describe block", () => {
|
||||
runTest("<!-- <Test In Describe [2]> -->", "});", [
|
||||
"beforeAll",
|
||||
"beforeEach",
|
||||
"Test #5 ran",
|
||||
"afterEach",
|
||||
"afterAll",
|
||||
]);
|
||||
});
|
||||
|
||||
test("multiple tests in Describe block", () => {
|
||||
runTest("<!-- <Test In Describe [1]> -->", "<!-- <Test In Describe [2]> -->", [
|
||||
"beforeAll",
|
||||
"beforeEach",
|
||||
"Test #4 ran",
|
||||
"afterEach",
|
||||
"beforeEach",
|
||||
"Test #5 ran",
|
||||
"afterEach",
|
||||
"afterAll",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
test("Test #6 (test.only)", () => {
|
||||
runTest("<Test [5]>", "#6 ran", ["beforeAll", "Test #6 ran", "afterAll"]);
|
||||
});
|
||||
|
||||
test("Test #7 after (test.only)", () => {
|
||||
runTest("// After test.only", "#7 ran", ["beforeAll", "Test #7 ran", "afterAll"]);
|
||||
});
|
||||
|
||||
test("entire file", () => {
|
||||
runTest("<Test [0]>", "Test #7 ran", [
|
||||
"beforeAll",
|
||||
"beforeEach",
|
||||
"Test #3 ran",
|
||||
"afterEach",
|
||||
"beforeEach",
|
||||
"Test #4 ran",
|
||||
"afterEach",
|
||||
"beforeEach",
|
||||
"Test #5 ran",
|
||||
"afterEach",
|
||||
"Test #1 ran",
|
||||
"Test #2 ran",
|
||||
"Test #6 ran",
|
||||
"Test #7 ran",
|
||||
"afterAll",
|
||||
]);
|
||||
});
|
||||
|
||||
test("entire file", () => {
|
||||
runTest("<Test [0]>", "Test #7 ran", [
|
||||
"beforeAll",
|
||||
"beforeEach",
|
||||
"Test #3 ran",
|
||||
"afterEach",
|
||||
"beforeEach",
|
||||
"Test #4 ran",
|
||||
"afterEach",
|
||||
"beforeEach",
|
||||
"Test #5 ran",
|
||||
"afterEach",
|
||||
"Test #1 ran",
|
||||
"Test #2 ran",
|
||||
"Test #6 ran",
|
||||
"Test #7 ran",
|
||||
"afterAll",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("multiple byte range filter", () => {
|
||||
test("Test #1 and #2", () => {
|
||||
runTestMultipleMarkers(
|
||||
[
|
||||
["<!-- <Test [0]> -->", ");"],
|
||||
["<!-- <Test [1]> -->", ");"],
|
||||
],
|
||||
["beforeAll", "Test #1 ran", "Test #2 ran", "afterAll"],
|
||||
);
|
||||
});
|
||||
|
||||
test("entire file", () => {
|
||||
runTestMultipleMarkers(
|
||||
[
|
||||
["Test #1", ");"],
|
||||
["Test #2", ");"],
|
||||
["Test #3", ");"],
|
||||
["Test #4", ");"],
|
||||
["Test #5", ");"],
|
||||
["Test #6", ");"],
|
||||
["Test #7", ");"],
|
||||
],
|
||||
[
|
||||
"beforeAll",
|
||||
"beforeEach",
|
||||
"Test #3 ran",
|
||||
"afterEach",
|
||||
"beforeEach",
|
||||
"Test #4 ran",
|
||||
"afterEach",
|
||||
"beforeEach",
|
||||
"Test #5 ran",
|
||||
"afterEach",
|
||||
"Test #1 ran",
|
||||
"Test #2 ran",
|
||||
"Test #6 ran",
|
||||
"Test #7 ran",
|
||||
"afterAll",
|
||||
],
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user