Compare commits

...

6 Commits

Author SHA1 Message Date
Jarred Sumner
bfe7e711eb Merge branch 'main' into jarred/byte-range 2024-09-03 13:26:31 -07:00
Jarred Sumner
dc03f0283c Merge branch 'main' into jarred/byte-range 2024-08-30 14:58:56 -07:00
Jarred Sumner
8195aa4c96 Report passing and failing tests over TCP sockets 2024-08-25 09:09:13 -07:00
Jarred Sumner
ad638a6bea Update jest.zig 2024-08-25 01:51:31 -07:00
Jarred Sumner
22176cb9bb Fix bug with afterAll not running 2024-08-25 01:39:57 -07:00
Jarred Sumner
cf3caba3dc [bun test] Introduce a way to filter tests to run by byte range in file 2024-08-25 01:39:44 -07:00
16 changed files with 1467 additions and 113 deletions

View File

@@ -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| {

View File

@@ -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

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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 => {

View File

@@ -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;

View File

@@ -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",

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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,

View File

@@ -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

View 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,
},
]);
});

View 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;

View 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");
});

View 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",
],
);
});
});