aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Degreat <mail@degreat.co.uk> 2023-05-21 02:03:55 +0000
committerGravatar GitHub <noreply@github.com> 2023-05-20 22:03:55 -0400
commit0e97f91f9f4ba1b7a1a24dd33c568a159c755850 (patch)
tree495853d3fe6c3fe95f03609125b6629648786e48
parent367f3a9c816580bbe89ddfb8c66de5d4ed5987c3 (diff)
downloadbun-0e97f91f9f4ba1b7a1a24dd33c568a159c755850.tar.gz
bun-0e97f91f9f4ba1b7a1a24dd33c568a159c755850.tar.zst
bun-0e97f91f9f4ba1b7a1a24dd33c568a159c755850.zip
Implement `test.todo` (#2961)
* Implement `test.todo` * remove skip condition * Allow callbacks in .todo * Add descriptive comment * Log todos * Include tests in title * edit test.todo tests --------- Co-authored-by: dave caruso <me@paperdave.net>
-rw-r--r--packages/bun-types/bun-test.d.ts17
-rw-r--r--src/bun.js/test/jest.zig73
-rw-r--r--src/cli/test_command.zig49
-rw-r--r--test/js/bun/test/test-test.test.ts32
-rw-r--r--test/js/bun/test/todo-test-fixture-2.js6
-rw-r--r--test/js/bun/test/todo-test-fixture.js9
6 files changed, 177 insertions, 9 deletions
diff --git a/packages/bun-types/bun-test.d.ts b/packages/bun-types/bun-test.d.ts
index 4674c9185..b66f42fe8 100644
--- a/packages/bun-types/bun-test.d.ts
+++ b/packages/bun-types/bun-test.d.ts
@@ -182,6 +182,23 @@ declare module "bun:test" {
| ((done: (err?: unknown) => void) => void),
timeoutMs?: number,
): 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.
diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig
index 9f96fc1ce..f84106085 100644
--- a/src/bun.js/test/jest.zig
+++ b/src/bun.js/test/jest.zig
@@ -469,6 +469,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 {
@@ -485,6 +486,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);
@@ -532,6 +538,7 @@ pub const TestRunner = struct {
pass,
fail,
skip,
+ todo,
};
};
};
@@ -3159,6 +3166,7 @@ pub const TestScope = struct {
ran: bool = false,
task: ?*TestRunnerTask = null,
skipped: bool = false,
+ is_todo: bool = false,
snapshot_count: usize = 0,
timeout_millis: u32 = default_timeout,
@@ -3169,6 +3177,7 @@ pub const TestScope = struct {
.call = call,
.only = only,
.skip = skip,
+ .todo = todo,
},
.{},
);
@@ -3214,6 +3223,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,
@@ -3240,16 +3260,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{
@@ -3369,10 +3409,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 };
}
@@ -3391,10 +3436,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 => {
@@ -3427,6 +3477,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 };
}
@@ -3465,6 +3520,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;
@@ -3940,6 +3996,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();
@@ -4067,6 +4130,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);
@@ -4101,4 +4165,5 @@ pub const Result = union(TestRunner.Test.Status) {
pass: u32, // assertion count
pending: void,
skip: void,
+ todo: void,
};
diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig
index fb73b0a15..548ed2dc6 100644
--- a/src/cli/test_command.zig
+++ b/src/cli/test_command.zig
@@ -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)),
};
}
@@ -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,
};
@@ -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 {
@@ -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;
@@ -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();
@@ -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 {
@@ -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 });
Output.printStartEnd(ctx.start_time, std.time.nanoTimestamp());
}
diff --git a/test/js/bun/test/test-test.test.ts b/test/js/bun/test/test-test.test.ts
index f2c22fbcf..d3e860e1f 100644
--- a/test/js/bun/test/test-test.test.ts
+++ b/test/js/bun/test/test-test.test.ts
@@ -2760,6 +2760,38 @@ describe(() => {
});
});
+it("test.todo", () => {
+ const path = join(realpathSync(tmpdir()), "todo-test.test.js");
+ copyFileSync(join(import.meta.dir, "todo-test-fixture.js"), path);
+ const { stdout, stderr, exitCode } = spawnSync({
+ cmd: [bunExe(), "test", path],
+ stdout: "pipe",
+ stderr: "pipe",
+ env: bunEnv,
+ cwd: realpathSync(dirname(path)),
+ });
+
+ const err = stderr!.toString();
+ expect(err).toContain("this test is marked as todo but passes");
+ expect(err).toContain("2 todo");
+ expect(err).toContain("1 fail");
+});
+
+it("test.todo doesnt cause exit code 1", () => {
+ const path = join(realpathSync(tmpdir()), "todo-test.test.js");
+ copyFileSync(join(import.meta.dir, "todo-test-fixture-2.js"), path);
+ const { stdout, stderr, exitCode } = spawnSync({
+ cmd: [bunExe(), "test", path],
+ stdout: "pipe",
+ stderr: "pipe",
+ env: bunEnv,
+ cwd: realpathSync(dirname(path)),
+ });
+
+ const err = stderr!.toString();
+ expect(exitCode).toBe(0);
+});
+
it("test timeouts when expected", () => {
const path = join(realpathSync(tmpdir()), "test-timeout.test.js");
copyFileSync(join(import.meta.dir, "timeout-test-fixture.js"), path);
diff --git a/test/js/bun/test/todo-test-fixture-2.js b/test/js/bun/test/todo-test-fixture-2.js
new file mode 100644
index 000000000..b2e552420
--- /dev/null
+++ b/test/js/bun/test/todo-test-fixture-2.js
@@ -0,0 +1,6 @@
+import { test } from "bun:test";
+
+test.todo("todo 1")
+test.todo("todo 2", () => {
+ throw new Error("this error is shown");
+})
diff --git a/test/js/bun/test/todo-test-fixture.js b/test/js/bun/test/todo-test-fixture.js
new file mode 100644
index 000000000..b1264ee3a
--- /dev/null
+++ b/test/js/bun/test/todo-test-fixture.js
@@ -0,0 +1,9 @@
+import { test } from "bun:test";
+
+test.todo("todo 1")
+test.todo("todo 2", () => {
+ throw new Error("this error is shown");
+})
+test.todo("todo 3", () => {
+ // passes
+});