diff options
author | 2023-01-09 08:25:39 -0800 | |
---|---|---|
committer | 2023-01-09 08:25:39 -0800 | |
commit | a1b2c23671d2ae1c2263b1227dc1cd6f071433d5 (patch) | |
tree | 7a62e07b8f5ef2b6141d2a2ae0854d0e9eb6281e | |
parent | 5d60aae3b3ba39fc1d48469d46161f53cfcb37e4 (diff) | |
download | bun-a1b2c23671d2ae1c2263b1227dc1cd6f071433d5.tar.gz bun-a1b2c23671d2ae1c2263b1227dc1cd6f071433d5.tar.zst bun-a1b2c23671d2ae1c2263b1227dc1cd6f071433d5.zip |
[bun:test] Implement `test.skip`
-rw-r--r-- | packages/bun-types/bun-test.d.ts | 42 | ||||
-rw-r--r-- | src/bun.js/test/jest.zig | 128 | ||||
-rw-r--r-- | src/cli/test_command.zig | 105 |
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 { |