aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> 2023-01-09 08:25:39 -0800
committerGravatar Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> 2023-01-09 08:25:39 -0800
commita1b2c23671d2ae1c2263b1227dc1cd6f071433d5 (patch)
tree7a62e07b8f5ef2b6141d2a2ae0854d0e9eb6281e
parent5d60aae3b3ba39fc1d48469d46161f53cfcb37e4 (diff)
downloadbun-a1b2c23671d2ae1c2263b1227dc1cd6f071433d5.tar.gz
bun-a1b2c23671d2ae1c2263b1227dc1cd6f071433d5.tar.zst
bun-a1b2c23671d2ae1c2263b1227dc1cd6f071433d5.zip
[bun:test] Implement `test.skip`
-rw-r--r--packages/bun-types/bun-test.d.ts42
-rw-r--r--src/bun.js/test/jest.zig128
-rw-r--r--src/cli/test_command.zig105
3 files changed, 210 insertions, 65 deletions
diff --git a/packages/bun-types/bun-test.d.ts b/packages/bun-types/bun-test.d.ts
index d494fc3f6..5e3adf041 100644
--- a/packages/bun-types/bun-test.d.ts
+++ b/packages/bun-types/bun-test.d.ts
@@ -19,18 +19,44 @@
declare module "bun:test" {
export function describe(label: string, body: () => void): any;
- export function test(
- label: string,
- test: (done: (err?: any) => void) => void | Promise<any>,
- ): any;
+ export interface Test {
+ (
+ label: string,
+ test: (done: (err?: any) => void) => void | Promise<any>,
+ ): any;
+ /**
+ * Note: does not fully work yet.
+ */
+ only(
+ label: string,
+ test: (done: (err?: any) => void) => void | Promise<any>,
+ ): any;
+
+ /**
+ * Skip a test
+ */
+ skip(
+ label: string,
+ test: (done: (err?: any) => void) => void | Promise<any>,
+ ): any;
+ }
+ export const test: Test;
export { test as it };
export function expect(value: any): Expect;
- export function afterAll(fn: (done: (err?: any) => void) => void | Promise<any>): void;
- export function beforeAll(fn: (done: (err?: any) => void) => void | Promise<any>): void;
+ export function afterAll(
+ fn: (done: (err?: any) => void) => void | Promise<any>,
+ ): void;
+ export function beforeAll(
+ fn: (done: (err?: any) => void) => void | Promise<any>,
+ ): void;
- export function afterEach(fn: (done: (err?: any) => void) => void | Promise<any>): void;
- export function beforeEach(fn: (done: (err?: any) => void) => void | Promise<any>): void;
+ export function afterEach(
+ fn: (done: (err?: any) => void) => void | Promise<any>,
+ ): void;
+ export function beforeEach(
+ fn: (done: (err?: any) => void) => void | Promise<any>,
+ ): void;
interface Expect {
not: Expect;
diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig
index 6882e547b..c1e38f0ac 100644
--- a/src/bun.js/test/jest.zig
+++ b/src/bun.js/test/jest.zig
@@ -133,6 +133,7 @@ pub const TestRunner = struct {
onTestStart: OnTestStart,
onTestPass: OnTestUpdate,
onTestFail: OnTestUpdate,
+ onTestSkip: OnTestUpdate,
};
pub fn reportPass(this: *TestRunner, test_id: Test.ID, file: string, label: string, expectations: u32, parent: ?*DescribeScope) void {
@@ -144,6 +145,11 @@ pub const TestRunner = struct {
this.callback.onTestFail(this.callback, test_id, file, label, expectations, parent);
}
+ pub fn reportSkip(this: *TestRunner, test_id: Test.ID, file: string, label: string, parent: ?*DescribeScope) void {
+ this.tests.items(.status)[test_id] = .skip;
+ this.callback.onTestSkip(this.callback, test_id, file, label, 0, parent);
+ }
+
pub fn addTestCount(this: *TestRunner, count: u32) u32 {
this.tests.ensureUnusedCapacity(this.allocator, count) catch unreachable;
const start = @truncate(Test.ID, this.tests.len);
@@ -190,6 +196,7 @@ pub const TestRunner = struct {
pending,
pass,
fail,
+ skip,
};
};
};
@@ -1245,8 +1252,18 @@ pub const TestScope = struct {
promise: ?*JSInternalPromise = null,
ran: bool = false,
task: ?*TestRunnerTask = null,
+ skipped: bool = false,
- pub const Class = NewClass(void, .{ .name = "test" }, .{ .call = call, .only = only }, .{});
+ pub const Class = NewClass(
+ void,
+ .{ .name = "test" },
+ .{
+ .call = call,
+ .only = only,
+ .skip = skip,
+ },
+ .{},
+ );
pub const Counter = struct {
expected: u32 = 0,
@@ -1262,7 +1279,19 @@ pub const TestScope = struct {
arguments: []const js.JSValueRef,
exception: js.ExceptionRef,
) js.JSObjectRef {
- return callMaybeOnly(this, ctx, arguments, exception, true);
+ return prepare(this, ctx, arguments, exception, .only);
+ }
+
+ pub fn skip(
+ // the DescribeScope here is the top of the file, not the real one
+ _: void,
+ ctx: js.JSContextRef,
+ this: js.JSObjectRef,
+ _: js.JSObjectRef,
+ arguments: []const js.JSValueRef,
+ exception: js.ExceptionRef,
+ ) js.JSObjectRef {
+ return prepare(this, ctx, arguments, exception, .skip);
}
pub fn call(
@@ -1274,15 +1303,15 @@ pub const TestScope = struct {
arguments: []const js.JSValueRef,
exception: js.ExceptionRef,
) js.JSObjectRef {
- return callMaybeOnly(this, ctx, arguments, exception, false);
+ return prepare(this, ctx, arguments, exception, .call);
}
- fn callMaybeOnly(
+ fn prepare(
this: js.JSObjectRef,
ctx: js.JSContextRef,
arguments: []const js.JSValueRef,
exception: js.ExceptionRef,
- is_only: bool,
+ comptime tag: @Type(.EnumLiteral),
) js.JSObjectRef {
var args = bun.cast([]const JSC.JSValue, arguments[0..@min(arguments.len, 2)]);
var label: string = "";
@@ -1309,12 +1338,20 @@ pub const TestScope = struct {
return this;
}
- if (is_only) {
+ if (tag == .only) {
Jest.runner.?.setOnly();
}
- if (!is_only and Jest.runner.?.only)
+ if (tag == .skip or (tag != .only and Jest.runner.?.only)) {
+ DescribeScope.active.skipped_counter += 1;
+ DescribeScope.active.tests.append(getAllocator(ctx), TestScope{
+ .label = label,
+ .parent = DescribeScope.active,
+ .skipped = true,
+ .callback = null,
+ }) catch unreachable;
return this;
+ }
js.JSValueProtect(ctx, function.asObjectRef());
@@ -1493,6 +1530,11 @@ pub const DescribeScope = struct {
current_test_id: TestRunner.Test.ID = 0,
value: JSValue = .zero,
done: bool = false,
+ skipped_counter: u32 = 0,
+
+ pub fn isAllSkipped(this: *const DescribeScope) bool {
+ return @as(usize, this.skipped_counter) >= this.tests.items.len;
+ }
pub fn push(new: *DescribeScope) void {
if (comptime is_bindgen) return undefined;
@@ -1752,15 +1794,17 @@ pub const DescribeScope = struct {
var i: TestRunner.Test.ID = 0;
- const beforeAll = this.runCallback(ctx, .beforeAll);
- if (!beforeAll.isEmpty()) {
- while (i < end) {
- Jest.runner.?.reportFailure(i + this.test_id_start, source.path.text, tests[i].label, 0, this);
- i += 1;
+ if (!this.isAllSkipped()) {
+ const beforeAll = this.runCallback(ctx, .beforeAll);
+ if (!beforeAll.isEmpty()) {
+ while (i < end) {
+ Jest.runner.?.reportFailure(i + this.test_id_start, source.path.text, tests[i].label, 0, this);
+ i += 1;
+ }
+ this.tests.clearAndFree(allocator);
+ this.pending_tests.deinit(allocator);
+ return;
}
- this.tests.clearAndFree(allocator);
- this.pending_tests.deinit(allocator);
- return;
}
while (i < end) : (i += 1) {
@@ -1778,24 +1822,29 @@ pub const DescribeScope = struct {
}
}
- pub fn onTestComplete(this: *DescribeScope, globalThis: *JSC.JSGlobalObject, test_id: TestRunner.Test.ID) void {
+ pub fn onTestComplete(this: *DescribeScope, globalThis: *JSC.JSGlobalObject, test_id: TestRunner.Test.ID, skipped: bool) void {
// invalidate it
this.current_test_id = std.math.maxInt(TestRunner.Test.ID);
this.pending_tests.unset(test_id);
- const afterEach = this.execCallback(globalThis, .afterEach);
- if (!afterEach.isEmpty()) {
- globalThis.bunVM().runErrorHandler(afterEach, null);
+ if (!skipped) {
+ const afterEach = this.execCallback(globalThis, .afterEach);
+ if (!afterEach.isEmpty()) {
+ globalThis.bunVM().runErrorHandler(afterEach, null);
+ }
}
if (this.pending_tests.findFirstSet() != null) {
return;
}
- // Step 1. Run the afterAll callbacks, in reverse order
- const afterAll = this.execCallback(globalThis, .afterAll);
- if (!afterAll.isEmpty()) {
- globalThis.bunVM().runErrorHandler(afterAll, null);
+ if (!this.isAllSkipped()) {
+ // Run the afterAll callbacks, in reverse order
+ // unless there were no tests for this scope
+ const afterAll = this.execCallback(globalThis, .afterAll);
+ if (!afterAll.isEmpty()) {
+ globalThis.bunVM().runErrorHandler(afterAll, null);
+ }
}
this.pending_tests.deinit(getAllocator(globalThis));
@@ -1907,16 +1956,21 @@ pub const TestRunnerTask = struct {
DescribeScope.active = describe;
active_test_expectation_counter = .{};
- describe.current_test_id = this.test_id;
+ const test_id = this.test_id;
+ var test_: TestScope = this.describe.tests.items[test_id];
+ describe.current_test_id = test_id;
var globalThis = this.globalThis;
- globalThis.bunVM().onUnhandledRejectionCtx = this;
- var test_: TestScope = this.describe.tests.items[this.test_id];
- const label = this.describe.tests.items[this.test_id].label;
+ if (test_.skipped) {
+ this.processTestResult(globalThis, .{ .skip = {} }, test_, test_id, describe);
+ this.deinit();
+ return false;
+ }
- const test_id = this.test_id;
+ globalThis.bunVM().onUnhandledRejectionCtx = this;
if (this.needs_before_each) {
this.needs_before_each = false;
+ const label = test_.label;
const beforeEach = this.describe.runCallback(globalThis, .beforeEach);
@@ -1986,20 +2040,21 @@ pub const TestRunnerTask = struct {
this.reported = true;
- var globalThis = this.globalThis;
- var test_ = this.describe.tests.items[this.test_id];
- const label = this.describe.tests.items[this.test_id].label;
const test_id = this.test_id;
+ var test_ = this.describe.tests.items[test_id];
var describe = this.describe;
+ describe.tests.items[test_id] = test_;
+ processTestResult(this, this.globalThis, result, test_, test_id, describe);
+ }
- describe.tests.items[this.test_id] = test_;
+ fn processTestResult(this: *TestRunnerTask, globalThis: *JSC.JSGlobalObject, result: Result, test_: TestScope, test_id: u32, describe: *DescribeScope) void {
switch (result) {
- .pass => |count| Jest.runner.?.reportPass(test_id, this.source.path.text, label, count, describe),
- .fail => |count| Jest.runner.?.reportFailure(test_id, this.source.path.text, label, count, describe),
+ .pass => |count| Jest.runner.?.reportPass(test_id, this.source.path.text, test_.label, count, describe),
+ .fail => |count| Jest.runner.?.reportFailure(test_id, this.source.path.text, test_.label, count, describe),
+ .skip => Jest.runner.?.reportSkip(test_id, this.source.path.text, test_.label, describe),
.pending => @panic("Unexpected pending test"),
}
- describe.onTestComplete(globalThis, this.test_id);
-
+ describe.onTestComplete(globalThis, test_id, result == .skip);
Jest.runner.?.runNextTest();
}
@@ -2021,4 +2076,5 @@ pub const Result = union(TestRunner.Test.Status) {
fail: u32,
pass: u32, // assertion count
pending: void,
+ skip: void,
};
diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig
index 66b9e2ec3..1f9fae3fe 100644
--- a/src/cli/test_command.zig
+++ b/src/cli/test_command.zig
@@ -44,6 +44,23 @@ const TestRunner = JSC.Jest.TestRunner;
const Test = TestRunner.Test;
const NetworkThread = @import("bun").HTTP.NetworkThread;
const uws = @import("bun").uws;
+
+fn fmtStatusTextLine(comptime status: @Type(.EnumLiteral), comptime emoji: bool) []const u8 {
+ return switch (comptime status) {
+ .pass => comptime Output.prettyFmt("<green>✓<r>", emoji),
+ .fail => comptime Output.prettyFmt("<r><red>✗<r>", emoji),
+ .skip => comptime Output.prettyFmt("<r><yellow>-", emoji),
+ else => @compileError("Invalid status " ++ @tagName(status)),
+ };
+}
+
+fn writeTestStatusLine(comptime status: @Type(.EnumLiteral), writer: anytype) void {
+ if (Output.enable_ansi_colors_stderr)
+ writer.print(fmtStatusTextLine(status, true), .{}) catch unreachable
+ else
+ writer.print(fmtStatusTextLine(status, false), .{}) catch unreachable;
+}
+
pub const CommandLineReporter = struct {
jest: TestRunner,
callback: TestRunner.Callback,
@@ -52,10 +69,12 @@ pub const CommandLineReporter = struct {
prev_file: u64 = 0,
failures_to_repeat_buf: std.ArrayListUnmanaged(u8) = .{},
+ skips_to_repeat_buf: std.ArrayListUnmanaged(u8) = .{},
pub const Summary = struct {
pass: u32 = 0,
expectations: u32 = 0,
+ skip: u32 = 0,
fail: u32 = 0,
};
@@ -76,7 +95,7 @@ pub const CommandLineReporter = struct {
// var this: *CommandLineReporter = @fieldParentPtr(CommandLineReporter, "callback", cb);
}
- fn printTestLine(label: string, parent: ?*Jest.DescribeScope, writer: anytype) void {
+ fn printTestLine(label: string, parent: ?*Jest.DescribeScope, comptime skip: bool, writer: anytype) void {
var scopes_stack = std.BoundedArray(*Jest.DescribeScope, 64).init(0) catch unreachable;
var parent_ = parent;
@@ -89,12 +108,14 @@ pub const CommandLineReporter = struct {
const display_label = if (label.len > 0) label else "test";
+ const color_code = comptime if (skip) "<yellow>" else "";
+
if (Output.enable_ansi_colors_stderr) {
for (scopes) |scope| {
if (scope.label.len == 0) continue;
writer.writeAll(" ") catch unreachable;
- writer.print(comptime Output.prettyFmt("<r>", true), .{}) catch unreachable;
+ writer.print(comptime Output.prettyFmt("<r>" ++ color_code, true), .{}) catch unreachable;
writer.writeAll(scope.label) catch unreachable;
writer.print(comptime Output.prettyFmt("<d>", true), .{}) catch unreachable;
writer.writeAll(" >") catch unreachable;
@@ -108,10 +129,12 @@ pub const CommandLineReporter = struct {
}
}
+ const line_color_code = if (comptime skip) "<r><yellow>" else "<r><b>";
+
if (Output.enable_ansi_colors_stderr)
- writer.print(comptime Output.prettyFmt("<r><b> {s}<r>", true), .{display_label}) catch unreachable
+ writer.print(comptime Output.prettyFmt(line_color_code ++ " {s}<r>", true), .{display_label}) catch unreachable
else
- writer.print(comptime Output.prettyFmt("<r><b> {s}<r>", false), .{display_label}) catch unreachable;
+ writer.print(comptime Output.prettyFmt(" {s}", false), .{display_label}) catch unreachable;
writer.writeAll("\n") catch unreachable;
}
@@ -124,12 +147,9 @@ pub const CommandLineReporter = struct {
var this: *CommandLineReporter = @fieldParentPtr(CommandLineReporter, "callback", cb);
- if (Output.enable_ansi_colors_stderr)
- writer.print(comptime Output.prettyFmt("<green>✓<r>", true), .{}) catch unreachable
- else
- writer.print(comptime Output.prettyFmt("<green>✓<r>", false), .{}) catch unreachable;
+ writeTestStatusLine(.pass, &writer);
- printTestLine(label, parent, writer);
+ printTestLine(label, parent, false, writer);
this.jest.tests.items(.status)[id] = TestRunner.Test.Status.pass;
this.summary.pass += 1;
@@ -145,12 +165,8 @@ pub const CommandLineReporter = struct {
const initial_length = this.failures_to_repeat_buf.items.len;
var writer = this.failures_to_repeat_buf.writer(bun.default_allocator);
- if (Output.enable_ansi_colors_stderr)
- writer.print(comptime Output.prettyFmt("<r><red>✗<r>", true), .{}) catch unreachable
- else
- writer.print(comptime Output.prettyFmt("<r><red>✗<r>", false), .{}) catch unreachable;
-
- printTestLine(label, parent, writer);
+ writeTestStatusLine(.fail, &writer);
+ printTestLine(label, parent, false, writer);
writer_.writeAll(this.failures_to_repeat_buf.items[initial_length..]) catch unreachable;
Output.flush();
@@ -160,6 +176,27 @@ pub const CommandLineReporter = struct {
this.summary.expectations += expectations;
this.jest.tests.items(.status)[id] = TestRunner.Test.Status.fail;
}
+
+ pub fn handleTestSkip(cb: *TestRunner.Callback, id: Test.ID, _: string, label: string, expectations: u32, parent: ?*Jest.DescribeScope) void {
+ var writer_: std.fs.File.Writer = Output.errorWriter();
+ var this: *CommandLineReporter = @fieldParentPtr(CommandLineReporter, "callback", cb);
+
+ // 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
+ const initial_length = this.skips_to_repeat_buf.items.len;
+ var writer = this.skips_to_repeat_buf.writer(bun.default_allocator);
+
+ writeTestStatusLine(.skip, &writer);
+ printTestLine(label, parent, true, writer);
+
+ writer_.writeAll(this.skips_to_repeat_buf.items[initial_length..]) catch unreachable;
+ Output.flush();
+
+ // this.updateDots();
+ this.summary.skip += 1;
+ this.summary.expectations += expectations;
+ this.jest.tests.items(.status)[id] = TestRunner.Test.Status.skip;
+ }
};
const Scanner = struct {
@@ -328,6 +365,7 @@ pub const TestCommand = struct {
.onTestStart = CommandLineReporter.handleTestStart,
.onTestPass = CommandLineReporter.handleTestPass,
.onTestFail = CommandLineReporter.handleTestFail,
+ .onTestSkip = CommandLineReporter.handleTestSkip,
};
reporter.jest.callback = &reporter.callback;
Jest.Jest.runner = &reporter.jest;
@@ -351,7 +389,15 @@ pub const TestCommand = struct {
.filter_names = ctx.positionals[1..],
.results = std.ArrayList(PathString).init(ctx.allocator),
};
- scanner.scan(scanner.fs.top_level_dir);
+ const dir_to_scan = brk: {
+ if (ctx.debug.test_directory.len > 0) {
+ break :brk try vm.allocator.dupe(u8, resolve_path.joinAbs(scanner.fs.top_level_dir, .auto, ctx.debug.test_directory));
+ }
+
+ break :brk scanner.fs.top_level_dir;
+ };
+
+ scanner.scan(dir_to_scan);
scanner.dirs_to_scan.deinit();
const test_files = try scanner.results.toOwnedSlice();
@@ -360,13 +406,26 @@ pub const TestCommand = struct {
runAllTests(reporter, vm, test_files, ctx.allocator);
}
- if (reporter.summary.pass > 20 and reporter.summary.fail > 0) {
- Output.prettyError("\n<r><d>{d} tests failed<r>:\n", .{reporter.summary.fail});
+ if (reporter.summary.pass > 20) {
+ if (reporter.summary.skip > 0) {
+ Output.prettyError("\n<r><d>{d} tests skipped:<r>\n", .{reporter.summary.skip});
+ Output.flush();
- Output.flush();
+ var error_writer = Output.errorWriter();
+ error_writer.writeAll(reporter.skips_to_repeat_buf.items) catch unreachable;
+ }
+
+ if (reporter.summary.fail > 0) {
+ if (reporter.summary.skip > 0) {
+ Output.prettyError("\n", .{});
+ }
- var error_writer = Output.errorWriter();
- error_writer.writeAll(reporter.failures_to_repeat_buf.items) catch unreachable;
+ Output.prettyError("\n<r><d>{d} tests failed:<r>\n", .{reporter.summary.fail});
+ Output.flush();
+
+ var error_writer = Output.errorWriter();
+ error_writer.writeAll(reporter.failures_to_repeat_buf.items) catch unreachable;
+ }
}
Output.flush();
@@ -395,6 +454,10 @@ pub const TestCommand = struct {
Output.prettyError(" {d:5>} pass<r>\n", .{reporter.summary.pass});
+ if (reporter.summary.skip > 0) {
+ Output.prettyError(" <r><yellow>{d:5>} skip<r>\n", .{reporter.summary.skip});
+ }
+
if (reporter.summary.fail > 0) {
Output.prettyError("<r><red>", .{});
} else {