Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement test.todo #2961

Merged
merged 8 commits into from May 21, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 17 additions & 0 deletions packages/bun-types/bun-test.d.ts
Expand Up @@ -170,6 +170,23 @@ declare module "bun:test" {
| (() => void | Promise<unknown>)
| ((done: (err?: unknown) => void) => void),
): void;
/**
* Indicate a test is yet to be written or implemented correctly.
*
* When a test function is passed, it will be marked as `todo` in the test results
* as long the test does not pass. When the test passes, the test will be marked as
* `fail` in the results; you will have to remove the `.todo` or check that your test
* is implemented correctly.
*
* @param label the label for the test
* @param fn the test function
*/
todo(
label: string,
fn?:
| (() => void | Promise<unknown>)
| ((done: (err?: unknown) => void) => void),
): void;
};
/**
* Runs a test.
Expand Down
73 changes: 69 additions & 4 deletions src/bun.js/test/jest.zig
Expand Up @@ -429,6 +429,7 @@ pub const TestRunner = struct {
onTestPass: OnTestUpdate,
onTestFail: OnTestUpdate,
onTestSkip: OnTestUpdate,
onTestTodo: OnTestUpdate,
};

pub fn reportPass(this: *TestRunner, test_id: Test.ID, file: string, label: string, expectations: u32, elapsed_ns: u64, parent: ?*DescribeScope) void {
Expand All @@ -445,6 +446,11 @@ pub const TestRunner = struct {
this.callback.onTestSkip(this.callback, test_id, file, label, 0, 0, parent);
}

pub fn reportTodo(this: *TestRunner, test_id: Test.ID, file: string, label: string, parent: ?*DescribeScope) void {
this.tests.items(.status)[test_id] = .todo;
this.callback.onTestTodo(this.callback, test_id, file, label, 0, 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);
Expand Down Expand Up @@ -492,6 +498,7 @@ pub const TestRunner = struct {
pass,
fail,
skip,
todo,
};
};
};
Expand Down Expand Up @@ -3119,6 +3126,7 @@ pub const TestScope = struct {
ran: bool = false,
task: ?*TestRunnerTask = null,
skipped: bool = false,
is_todo: bool = false,
snapshot_count: usize = 0,

pub const Class = NewClass(
Expand All @@ -3128,6 +3136,7 @@ pub const TestScope = struct {
.call = call,
.only = only,
.skip = skip,
.todo = todo,
},
.{},
);
Expand Down Expand Up @@ -3173,6 +3182,17 @@ pub const TestScope = struct {
return prepare(this, ctx, arguments, exception, .call);
}

pub fn todo(
_: void,
ctx: js.JSContextRef,
this: js.JSObjectRef,
_: js.JSObjectRef,
arguments: []const js.JSValueRef,
exception: js.ExceptionRef,
) js.JSObjectRef {
return prepare(this, ctx, arguments, exception, .todo);
}

fn prepare(
this: js.JSObjectRef,
ctx: js.JSContextRef,
Expand All @@ -3199,16 +3219,36 @@ pub const TestScope = struct {
label = (label_value.toSlice(ctx, allocator).cloneIfNeeded(allocator) catch unreachable).slice();
}

if (tag == .todo and label_value == .zero) {
JSError(getAllocator(ctx), "test.todo() requires a description", .{}, ctx, exception);
return this;
}

const function = function_value;
if (function.isEmptyOrUndefinedOrNull() or !function.isCell() or !function.isCallable(ctx.vm())) {
JSError(getAllocator(ctx), "test() expects a function", .{}, ctx, exception);
return this;
// a callback is not required for .todo
if (tag != .todo) {
JSError(getAllocator(ctx), "test() expects a function", .{}, ctx, exception);
return this;
}
}

if (tag == .only) {
Jest.runner.?.setOnly();
}

if (tag == .todo) {
DescribeScope.active.todo_counter += 1;
DescribeScope.active.tests.append(getAllocator(ctx), TestScope{
.label = label,
.parent = DescribeScope.active,
.is_todo = true,
.callback = if (function == .zero) null else function.asObjectRef(),
}) catch unreachable;

return this;
}

if (tag == .skip or (tag != .only and Jest.runner.?.only)) {
DescribeScope.active.skipped_counter += 1;
DescribeScope.active.tests.append(getAllocator(ctx), TestScope{
Expand Down Expand Up @@ -3321,10 +3361,15 @@ pub const TestScope = struct {

if (initial_value.isAnyError()) {
if (!Jest.runner.?.did_pending_test_fail) {
Jest.runner.?.did_pending_test_fail = true;
// test failed unless it's a todo
Jest.runner.?.did_pending_test_fail = !this.is_todo;
vm.runErrorHandler(initial_value, null);
}

if (this.is_todo) {
return .{ .todo = {} };
}

return .{ .fail = active_test_expectation_counter.actual };
}

Expand All @@ -3343,10 +3388,15 @@ pub const TestScope = struct {
switch (promise.status(vm.global.vm())) {
.Rejected => {
if (!Jest.runner.?.did_pending_test_fail) {
Jest.runner.?.did_pending_test_fail = true;
// test failed unless it's a todo
Jest.runner.?.did_pending_test_fail = !this.is_todo;
vm.runErrorHandler(promise.result(vm.global.vm()), null);
}

if (this.is_todo) {
return .{ .todo = {} };
}

return .{ .fail = active_test_expectation_counter.actual };
},
.Pending => {
Expand Down Expand Up @@ -3379,6 +3429,11 @@ pub const TestScope = struct {
return .{ .fail = active_test_expectation_counter.actual };
}

if (this.is_todo) {
Output.prettyErrorln(" <d>^<r> <red>this test is marked as todo but passes.<r> <d>Remove `.todo` or check that test is correct.<r>", .{});
return .{ .fail = active_test_expectation_counter.actual };
}

return .{ .pass = active_test_expectation_counter.actual };
}

Expand Down Expand Up @@ -3417,6 +3472,7 @@ pub const DescribeScope = struct {
done: bool = false,
skipped: bool = false,
skipped_counter: u32 = 0,
todo_counter: u32 = 0,

pub fn isAllSkipped(this: *const DescribeScope) bool {
return this.skipped or @as(usize, this.skipped_counter) >= this.tests.items.len;
Expand Down Expand Up @@ -3888,6 +3944,13 @@ pub const TestRunnerTask = struct {
var test_: TestScope = this.describe.tests.items[test_id];
describe.current_test_id = test_id;
var globalThis = this.globalThis;

if (!describe.skipped and test_.is_todo and test_.callback == null) {
this.processTestResult(globalThis, .{ .todo = {} }, test_, test_id, describe);
this.deinit();
return false;
}

if (test_.skipped or describe.skipped) {
this.processTestResult(globalThis, .{ .skip = {} }, test_, test_id, describe);
this.deinit();
Expand Down Expand Up @@ -4003,6 +4066,7 @@ pub const TestRunnerTask = struct {
describe,
),
.skip => Jest.runner.?.reportSkip(test_id, this.source_file_path, test_.label, describe),
.todo => Jest.runner.?.reportTodo(test_id, this.source_file_path, test_.label, describe),
.pending => @panic("Unexpected pending test"),
}
describe.onTestComplete(globalThis, test_id, result == .skip);
Expand Down Expand Up @@ -4037,4 +4101,5 @@ pub const Result = union(TestRunner.Test.Status) {
pass: u32, // assertion count
pending: void,
skip: void,
todo: void,
};
49 changes: 44 additions & 5 deletions src/cli/test_command.zig
Expand Up @@ -52,6 +52,7 @@ fn fmtStatusTextLine(comptime status: @Type(.EnumLiteral), comptime emoji: bool)
.pass => Output.prettyFmt("<r><green>✓<r>", emoji),
.fail => Output.prettyFmt("<r><red>✗<r>", emoji),
.skip => Output.prettyFmt("<r><yellow>-<d>", emoji),
.todo => Output.prettyFmt("<r><magenta>✎<r>", emoji),
else => @compileError("Invalid status " ++ @tagName(status)),
};
}
Expand All @@ -74,11 +75,13 @@ pub const CommandLineReporter = struct {

failures_to_repeat_buf: std.ArrayListUnmanaged(u8) = .{},
skips_to_repeat_buf: std.ArrayListUnmanaged(u8) = .{},
todos_to_repeat_buf: std.ArrayListUnmanaged(u8) = .{},

pub const Summary = struct {
pass: u32 = 0,
expectations: u32 = 0,
skip: u32 = 0,
todo: u32 = 0,
fail: u32 = 0,
};

Expand Down Expand Up @@ -217,6 +220,27 @@ pub const CommandLineReporter = struct {
this.summary.expectations += expectations;
this.jest.tests.items(.status)[id] = TestRunner.Test.Status.skip;
}

pub fn handleTestTodo(cb: *TestRunner.Callback, id: Test.ID, _: string, label: string, expectations: u32, elapsed_ns: u64, parent: ?*jest.DescribeScope) void {
var writer_: std.fs.File.Writer = Output.errorWriter();
var this: *CommandLineReporter = @fieldParentPtr(CommandLineReporter, "callback", cb);

// when the tests skip, 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.todos_to_repeat_buf.items.len;
var writer = this.todos_to_repeat_buf.writer(bun.default_allocator);

writeTestStatusLine(.todo, &writer);
printTestLine(label, elapsed_ns, parent, true, writer);

writer_.writeAll(this.todos_to_repeat_buf.items[initial_length..]) catch unreachable;
Output.flush();

// this.updateDots();
this.summary.todo += 1;
this.summary.expectations += expectations;
this.jest.tests.items(.status)[id] = TestRunner.Test.Status.todo;
}
};

const Scanner = struct {
Expand Down Expand Up @@ -405,6 +429,7 @@ pub const TestCommand = struct {
.onTestPass = CommandLineReporter.handleTestPass,
.onTestFail = CommandLineReporter.handleTestFail,
.onTestSkip = CommandLineReporter.handleTestSkip,
.onTestTodo = CommandLineReporter.handleTestTodo,
};
reporter.repeat_count = @max(ctx.test_options.repeat_count, 1);
reporter.jest.callback = &reporter.callback;
Expand Down Expand Up @@ -473,11 +498,23 @@ pub const TestCommand = struct {
error_writer.writeAll(reporter.skips_to_repeat_buf.items) catch unreachable;
}

if (reporter.summary.fail > 0) {
if (reporter.summary.todo > 0) {
if (reporter.summary.skip > 0) {
Output.prettyError("\n", .{});
}

Output.prettyError("\n<r><d>{d} tests todo:<r>\n", .{reporter.summary.todo});
Output.flush();

var error_writer = Output.errorWriter();
error_writer.writeAll(reporter.todos_to_repeat_buf.items) catch unreachable;
}

if (reporter.summary.fail > 0) {
if (reporter.summary.skip > 0 or reporter.summary.todo > 0) {
Output.prettyError("\n", .{});
}

Output.prettyError("\n<r><d>{d} tests failed:<r>\n", .{reporter.summary.fail});
Output.flush();

Expand Down Expand Up @@ -516,6 +553,10 @@ pub const TestCommand = struct {
Output.prettyError(" <r><yellow>{d:5>} skip<r>\n", .{reporter.summary.skip});
}

if (reporter.summary.todo > 0) {
Output.prettyError(" <r><magenta>{d:5>} todo<r>\n", .{reporter.summary.todo});
}

if (reporter.summary.fail > 0) {
Output.prettyError("<r><red>", .{});
} else {
Expand Down Expand Up @@ -567,10 +608,8 @@ pub const TestCommand = struct {
Output.prettyError(" {d:5>} expect() calls\n", .{reporter.summary.expectations});
}

Output.prettyError("Ran {d} tests across {d} files ", .{
reporter.summary.fail + reporter.summary.pass,
test_files.len,
});
const total_tests = reporter.summary.fail + reporter.summary.pass + reporter.summary.skip + reporter.summary.todo;
Output.prettyError("Ran {d} tests across {d} files. <d>{d} total<r> ", .{ reporter.summary.fail + reporter.summary.pass, test_files.len, total_tests });
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added total count

Output.printStartEnd(ctx.start_time, std.time.nanoTimestamp());
}

Expand Down
13 changes: 13 additions & 0 deletions test/js/bun/test/test-test.test.ts
Expand Up @@ -2758,3 +2758,16 @@ describe(() => {
});
});
});

describe("todo", () => {
test.todo("implement this feature", () => {
throw new Error();
});

test.todo("make this test pass", () => {
expect(5).toBe(5);
expect(4).toBe(3);
});

it.todo("implement this test");
});