test(node): get test-assert.js working (#15698)

Co-authored-by: Don Isaac <don@bun.sh>
Co-authored-by: DonIsaac <DonIsaac@users.noreply.github.com>
This commit is contained in:
Don Isaac
2025-01-09 18:45:43 -06:00
committed by GitHub
parent 7bcd825d13
commit 0372ca5c0a
30 changed files with 4588 additions and 1304 deletions

4
bunfig.node-test.toml Normal file
View File

@@ -0,0 +1,4 @@
# FIXME: move this back to test/js/node
# https://github.com/oven-sh/bun/issues/16289
[test]
preload = ["./test/js/node/harness.ts", "./test/preload.ts"]

View File

@@ -63,6 +63,7 @@ const { values: options, positionals: filters } = parseArgs({
type: "boolean",
default: false,
},
/** Path to bun binary */
["exec-path"]: {
type: "string",
default: "bun",
@@ -252,15 +253,19 @@ async function runTests() {
if (!failedResults.length) {
for (const testPath of tests) {
const title = relative(cwd, join(testsPath, testPath)).replace(/\\/g, "/");
const absoluteTestPath = join(testsPath, testPath);
const title = relative(cwd, absoluteTestPath).replaceAll(sep, "/");
if (isNodeParallelTest(testPath)) {
const subcommand = title.includes("needs-test") ? "test" : "run";
await runTest(title, async () => {
const { ok, error, stdout } = await spawnBun(execPath, {
cwd: cwd,
args: [title],
args: [subcommand, "--config=./bunfig.node-test.toml", absoluteTestPath],
timeout: getNodeParallelTestTimeout(title),
env: {
FORCE_COLOR: "0",
NO_COLOR: "1",
BUN_DEBUG_QUIET_LOGS: "1",
},
stdout: chunk => pipeTestStdout(process.stdout, chunk),
stderr: chunk => pipeTestStdout(process.stderr, chunk),
@@ -278,11 +283,11 @@ async function runTests() {
stdoutPreview: stdoutPreview,
};
});
continue;
}
} else {
await runTest(title, async () => spawnBunTest(execPath, join("test", testPath)));
}
}
}
if (vendorTests?.length) {
for (const { cwd: vendorPath, packageManager, testRunner, testPaths } of vendorTests) {
@@ -537,7 +542,7 @@ async function spawnSafe(options) {
}
/**
* @param {string} execPath
* @param {string} execPath Path to bun binary
* @param {SpawnOptions} options
* @returns {Promise<SpawnResult>}
*/
@@ -565,9 +570,11 @@ async function spawnBun(execPath, { args, cwd, timeout, env, stdout, stderr }) {
// Used in Node.js tests.
TEST_TMPDIR: tmpdirPath,
};
if (env) {
Object.assign(bunEnv, env);
}
if (isWindows) {
delete bunEnv["PATH"];
bunEnv["Path"] = path;
@@ -862,7 +869,7 @@ function isJavaScriptTest(path) {
* @returns {boolean}
*/
function isNodeParallelTest(testPath) {
return testPath.replaceAll(sep, "/").includes("js/node/test/parallel/")
return testPath.replaceAll(sep, "/").includes("js/node/test/parallel/");
}
/**
@@ -892,6 +899,9 @@ function isHidden(path) {
return /node_modules|node.js/.test(dirname(path)) || /^\./.test(basename(path));
}
/** Files with these extensions are not treated as test cases */
const IGNORED_EXTENSIONS = new Set([".md"]);
/**
* @param {string} cwd
* @returns {string[]}
@@ -901,8 +911,9 @@ function getTests(cwd) {
const dirname = join(cwd, path);
for (const entry of readdirSync(dirname, { encoding: "utf-8", withFileTypes: true })) {
const { name } = entry;
const ext = name.slice(name.lastIndexOf("."));
const filename = join(path, name);
if (isHidden(filename)) {
if (isHidden(filename) || IGNORED_EXTENSIONS.has(ext)) {
continue;
}
if (entry.isFile() && isTest(filename)) {

View File

@@ -31,6 +31,13 @@ pub const Lifetime = enum {
allocated,
temporary,
};
/// Marshall a zig value into a JSValue using comptime reflection.
///
/// - Primitives are converted to their JS equivalent.
/// - Types with `toJS` or `toJSNewlyCreated` methods have them called
/// - Slices are converted to JS arrays
/// - Enums are converted to 32-bit numbers.
pub fn toJS(globalObject: *JSC.JSGlobalObject, comptime ValueType: type, value: ValueType, comptime lifetime: Lifetime) JSC.JSValue {
const Type = comptime brk: {
var CurrentType = ValueType;
@@ -75,7 +82,7 @@ pub fn toJS(globalObject: *JSC.JSGlobalObject, comptime ValueType: type, value:
var array = JSC.JSValue.createEmptyArray(globalObject, value.len);
for (value, 0..) |*item, i| {
const res = toJS(globalObject, *Child, item, lifetime);
const res = toJS(globalObject, *const Child, item, lifetime);
if (res == .zero) return .zero;
array.putIndex(
globalObject,
@@ -94,6 +101,13 @@ pub fn toJS(globalObject: *JSC.JSGlobalObject, comptime ValueType: type, value:
return value.toJS(globalObject);
}
// must come after toJS check in case this enum implements its own serializer.
if (@typeInfo(Type) == .Enum) {
// FIXME: creates non-normalized integers (e.g. u2), which
// aren't handled by `jsNumberWithType` rn
return JSC.JSValue.jsNumberWithType(u32, @as(u32, @intFromEnum(value)));
}
@compileError("dont know how to convert " ++ @typeName(ValueType) ++ " to JS");
},
}

View File

@@ -351,8 +351,8 @@ WTF::String ERR_INVALID_ARG_TYPE(JSC::ThrowScope& scope, JSC::JSGlobalObject* gl
} else {
for (unsigned i = 0; i < length - 1; i++) {
JSValue expected_type = expected_types.at(i);
if (i > 0) result.append(", "_s);
result.append(expected_type.toWTFString(globalObject));
result.append(", "_s);
}
result.append(" or "_s);
result.append(expected_types.at(length - 1).toWTFString(globalObject));

View File

@@ -18,9 +18,11 @@ export default [
["ERR_INVALID_ARG_VALUE", TypeError],
["ERR_INVALID_PROTOCOL", TypeError],
["ERR_INVALID_THIS", TypeError],
["ERR_INVALID_RETURN_VALUE", TypeError],
["ERR_IPC_CHANNEL_CLOSED", Error],
["ERR_IPC_DISCONNECTED", Error],
["ERR_MISSING_ARGS", TypeError],
["ERR_AMBIGUOUS_ARGUMENT", TypeError],
["ERR_OUT_OF_RANGE", RangeError],
["ERR_PARSE_ARGS_INVALID_OPTION_VALUE", TypeError],
["ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL", TypeError],
@@ -47,11 +49,12 @@ export default [
["ERR_UNKNOWN_SIGNAL", TypeError],
["ERR_SOCKET_BAD_PORT", RangeError],
["ERR_STREAM_RELEASE_LOCK", Error, "AbortError"],
["ERR_INCOMPATIBLE_OPTION_PAIR", TypeError, "TypeError"],
["ERR_INVALID_URI", URIError, "URIError"],
["ERR_INVALID_IP_ADDRESS", TypeError, "TypeError"],
["ERR_SCRIPT_EXECUTION_TIMEOUT", Error, "Error"],
["ERR_SCRIPT_EXECUTION_INTERRUPTED", Error, "Error"],
["ERR_INCOMPATIBLE_OPTION_PAIR", TypeError],
["ERR_INVALID_IP_ADDRESS", TypeError],
["ERR_UNAVAILABLE_DURING_EXIT", Error],
["ERR_INVALID_URI", URIError],
["ERR_SCRIPT_EXECUTION_TIMEOUT", Error],
["ERR_SCRIPT_EXECUTION_INTERRUPTED", Error],
["ERR_UNHANDLED_ERROR", Error],
["ERR_UNKNOWN_CREDENTIAL", Error],
["ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET", Error],

View File

@@ -2970,6 +2970,7 @@ pub const JSGlobalObject = opaque {
return this.ERR_INVALID_ARG_VALUE("The \"{s}\" argument is invalid. Received {}", .{ argname, value.toFmt(&formatter) }).throw();
}
/// "The <argname> argument must be of type <typename>. Received <value>"
pub fn throwInvalidArgumentTypeValue(
this: *JSGlobalObject,
argname: []const u8,
@@ -3009,6 +3010,7 @@ pub const JSGlobalObject = opaque {
return JSC.toTypeError(.ERR_MISSING_ARGS, "Not enough arguments to '" ++ name_ ++ "'. Expected {d}, got {d}.", .{ expected, got }, this);
}
/// Not enough arguments passed to function named `name_`
pub fn throwNotEnoughArguments(
this: *JSGlobalObject,
comptime name_: []const u8,

View File

@@ -0,0 +1,631 @@
//! ## IMPORTANT NOTE
//!
//! Do _NOT_ import from "root" in this file! Do _NOT_ use the Bun object in this file!
//!
//! This file has tests defined in it which _cannot_ be run if `@import("root")` is used!
//!
//! Run tests with `:zig test %`
const std = @import("std");
const builtin = @import("builtin");
const mem = std.mem;
const Allocator = mem.Allocator;
const stackFallback = std.heap.stackFallback;
const assert = std.debug.assert;
const print = std.debug.print;
/// Comptime diff configuration. Defaults are usually sufficient.
pub const Options = struct {
/// Guesstimate for the number of bytes `expected` and `actual` will be.
/// Defaults to 256.
///
/// Used to reserve space on the stack for the edit graph.
avg_input_size: comptime_int = 256,
/// How much stack space to reserve for edit trace frames. Defaults to 64.
initial_trace_capacity: comptime_int = 64,
/// When `true`, string lines that are only different by a trailing comma
/// are considered equal. Not used when comparing chars. Defaults to
/// `false`.
check_comma_disparity: bool = false,
};
// By limiting maximum string and buffer lengths, we can store u32s in the
// edit graph instead of usize's, halving our memory footprint. The
// downside is that `(2 * (actual.len + expected.len))` must be less than
// 4Gb. If this becomes a problem in real user scenarios, we can adjust this.
//
// Note that overflows are much more likely to occur in real user scenarios
// than in our own testing, so overflow checks _must_ be handled. Do _not_
// use `assert` unless you also use `@setRuntimeSafety(true)`.
//
// TODO: make this configurable in `Options`?
const MAXLEN = std.math.maxInt(u32);
// Type aliasing to make future refactors easier
const uint = u32;
const int = i64; // must be large enough to hold all valid values of `uint` w/o overflow.
/// diffs two sets of lines, returning the minimal number of edits needed to
/// make them equal.
///
/// Lines may be string slices or chars. Derived from node's implementation of
/// the Myers' diff algorithm.
///
/// ## Example
/// ```zig
/// const myers_diff = @import("inode/assert/myers_diff.zig");
/// const StrDiffer = myers_diff.Differ([]const u8, .{});
/// const actual = &[_][]const u8{
/// "foo",
/// "bar",
/// "baz",
/// };
/// const expected = &[_][]const u8{
/// "foo",
/// "barrr",
/// "baz",
/// };
/// const diff = try StrDiffer.diff(allocator, actual, expected);
/// ```
///
/// TODO: support non-ASCII UTF-8 characters.
///
/// ## References
/// - [Node- `myers_diff.js`](https://github.com/nodejs/node/blob/main/lib/internal/assert/myers_diff.js)
/// - [An O(ND) Difference Algorithm and Its Variations](http://www.xmailserver.org/diff2.pdf)
pub fn Differ(comptime Line: type, comptime opts: Options) type {
const eql: LineCmp(Line) = switch (Line) {
// char-by-char comparison. u16 is for utf16
u8, u16 => blk: {
const gen = struct {
pub fn eql(a: Line, b: Line) bool {
return a == b;
}
};
break :blk gen.eql;
},
[]const u8,
[]u8,
[:0]const u8,
[:0]u8,
[]const u16,
[]u16,
[:0]const u16,
[:0]u16,
=> blk: {
const gen = struct {
pub fn eql(a: Line, b: Line) bool {
return areStrLinesEqual(Line, a, b, opts.check_comma_disparity);
}
};
break :blk gen.eql;
},
else => @compileError("Differ can only compare lines of chars or strings. Received: " ++ @typeName(Line)),
};
return DifferWithEql(Line, opts, eql);
}
/// Like `Differ`, but allows the user to provide a custom equality function.
pub fn DifferWithEql(comptime Line: type, comptime opts: Options, comptime areLinesEql: LineCmp(Line)) type {
// `V = [-MAX, MAX]`.
const graph_initial_size = comptime guess: {
const size_wanted = 2 * opts.avg_input_size + 1;
break :guess size_wanted + (size_wanted % 8); // 8-byte align
};
if (graph_initial_size > MAXLEN) @compileError("Input guess size is too large. The edit graph must be 32-bit addressable.");
return struct {
pub const eql = areLinesEql;
pub const LineType = Line;
/// Compute the shortest edit path (diff) between two sets of lines.
///
/// Returned `Diff` objects borrow from the input slices. Both `actual`
/// and `expected` must outlive them.
///
/// ## References
/// - [Node- `myers_diff.js`](https://github.com/nodejs/node/blob/main/lib/internal/assert/myers_diff.js)
/// - [An O(ND) Difference Algorithm and Its Variations](http://www.xmailserver.org/diff2.pdf)
pub fn diff(bun_allocator: Allocator, actual: []const Line, expected: []const Line) Error!DiffList(Line) {
// Edit graph's allocator
var graph_stack_alloc = stackFallback(graph_initial_size, bun_allocator);
const graph_alloc = graph_stack_alloc.get();
// Match point trace's allocator
var trace_stack_alloc = stackFallback(opts.initial_trace_capacity, bun_allocator);
const trace_alloc = trace_stack_alloc.get();
// const MAX \in [0, M+N]
// let V: int array = [-MAX..MAX]. V is a flattened representation of the edit graph.
const max: uint, const graph_size: uint = blk: {
// This is to preserve overflow protections even when runtime safety
// checks are disabled. We don't know what kind of stuff users are
// diffing in the wild.
const _max: usize = actual.len + expected.len;
const _graph_size = (2 * _max) + 1;
if (_max > MAXLEN) return Error.InputsTooLarge;
if (_graph_size > MAXLEN) return Error.DiffTooLarge;
// const m:
break :blk .{ @intCast(_max), @intCast(_graph_size) };
};
var graph = try graph_alloc.alloc(uint, graph_size);
defer graph_alloc.free(graph);
@memset(graph, 0);
graph.len = graph_size;
var trace = std.ArrayList([]const uint).init(trace_alloc);
// reserve enough space for each frame to avoid realloc on ptr list. Lists may end up in the heap, but
// this list is at the very from (and ∴ on stack).
try trace.ensureTotalCapacityPrecise(max + 1);
defer {
for (trace.items) |frame| {
trace_alloc.free(frame);
}
trace.deinit();
}
// ================================================================
// ==================== actual implementation =====================
// ================================================================
for (0..max + 1) |_diff_level| {
const diff_level: int = @intCast(_diff_level); // why is this always usize?
// const new_trace = try TraceFrame.initCapacity(trace_alloc, graph.len);
const new_trace = try trace_alloc.dupe(uint, graph);
trace.appendAssumeCapacity(new_trace);
const diag_start: int = -@as(int, @intCast(diff_level));
const diag_end: int = @intCast(diff_level);
// for k ← -D in steps of 2 do
var diag_idx = diag_start;
while (diag_idx <= diag_end) : (diag_idx += 2) {
// if k = -D or K ≠ D and V[k-1] < V[k+1] then
// x ← V[k+1]
// else
// x ← V[k-1] + 1
assert(diag_idx + max >= 0); // sanity check. Fine to be stripped in release.
const k: uint = u(diag_idx + max);
const uk = u(k);
var x = if (diag_idx == diag_start or
(diag_idx != diag_end and graph[uk - 1] < graph[uk + 1]))
graph[uk + 1]
else
graph[uk - 1] + 1;
// y = x - diag_idx
var y: usize = blk: {
const x2: int = @intCast(x);
const y: int = x2 - diag_idx;
assert(y >= 0 and y <= MAXLEN); // sanity check. Fine to be stripped in release.
break :blk @intCast(y);
};
while (x < actual.len and y < expected.len and eql(actual[x], expected[y])) {
x += 1;
y += 1;
}
graph[k] = @intCast(x);
if (x >= actual.len and y >= expected.len) {
// todo: arena
return backtrack(bun_allocator, &trace, actual, expected);
}
}
}
@panic("unreachable. Diffing should always reach the end of either `actual` or `expected` first.");
}
fn backtrack(
allocator: Allocator,
trace: *const std.ArrayList([]const uint),
actual: []const Line,
expected: []const Line,
) Error!DiffList(Line) {
const max = i(actual.len + expected.len);
var x = i(actual.len);
var y = i(expected.len);
var result = DiffList(Line).init(allocator);
if (trace.items.len == 0) return result;
//for (let diffLevel = trace.length - 1; diffLevel >= 0; diffLevel--) {
var diff_level: usize = trace.items.len;
while (diff_level > 0) {
diff_level -= 1;
const graph = trace.items[diff_level];
const diagonal_index = x - y;
const diag_offset = u(diagonal_index + max);
const prev_diagonal_index: int = if (diagonal_index == -i(diff_level) or
(diagonal_index != diff_level and graph[u(diag_offset - 1)] < graph[u(diag_offset + 1)]))
diagonal_index + 1
else
diagonal_index - 1;
const prev_x: int = i(graph[u(prev_diagonal_index + i(max))]); // v[prevDiagonalIndex + max]
const prev_y: int = i(prev_x) - prev_diagonal_index;
try result.ensureUnusedCapacity(u(@max(x - prev_x, y - prev_y)));
while (x > prev_x and y > prev_y) {
const line: Line = blk: {
if (@typeInfo(Line) == .Pointer and comptime opts.check_comma_disparity) {
const actual_el = actual[u(x) - 1];
// actual[x-1].endsWith(',')
break :blk if (actual_el[actual_el.len - 1] == ',')
actual[u(x) - 1]
else
expected[u(y) - 1];
} else {
break :blk actual[u(x) - 1];
}
};
result.appendAssumeCapacity(.{ .kind = .equal, .value = line });
x -= 1;
y -= 1;
}
if (diff_level > 0) {
if (x > prev_x) {
try result.append(.{ .kind = .insert, .value = actual[u(x) - 1] });
x -= 1;
} else {
try result.append(.{ .kind = .delete, .value = expected[u(y) - 1] });
y -= 1;
}
}
}
return result;
}
// shorthands for int casting since I'm tired of writing `@as(int, @intCast(x))` everywhere
inline fn u(n: anytype) uint {
return @intCast(n);
}
inline fn us(n: anytype) usize {
return @intCast(n);
}
inline fn i(n: anytype) int {
return @intCast(n);
}
};
}
pub fn printDiff(T: type, diffs: std.ArrayList(Diff(T))) !void {
const stdout = if (builtin.is_test)
std.io.getStdErr().writer()
else
std.io.getStdOut().writer();
const specifier = switch (T) {
u8 => "c",
u32 => "u",
[]const u8 => "s",
else => @compileError("printDiff can only print chars and strings. Received: " ++ @typeName(T)),
};
for (0..diffs.items.len) |idx| {
const d = diffs.items[diffs.items.len - (idx + 1)];
const op: u8 = switch (d.kind) {
inline .equal => ' ',
inline .insert => '+',
inline .delete => '-',
};
try stdout.writeByte(op);
try stdout.print(" {" ++ specifier ++ "}\n", .{d.value});
}
}
// =============================================================================
// ============================ EQUALITY FUNCTIONS ============================
// =============================================================================
fn areCharsEqual(comptime T: type, a: T, b: T) bool {
return a == b;
}
fn areLinesEqual(comptime T: type, a: T, b: T, comptime check_comma_disparity: bool) bool {
return switch (T) {
u8, u32 => a == b,
[]const u8, []u8, [:0]const u8, [:0]u8 => areStrLinesEqual(T, a, b, check_comma_disparity),
else => @compileError("areLinesEqual can only compare chars and strings. Received: " ++ @typeName(T)),
};
}
fn areStrLinesEqual(comptime T: type, a: T, b: T, comptime check_comma_disparity: bool) bool {
// Hypothesis: unlikely to be the same, since assert.equal, etc. is rarely
// used to compare the same object. May be true on shallow copies.
// TODO: check Godbolt
// if (a.ptr == b.ptr) return true;
// []const u8 -> u8
const info = @typeInfo(T);
const ChildType = info.Pointer.child;
if (comptime !check_comma_disparity) {
return mem.eql(ChildType, a, b);
}
const largest, const smallest = if (a.len > b.len) .{ a, b } else .{ b, a };
return switch (largest.len - smallest.len) {
inline 0 => mem.eql(ChildType, a, b),
inline 1 => largest[largest.len - 1] == ',' and mem.eql(ChildType, largest[0..smallest.len], smallest), // 'foo,' == 'foo'
else => false,
};
}
// =============================================================================
// =================================== TYPES ===================================
// =============================================================================
/// Generic equality function. Returns `true` if two lines are equal.
pub fn LineCmp(Line: type) type {
return fn (a: Line, b: Line) bool;
}
pub const Error = error{
DiffTooLarge,
InputsTooLarge,
} || Allocator.Error;
const TraceFrame = std.ArrayListUnmanaged(u8);
pub const DiffKind = enum {
insert,
delete,
equal,
pub fn format(value: DiffKind, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
return switch (value) {
.insert => writer.writeByte('+'),
.delete => writer.writeByte('-'),
.equal => writer.writeByte(' '),
};
}
};
pub fn Diff(comptime T: type) type {
return struct {
kind: DiffKind,
value: T,
const Self = @This();
pub fn eql(self: Self, other: Self) bool {
return self.kind == other.kind and mem.eql(T, self.value, other.value);
}
/// pub fn format(value: ?, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void
pub fn format(value: anytype, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
const specifier = switch (T) {
u8 => "c",
u32 => "u",
[]const u8, [:0]const u8, []u8, [:0]u8 => "s",
else => @compileError("printDiff can only print chars and strings. Received: " ++ @typeName(T)),
};
return writer.print("{} {" ++ specifier ++ "}", .{ value.kind, value.value });
}
};
}
pub fn DiffList(comptime T: type) type {
return std.ArrayList(Diff(T));
}
// =============================================================================
const t = std.testing;
test areLinesEqual {
// check_comma_disparity is never respected when comparing chars
try t.expect(areLinesEqual(u8, 'a', 'a', false));
try t.expect(areLinesEqual(u8, 'a', 'a', true));
try t.expect(!areLinesEqual(u8, ',', 'a', false));
try t.expect(!areLinesEqual(u8, ',', 'a', true));
// strings w/o comma check
try t.expect(areLinesEqual([]const u8, "", "", false));
try t.expect(areLinesEqual([]const u8, "a", "a", false));
try t.expect(areLinesEqual([]const u8, "Bun", "Bun", false));
try t.expect(areLinesEqual([]const u8, "😤", "😤", false));
// not equal
try t.expect(!areLinesEqual([]const u8, "", "a", false));
try t.expect(!areLinesEqual([]const u8, "", " ", false));
try t.expect(!areLinesEqual([]const u8, "\n", "\t", false));
try t.expect(!areLinesEqual([]const u8, "bun", "Bun", false));
try t.expect(!areLinesEqual([]const u8, "😤", "😩", false));
// strings w/ comma check
try t.expect(areLinesEqual([]const u8, "", "", true));
try t.expect(areLinesEqual([]const u8, "", ",", true));
try t.expect(areLinesEqual([]const u8, " ", " ,", true));
try t.expect(areLinesEqual([]const u8, "I am speed", "I am speed", true));
try t.expect(areLinesEqual([]const u8, "I am speed,", "I am speed", true));
try t.expect(areLinesEqual([]const u8, "I am speed", "I am speed,", true));
try t.expect(areLinesEqual([]const u8, "😤", "😤", false));
// try t.expect(areLinesEqual([]const u8, "😤", "😤,", false));
// try t.expect(areLinesEqual([]const u8, "😤,", "😤", false));
// not equal
try t.expect(!areLinesEqual([]const u8, "", "Bun", true));
try t.expect(!areLinesEqual([]const u8, "bun", "Bun", true));
try t.expect(!areLinesEqual([]const u8, ",Bun", "Bun", true));
try t.expect(!areLinesEqual([]const u8, "Bun", ",Bun", true));
try t.expect(!areLinesEqual([]const u8, "", " ,", true));
try t.expect(!areLinesEqual([]const u8, " ", " , ", true));
try t.expect(!areLinesEqual([]const u8, "I, am speed", "I am speed", true));
try t.expect(!areLinesEqual([]const u8, ",😤", "😤", true));
}
// const CharList = DiffList(u8);
// const CDiff = Diff(u8);
// const CharDiffer = Differ(u8, .{});
// fn testCharDiff(actual: []const u8, expected: []const u8, expected_diff: []const Diff(u8)) !void {
// const allocator = t.allocator;
// const actual_diff = try CharDiffer.diff(allocator, actual, expected);
// defer actual_diff.deinit();
// try t.expectEqualSlices(Diff(u8), expected_diff, actual_diff.items);
// }
// test CharDiffer {
// const TestCase = std.meta.Tuple(&[_]type{ []const CDiff, []const u8, []const u8 });
// const test_cases = &[_]TestCase{
// .{ &[_]CDiff{}, "foo", "foo" },
// };
// for (test_cases) |test_case| {
// const expected_diff, const actual, const expected = test_case;
// try testCharDiff(actual, expected, expected_diff);
// }
// }
const StrDiffer = Differ([]const u8, .{ .check_comma_disparity = true });
test StrDiffer {
const a = t.allocator;
inline for (.{
// .{ "foo", "foo" },
// .{ "foo", "bar" },
.{
// actual
\\[
\\ 1,
\\ 2,
\\ 3,
\\ 4,
\\ 5,
\\ 6,
\\ 7
\\]
,
// expected
\\[
\\ 1,
\\ 2,
\\ 3,
\\ 4,
\\ 5,
\\ 9,
\\ 7
\\]
},
// // remove line
// .{
// \\Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
// \\incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
// \\nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
// \\Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
// \\fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
// \\culpa qui officia deserunt mollit anim id est laborum.
// ,
// \\Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
// \\incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
// \\Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
// \\fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
// \\culpa qui officia deserunt mollit anim id est laborum.
// ,
// },
// // add some line
// .{
// \\Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
// \\incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
// \\nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
// \\Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
// \\fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
// \\culpa qui officia deserunt mollit anim id est laborum.
// ,
// \\Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
// \\incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
// \\Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
// \\nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
// \\Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
// \\fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
// \\culpa qui officia deserunt mollit anim id est laborum.
// \\Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
// ,
// },
// // modify lines
// .{
// \\foo
// \\bar
// \\baz
// ,
// \\foo
// \\barrr
// \\baz
// },
// .{
// \\foooo
// \\bar
// \\baz
// ,
// \\foo
// \\bar
// \\baz
// },
// .{
// \\foo
// \\bar
// \\baz
// ,
// \\foo
// \\bar
// \\baz
// },
// .{
// \\Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
// \\incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
// \\nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
// \\Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
// \\fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
// \\culpa qui officia deserunt mollit anim id est laborum.
// ,
// \\Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor modified
// \\incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
// \\nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
// \\Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
// \\fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in also modified
// \\culpa qui officia deserunt mollit anim id est laborum.
// ,
// },
}) |thing| {
var actual = try split(u8, a, thing[0]);
var expected = try split(u8, a, thing[1]);
defer {
actual.deinit(a);
expected.deinit(a);
}
var d = try StrDiffer.diff(a, actual.items, expected.items);
defer d.deinit();
for (d.items) |diff| {
std.debug.print("{}\n", .{diff});
}
}
}
pub fn split(
comptime T: type,
alloc: Allocator,
s: []const T,
) Allocator.Error!std.ArrayListUnmanaged([]const T) {
comptime {
if (T != u8 and T != u16) {
@compileError("Split only supports latin1, utf8, and utf16. Received: " ++ @typeName(T));
}
}
const newline: T = if (comptime T == u8) '\n' else '\n';
//
// thing
var it = std.mem.splitScalar(T, s, newline);
var lines = std.ArrayListUnmanaged([]const T){};
try lines.ensureUnusedCapacity(alloc, s.len >> 4);
errdefer lines.deinit(alloc);
while (it.next()) |l| {
try lines.append(alloc, l);
}
return lines;
}

View File

@@ -0,0 +1,132 @@
const std = @import("std");
const bun = @import("root").bun;
const MyersDiff = @import("./assert/myers_diff.zig");
const Allocator = std.mem.Allocator;
const BunString = bun.String;
const JSC = bun.JSC;
const JSValue = JSC.JSValue;
const StringDiffList = MyersDiff.DiffList([]const u8);
const print = std.debug.print;
/// Compare `actual` and `expected`, producing a diff that would turn `actual`
/// into `expected`.
///
/// Lines in the returned diff have the same encoding as `actual` and
/// `expected`. Lines borrow from these inputs, but the diff list itself must
/// be deallocated.
///
/// Use an arena allocator, otherwise this will leak memory.
///
/// ## Invariants
/// If not met, this function will panic.
/// - `actual` and `expected` are alive and have the same encoding.
pub fn myersDiff(
allocator: Allocator,
global: *JSC.JSGlobalObject,
actual: *const BunString,
expected: *const BunString,
// If true, strings that have a trailing comma but are otherwise equal are
// considered equal.
check_comma_disparity: bool,
// split `actual` and `expected` into lines before diffing
lines: bool,
) bun.JSError!JSC.JSValue {
// Short circuit on empty strings. Note that, in release builds where
// assertions are disabled, if `actual` and `expected` are both dead, this
// branch will be hit since dead strings have a length of 0. This should be
// moot since BunStrings with non-zero reference counds should never be
// dead.
if (actual.length() == 0 and expected.length() == 0) {
return JSC.JSValue.createEmptyArray(global, 0);
}
const actual_encoding = actual.encoding();
const expected_encoding = expected.encoding();
if (lines) {
if (actual_encoding != expected_encoding) {
const actual_utf8 = actual.toUTF8WithoutRef(allocator);
defer actual_utf8.deinit();
const expected_utf8 = expected.toUTF8WithoutRef(allocator);
defer expected_utf8.deinit();
return diffLines(u8, allocator, global, actual_utf8.byteSlice(), expected_utf8.byteSlice(), check_comma_disparity);
}
return switch (actual_encoding) {
.latin1, .utf8 => diffLines(u8, allocator, global, actual.byteSlice(), expected.byteSlice(), check_comma_disparity),
.utf16 => diffLines(u16, allocator, global, actual.utf16(), expected.utf16(), check_comma_disparity),
};
}
if (actual_encoding != expected_encoding) {
const actual_utf8 = actual.toUTF8WithoutRef(allocator);
defer actual_utf8.deinit();
const expected_utf8 = expected.toUTF8WithoutRef(allocator);
defer expected_utf8.deinit();
return diffChars(u8, allocator, global, actual.byteSlice(), expected.byteSlice());
}
return switch (actual_encoding) {
.latin1, .utf8 => diffChars(u8, allocator, global, actual.byteSlice(), expected.byteSlice()),
.utf16 => diffChars(u16, allocator, global, actual.utf16(), expected.utf16()),
};
}
fn diffChars(
comptime T: type,
allocator: Allocator,
global: *JSC.JSGlobalObject,
actual: []const T,
expected: []const T,
) bun.JSError!JSC.JSValue {
const Differ = MyersDiff.Differ(T, .{ .check_comma_disparity = false });
const diff: MyersDiff.DiffList(T) = Differ.diff(allocator, actual, expected) catch |err| return mapDiffError(global, err);
return diffListToJS(T, global, diff);
}
fn diffLines(
comptime T: type,
allocator: Allocator,
global: *JSC.JSGlobalObject,
actual: []const T,
expected: []const T,
check_comma_disparity: bool,
) bun.JSError!JSC.JSValue {
var a = try MyersDiff.split(T, allocator, actual);
defer a.deinit(allocator);
var e = try MyersDiff.split(T, allocator, expected);
defer e.deinit(allocator);
const diff: MyersDiff.DiffList([]const T) = blk: {
if (check_comma_disparity) {
const Differ = MyersDiff.Differ([]const T, .{ .check_comma_disparity = true });
break :blk Differ.diff(allocator, a.items, e.items) catch |err| return mapDiffError(global, err);
} else {
const Differ = MyersDiff.Differ([]const T, .{ .check_comma_disparity = false });
break :blk Differ.diff(allocator, a.items, e.items) catch |err| return mapDiffError(global, err);
}
};
return diffListToJS([]const T, global, diff);
}
fn diffListToJS(comptime T: type, global: *JSC.JSGlobalObject, diff_list: MyersDiff.DiffList(T)) bun.JSError!JSC.JSValue {
var array = JSC.JSValue.createEmptyArray(global, diff_list.items.len);
for (diff_list.items, 0..) |*line, i| {
array.putIndex(global, @truncate(i), JSC.JSObject.createNullProto(line.*, global).toJS());
}
return array;
}
fn mapDiffError(global: *JSC.JSGlobalObject, err: MyersDiff.Error) bun.JSError {
return switch (err) {
error.OutOfMemory => error.OutOfMemory,
error.DiffTooLarge => global.throwInvalidArguments("Diffing these two values would create a string that is too large. If this was intentional, please open a bug report on GitHub.", .{}),
error.InputsTooLarge => global.throwInvalidArguments("Input strings are too large to diff. Please open a bug report on GitHub.", .{}),
};
}

View File

@@ -0,0 +1,86 @@
const std = @import("std");
const bun = @import("root").bun;
const assert = @import("./node_assert.zig");
const DiffList = @import("./assert/myers_diff.zig").DiffList;
const Allocator = std.mem.Allocator;
const JSC = bun.JSC;
const JSValue = JSC.JSValue;
/// ```ts
/// const enum DiffType {
/// Insert = 0,
/// Delete = 1,
/// Equal = 2,
/// }
/// type Diff = { operation: DiffType, text: string };
/// declare function myersDiff(actual: string, expected: string): Diff[];
/// ```
pub fn myersDiff(global: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue {
var stack_fallback = std.heap.stackFallback(1024 * 2, bun.default_allocator);
var arena = std.heap.ArenaAllocator.init(stack_fallback.get());
defer arena.deinit();
const allocator = arena.allocator();
const nargs = callframe.argumentsCount();
if (nargs < 2) {
return global.throwNotEnoughArguments("printMyersDiff", 2, callframe.argumentsCount());
}
const actual_arg: JSValue = callframe.argument(0);
const expected_arg: JSValue = callframe.argument(1);
const check_comma_disparity: bool, const lines: bool = switch (nargs) {
0, 1 => unreachable,
2 => .{ false, false },
3 => .{ callframe.argument(2).isTruthy(), false },
else => .{ callframe.argument(2).isTruthy(), callframe.argument(3).isTruthy() },
};
if (!actual_arg.isString()) return global.throwInvalidArgumentTypeValue("actual", "string", actual_arg);
if (!expected_arg.isString()) return global.throwInvalidArgumentTypeValue("expected", "string", expected_arg);
const actual_str = try actual_arg.toBunString2(global);
defer actual_str.deref();
const expected_str = try expected_arg.toBunString2(global);
defer expected_str.deref();
bun.assertWithLocation(actual_str.tag != .Dead, @src());
bun.assertWithLocation(expected_str.tag != .Dead, @src());
return assert.myersDiff(
allocator,
global,
&actual_str,
&expected_str,
check_comma_disparity,
lines,
);
}
const StrDiffList = DiffList([]const u8);
fn diffListToJS(global: *JSC.JSGlobalObject, diff_list: StrDiffList) bun.JSError!JSC.JSValue {
// todo: replace with toJS
var array = JSC.JSValue.createEmptyArray(global, diff_list.items.len);
for (diff_list.items, 0..) |*line, i| {
var obj = JSC.JSValue.createEmptyObjectWithNullPrototype(global);
if (obj == .zero) return global.throwOutOfMemory();
obj.put(global, bun.String.static("kind"), JSC.JSValue.jsNumber(@as(u32, @intFromEnum(line.kind))));
obj.put(global, bun.String.static("value"), JSC.toJS(global, []const u8, line.value, .allocated));
array.putIndex(global, @truncate(i), obj);
}
return array;
}
// =============================================================================
pub fn generate(global: *JSC.JSGlobalObject) JSC.JSValue {
const exports = JSC.JSValue.createEmptyObject(global, 1);
exports.put(
global,
bun.String.static("myersDiff"),
JSC.JSFunction.create(global, "myersDiff", myersDiff, 2, .{}),
);
return exports;
}

View File

@@ -448,6 +448,7 @@ writeIfNotChanged(
(() => {
let dts = `
// GENERATED TEMP FILE - DO NOT EDIT
// generated by ${import.meta.path}
`;
for (let i = 0; i < ErrorCode.length; i++) {

10
src/js/builtins.d.ts vendored
View File

@@ -10,8 +10,14 @@ type TODO = any;
* This only works in debug builds, the log fn is completely removed in release builds.
*/
declare function $debug(...args: any[]): void;
/** $assert is a preprocessor macro that only runs in debug mode. it throws an error if the first argument is falsy.
* The source code passed to `check` is inlined in the message, but in addition you can pass additional messages.
/**
* Assert that a condition holds in debug builds.
*
* $assert is a preprocessor macro that only runs in debug mode. it throws an
* error if the first argument is falsy. The source code passed to `check` is
* inlined in the message, but in addition you can pass additional messages.
*
* @note gets removed in release builds. Do not put code with side effects in the `check`.
*/
declare function $assert(check: any, ...message: any[]): asserts check;

View File

@@ -0,0 +1,425 @@
"use strict";
const {
ArrayPrototypeJoin,
ArrayPrototypePop,
ArrayPrototypeSlice,
Error,
ErrorCaptureStackTrace,
ObjectAssign,
ObjectDefineProperty,
ObjectGetPrototypeOf,
ObjectPrototypeHasOwnProperty,
String,
StringPrototypeRepeat,
StringPrototypeSlice,
StringPrototypeSplit,
} = require("internal/primordials");
const { inspect } = require("internal/util/inspect");
const colors = require("internal/util/colors");
const { validateObject } = require("internal/validators");
declare namespace Internal {
const enum Operation {
Insert = 0,
Delete = 1,
Equal = 2,
}
interface Diff {
kind: Operation;
value: string;
}
function myersDiff(actual: string, expected: string, checkCommaDisparity?: boolean, lines?: boolean): string;
// todo
function printMyersDiff(...args: any[]): any;
function printSimpleMyersDiff(...args: any[]): any;
}
const { myersDiff, printMyersDiff, printSimpleMyersDiff } = require("internal/assert/myers_diff") as typeof Internal;
const kReadableOperator = {
deepStrictEqual: "Expected values to be strictly deep-equal:",
strictEqual: "Expected values to be strictly equal:",
strictEqualObject: 'Expected "actual" to be reference-equal to "expected":',
deepEqual: "Expected values to be loosely deep-equal:",
notDeepStrictEqual: 'Expected "actual" not to be strictly deep-equal to:',
notStrictEqual: 'Expected "actual" to be strictly unequal to:',
notStrictEqualObject: 'Expected "actual" not to be reference-equal to "expected":',
notDeepEqual: 'Expected "actual" not to be loosely deep-equal to:',
notIdentical: "Values have same structure but are not reference-equal:",
notDeepEqualUnequal: "Expected values not to be loosely deep-equal:",
};
const kMaxShortStringLength = 12;
const kMaxLongStringLength = 512;
function copyError(source) {
const target = ObjectAssign({ __proto__: ObjectGetPrototypeOf(source) }, source);
ObjectDefineProperty(target, "message", {
__proto__: null,
value: source.message,
});
if (ObjectPrototypeHasOwnProperty(source, "cause")) {
let { cause } = source;
if (Error.isError(cause)) {
cause = copyError(cause);
}
ObjectDefineProperty(target, "cause", { __proto__: null, value: cause });
}
return target;
}
function inspectValue(val) {
// The util.inspect default values could be changed. This makes sure the
// error messages contain the necessary information nevertheless.
return inspect(val, {
compact: false,
customInspect: false,
depth: 1000,
maxArrayLength: Infinity,
// Assert compares only enumerable properties (with a few exceptions).
showHidden: false,
// Assert does not detect proxies currently.
showProxy: false,
sorted: true,
// Inspect getters as we also check them when comparing entries.
getters: true,
});
}
function getErrorMessage(operator, message) {
return message || kReadableOperator[operator];
}
function checkOperator(actual, expected, operator) {
// In case both values are objects or functions explicitly mark them as not
// reference equal for the `strictEqual` operator.
if (
operator === "strictEqual" &&
((typeof actual === "object" && actual !== null && typeof expected === "object" && expected !== null) ||
(typeof actual === "function" && typeof expected === "function"))
) {
operator = "strictEqualObject";
}
return operator;
}
function getColoredMyersDiff(actual, expected) {
const header = `${colors.green}actual${colors.white} ${colors.red}expected${colors.white}`;
const skipped = false;
// const diff = myersDiff(StringPrototypeSplit(actual, ""), StringPrototypeSplit(expected, ""));
const diff = myersDiff(actual, expected, false, false);
let message = printSimpleMyersDiff(diff);
if (skipped) {
message += "...";
}
return { message, header, skipped };
}
function getStackedDiff(actual, expected) {
const isStringComparison = typeof actual === "string" && typeof expected === "string";
let message = `\n${colors.green}+${colors.white} ${actual}\n${colors.red}- ${colors.white}${expected}`;
const stringsLen = actual.length + expected.length;
const maxTerminalLength = process.stderr.isTTY ? process.stderr.columns : 80;
const showIndicator = isStringComparison && stringsLen <= maxTerminalLength;
if (showIndicator) {
let indicatorIdx = -1;
for (let i = 0; i < actual.length; i++) {
if (actual[i] !== expected[i]) {
// Skip the indicator for the first 2 characters because the diff is immediately apparent
// It is 3 instead of 2 to account for the quotes
if (i >= 3) {
indicatorIdx = i;
}
break;
}
}
if (indicatorIdx !== -1) {
message += `\n${StringPrototypeRepeat(" ", indicatorIdx + 2)}^`;
}
}
return { message };
}
function getSimpleDiff(originalActual, actual: string, originalExpected, expected: string) {
let stringsLen = actual.length + expected.length;
// Accounting for the quotes wrapping strings
if (typeof originalActual === "string") {
stringsLen -= 2;
}
if (typeof originalExpected === "string") {
stringsLen -= 2;
}
if (stringsLen <= kMaxShortStringLength && (originalActual !== 0 || originalExpected !== 0)) {
return { message: `${actual} !== ${expected}`, header: "" };
}
const isStringComparison = typeof originalActual === "string" && typeof originalExpected === "string";
// colored myers diff
if (isStringComparison && colors.hasColors) {
return getColoredMyersDiff(actual, expected);
}
return getStackedDiff(actual, expected);
}
function isSimpleDiff(actual, inspectedActual, expected, inspectedExpected) {
if (inspectedActual.length > 1 || inspectedExpected.length > 1) {
return false;
}
return typeof actual !== "object" || actual === null || typeof expected !== "object" || expected === null;
}
function createErrDiff(actual, expected, operator, customMessage) {
operator = checkOperator(actual, expected, operator);
let skipped = false;
let message = "";
const inspectedActual = inspectValue(actual);
const inspectedExpected = inspectValue(expected);
const inspectedSplitActual = StringPrototypeSplit(inspectedActual, "\n");
const inspectedSplitExpected = StringPrototypeSplit(inspectedExpected, "\n");
const showSimpleDiff = isSimpleDiff(actual, inspectedSplitActual, expected, inspectedSplitExpected);
let header = `${colors.green}+ actual${colors.white} ${colors.red}- expected${colors.white}`;
if (showSimpleDiff) {
const simpleDiff = getSimpleDiff(actual, inspectedSplitActual[0], expected, inspectedSplitExpected[0]);
message = simpleDiff.message;
if (typeof simpleDiff.header !== "undefined") {
header = simpleDiff.header;
}
if (simpleDiff.skipped) {
skipped = true;
}
} else if (inspectedActual === inspectedExpected) {
// Handles the case where the objects are structurally the same but different references
operator = "notIdentical";
if (inspectedSplitActual.length > 50) {
message = `${ArrayPrototypeJoin(ArrayPrototypeSlice(inspectedSplitActual, 0, 50), "\n")}\n...}`;
skipped = true;
} else {
message = ArrayPrototypeJoin(inspectedSplitActual, "\n");
}
header = "";
} else {
const checkCommaDisparity = actual != null && typeof actual === "object";
const diff = myersDiff(inspectedActual, inspectedExpected, checkCommaDisparity, true);
const myersDiffMessage = printMyersDiff(diff);
message = myersDiffMessage.message;
if (myersDiffMessage.skipped) {
skipped = true;
}
}
const headerMessage = `${getErrorMessage(operator, customMessage)}\n${header}`;
const skippedMessage = skipped ? "\n... Skipped lines" : "";
return `${headerMessage}${skippedMessage}\n${message}\n`;
}
function addEllipsis(string) {
const lines = StringPrototypeSplit(string, "\n", 11);
if (lines.length > 10) {
lines.length = 10;
return `${ArrayPrototypeJoin(lines, "\n")}\n...`;
} else if (string.length > kMaxLongStringLength) {
return `${StringPrototypeSlice(string, kMaxLongStringLength)}...`;
}
return string;
}
class AssertionError extends Error {
constructor(options) {
validateObject(options, "options");
const {
message,
operator,
stackStartFn,
details,
// Compatibility with older versions.
stackStartFunction,
} = options;
let { actual, expected } = options;
// NOTE: stack trace is always writable.
const limit = Error.stackTraceLimit;
Error.stackTraceLimit = 0;
if (message != null) {
if (operator === "deepStrictEqual" || operator === "strictEqual") {
super(createErrDiff(actual, expected, operator, message));
} else {
super(String(message));
}
} else {
// Reset colors on each call to make sure we handle dynamically set environment
// variables correct.
colors.refresh();
// Prevent the error stack from being visible by duplicating the error
// in a very close way to the original in case both sides are actually
// instances of Error.
if (
typeof actual === "object" &&
actual !== null &&
typeof expected === "object" &&
expected !== null &&
"stack" in actual &&
actual instanceof Error &&
"stack" in expected &&
expected instanceof Error
) {
actual = copyError(actual);
expected = copyError(expected);
}
if (operator === "deepStrictEqual" || operator === "strictEqual") {
super(createErrDiff(actual, expected, operator, message));
} else if (operator === "notDeepStrictEqual" || operator === "notStrictEqual") {
// In case the objects are equal but the operator requires unequal, show
// the first object and say A equals B
let base = kReadableOperator[operator];
const res = StringPrototypeSplit(inspectValue(actual), "\n");
// In case "actual" is an object or a function, it should not be
// reference equal.
if (
operator === "notStrictEqual" &&
((typeof actual === "object" && actual !== null) || typeof actual === "function")
) {
base = kReadableOperator.notStrictEqualObject;
}
// Only remove lines in case it makes sense to collapse those.
// TODO: Accept env to always show the full error.
if (res.length > 50) {
res[46] = `${colors.blue}...${colors.white}`;
while (res.length > 47) {
ArrayPrototypePop(res);
}
}
// Only print a single input.
if (res.length === 1) {
super(`${base}${res[0].length > 5 ? "\n\n" : " "}${res[0]}`);
} else {
super(`${base}\n\n${ArrayPrototypeJoin(res, "\n")}\n`);
}
} else {
let res = inspectValue(actual);
let other = inspectValue(expected);
const knownOperator = kReadableOperator[operator];
if (operator === "notDeepEqual" && res === other) {
res = `${knownOperator}\n\n${res}`;
if (res.length > 1024) {
res = `${StringPrototypeSlice(res, 0, 1021)}...`;
}
super(res);
} else {
if (res.length > kMaxLongStringLength) {
res = `${StringPrototypeSlice(res, 0, 509)}...`;
}
if (other.length > kMaxLongStringLength) {
other = `${StringPrototypeSlice(other, 0, 509)}...`;
}
if (operator === "deepEqual") {
res = `${knownOperator}\n\n${res}\n\nshould loosely deep-equal\n\n`;
} else {
const newOp = kReadableOperator[`${operator}Unequal`];
if (newOp) {
res = `${newOp}\n\n${res}\n\nshould not loosely deep-equal\n\n`;
} else {
other = ` ${operator} ${other}`;
}
}
super(`${res}${other}`);
}
}
}
Error.stackTraceLimit = limit;
this.generatedMessage = !message;
ObjectDefineProperty(this, "name", {
__proto__: null,
value: "AssertionError [ERR_ASSERTION]",
enumerable: false,
writable: true,
configurable: true,
});
this.code = "ERR_ASSERTION";
if (details) {
this.actual = undefined;
this.expected = undefined;
this.operator = undefined;
for (let i = 0; i < details.length; i++) {
this["message " + i] = details[i].message;
this["actual " + i] = details[i].actual;
this["expected " + i] = details[i].expected;
this["operator " + i] = details[i].operator;
this["stack trace " + i] = details[i].stack;
}
} else {
this.actual = actual;
this.expected = expected;
this.operator = operator;
}
ErrorCaptureStackTrace(this, stackStartFn || stackStartFunction);
// Create error message including the error code in the name.
this.stack; // eslint-disable-line no-unused-expressions
// Reset the name.
this.name = "AssertionError";
}
toString() {
return `${this.name} [${this.code}]: ${this.message}`;
}
[inspect.custom](recurseTimes, ctx) {
// Long strings should not be fully inspected.
const tmpActual = this.actual;
const tmpExpected = this.expected;
if (typeof this.actual === "string") {
this.actual = addEllipsis(this.actual);
}
if (typeof this.expected === "string") {
this.expected = addEllipsis(this.expected);
}
// This limits the `actual` and `expected` property default inspection to
// the minimum depth. Otherwise those values would be too verbose compared
// to the actual error message which contains a combined view of these two
// input values.
const result = inspect(this, {
...ctx,
customInspect: false,
depth: 0,
});
// Reset the properties after inspection.
this.actual = tmpActual;
this.expected = tmpExpected;
return result;
}
}
export default AssertionError;

View File

@@ -0,0 +1,141 @@
"use strict";
const {
ArrayPrototypePush,
ArrayPrototypeSlice,
Error,
FunctionPrototype,
ObjectFreeze,
Proxy,
SafeSet,
SafeWeakMap,
} = require("internal/primordials");
const AssertionError = require("internal/assert/assertion_error");
const { validateUint32 } = require("internal/validators");
const noop = FunctionPrototype;
class CallTrackerContext {
#expected;
#calls;
#name;
#stackTrace;
constructor({ expected, stackTrace, name }) {
this.#calls = [];
this.#expected = expected;
this.#stackTrace = stackTrace;
this.#name = name;
}
track(thisArg, args) {
const argsClone = ObjectFreeze(ArrayPrototypeSlice(args));
ArrayPrototypePush(this.#calls, ObjectFreeze({ thisArg, arguments: argsClone }));
}
get delta() {
return this.#calls.length - this.#expected;
}
reset() {
this.#calls = [];
}
getCalls() {
return ObjectFreeze(ArrayPrototypeSlice(this.#calls));
}
report() {
if (this.delta !== 0) {
const message =
`Expected the ${this.#name} function to be ` +
`executed ${this.#expected} time(s) but was ` +
`executed ${this.#calls.length} time(s).`;
return {
message,
actual: this.#calls.length,
expected: this.#expected,
operator: this.#name,
stack: this.#stackTrace,
};
}
}
}
class CallTracker {
#callChecks = new SafeSet();
#trackedFunctions = new SafeWeakMap();
#getTrackedFunction(tracked) {
if (!this.#trackedFunctions.has(tracked)) {
throw $ERR_INVALID_ARG_VALUE("tracked", tracked, "is not a tracked function");
}
return this.#trackedFunctions.get(tracked);
}
reset(tracked) {
if (tracked === undefined) {
this.#callChecks.forEach(check => check.reset());
return;
}
this.#getTrackedFunction(tracked).reset();
}
getCalls(tracked) {
return this.#getTrackedFunction(tracked).getCalls();
}
calls(fn, expected = 1) {
if (process._exiting) throw $ERR_UNAVAILABLE_DURING_EXIT("Cannot call function in process exit handler");
if (typeof fn === "number") {
expected = fn;
fn = noop;
} else if (fn === undefined) {
fn = noop;
}
validateUint32(expected, "expected", true);
const context = new CallTrackerContext({
expected,
// eslint-disable-next-line no-restricted-syntax
stackTrace: new Error(),
name: fn.name || "calls",
});
const tracked = new Proxy(fn, {
__proto__: null,
apply(fn, thisArg, argList) {
context.track(thisArg, argList);
return fn.$apply(thisArg, argList);
},
});
this.#callChecks.add(context);
this.#trackedFunctions.set(tracked, context);
return tracked;
}
report() {
const errors = [];
for (const context of this.#callChecks) {
const message = context.report();
if (message !== undefined) {
ArrayPrototypePush(errors, message);
}
}
return errors;
}
verify() {
const errors = this.report();
if (errors.length === 0) {
return;
}
const message = errors.length === 1 ? errors[0].message : "Functions were not called the expected number of times";
throw new AssertionError({
message,
details: errors,
});
}
}
export default CallTracker;

View File

@@ -0,0 +1,111 @@
/// <reference path="../../builtins.d.ts" />
"use strict";
const colors = require("internal/util/colors");
const enum Operation {
Insert = 0,
Delete = 1,
Equal = 2,
}
interface Diff {
kind: Operation;
/**
* When diffing chars (that is, `line == false`, this is a char code.)
*/
value: string | number;
}
declare namespace Internal {
export function myersDiff(
actual: string[],
expected: string[],
checkCommaDisparity?: boolean,
lines?: boolean,
): Diff[];
}
const kNopLinesToCollapse = 5;
const { myersDiff } = $zig("node_assert_binding.zig", "generate") as typeof Internal;
function printSimpleMyersDiff(diff: Diff[]) {
let message = "";
for (let diffIdx = diff.length - 1; diffIdx >= 0; diffIdx--) {
let { kind, value } = diff[diffIdx];
if (typeof value === "number") {
value = String.fromCharCode(value);
}
switch (kind) {
case Operation.Insert:
message += `${colors.green}${value}${colors.white}`;
break;
case Operation.Delete:
message += `${colors.red}${value}${colors.white}`;
break;
case Operation.Equal:
message += `${colors.white}${value}${colors.white}`;
break;
default:
throw new TypeError(`Invalid diff operation kind: ${kind}`); // should be unreachable
}
}
return `\n${message}`;
}
function printMyersDiff(diff: Diff[], simple = false) {
let message = "";
let skipped = false;
let nopCount = 0;
for (let diffIdx = diff.length - 1; diffIdx >= 0; diffIdx--) {
const { kind, value } = diff[diffIdx];
$assert(
typeof value !== "number",
"printMyersDiff is only called for line diffs, which never return numeric char code values.",
);
const previousType = diffIdx < diff.length - 1 ? diff[diffIdx + 1].kind : null;
const typeChanged = previousType && kind !== previousType;
if (typeChanged && previousType === Operation.Equal) {
// Avoid grouping if only one line would have been grouped otherwise
if (nopCount === kNopLinesToCollapse + 1) {
message += `${colors.white} ${diff[diffIdx + 1].value}\n`;
} else if (nopCount === kNopLinesToCollapse + 2) {
message += `${colors.white} ${diff[diffIdx + 2].value}\n`;
message += `${colors.white} ${diff[diffIdx + 1].value}\n`;
}
if (nopCount >= kNopLinesToCollapse + 3) {
message += `${colors.blue}...${colors.white}\n`;
message += `${colors.white} ${diff[diffIdx + 1].value}\n`;
skipped = true;
}
nopCount = 0;
}
switch (kind) {
case Operation.Insert:
message += `${colors.green}+${colors.white} ${value}\n`;
break;
case Operation.Delete:
message += `${colors.red}-${colors.white} ${value}\n`;
break;
case Operation.Equal:
if (nopCount < kNopLinesToCollapse) {
message += `${colors.white} ${value}\n`;
}
nopCount++;
break;
default:
throw new TypeError(`Invalid diff operation kind: ${kind}`); // should be unreachable
}
}
message = message.trimEnd();
return { message: `\n${message}`, skipped };
}
export default { myersDiff, printMyersDiff, printSimpleMyersDiff };

View File

@@ -0,0 +1,288 @@
/* prettier-ignore */
'use strict';
// const {
// ArrayPrototypeShift,
// Error,
// ErrorCaptureStackTrace,
// FunctionPrototypeBind,
// RegExpPrototypeSymbolReplace,
// SafeMap,
// StringPrototypeCharCodeAt,
// StringPrototypeIncludes,
// StringPrototypeIndexOf,
// StringPrototypeReplace,
// StringPrototypeSlice,
// StringPrototypeSplit,
// StringPrototypeStartsWith,
// } = require("internal/primordials");
var AssertionError;
function loadAssertionError() {
if (AssertionError === undefined) {
AssertionError = require("internal/assert/assertion_error");
}
}
// const { Buffer } = require('node:buffer');
// const {
// isErrorStackTraceLimitWritable,
// overrideStackTrace,
// } = require('internal/errors');
// const { openSync, closeSync, readSync } = require('node:fs');
// // const { EOL } = require('internal/constants');
// // const { BuiltinModule } = require('internal/bootstrap/realm');
// // const { isError } = require('internal/util');
// const errorCache = new SafeMap();
// // const { fileURLToPath } = require('internal/url');
// let parseExpressionAt;
// let findNodeAround;
// let tokenizer;
// let decoder;
// // Escape control characters but not \n and \t to keep the line breaks and
// // indentation intact.
// // eslint-disable-next-line no-control-regex
// const escapeSequencesRegExp = /[\x00-\x08\x0b\x0c\x0e-\x1f]/g;
// const meta = [
// '\\u0000', '\\u0001', '\\u0002', '\\u0003', '\\u0004',
// '\\u0005', '\\u0006', '\\u0007', '\\b', '',
// '', '\\u000b', '\\f', '', '\\u000e',
// '\\u000f', '\\u0010', '\\u0011', '\\u0012', '\\u0013',
// '\\u0014', '\\u0015', '\\u0016', '\\u0017', '\\u0018',
// '\\u0019', '\\u001a', '\\u001b', '\\u001c', '\\u001d',
// '\\u001e', '\\u001f',
// ];
// const escapeFn = (str) => meta[StringPrototypeCharCodeAt(str, 0)];
// function findColumn(fd, column: number, code: string) {
// if (code.length > column + 100) {
// try {
// return parseCode(code, column);
// } catch {
// // End recursion in case no code could be parsed. The expression should
// // have been found after 2500 characters, so stop trying.
// if (code.length - column > 2500) {
// // eslint-disable-next-line no-throw-literal
// throw null;
// }
// }
// }
// // Read up to 2500 bytes more than necessary in columns. That way we address
// // multi byte characters and read enough data to parse the code.
// const bytesToRead = column - code.length + 2500;
// const buffer = Buffer.allocUnsafe(bytesToRead);
// const bytesRead = readSync(fd, buffer, 0, bytesToRead);
// code += decoder.write(buffer.slice(0, bytesRead));
// // EOF: fast path.
// if (bytesRead < bytesToRead) {
// return parseCode(code, column);
// }
// // Read potentially missing code.
// return findColumn(fd, column, code);
// }
// function getCode(fd, line: number, column: number) {
// let bytesRead = 0;
// if (line === 0) {
// // Special handle line number one. This is more efficient and simplifies the
// // rest of the algorithm. Read more than the regular column number in bytes
// // to prevent multiple reads in case multi byte characters are used.
// return findColumn(fd, column, '');
// }
// let lines = 0;
// // Prevent blocking the event loop by limiting the maximum amount of
// // data that may be read.
// let maxReads = 32; // bytesPerRead * maxReads = 512 KiB
// const bytesPerRead = 16384;
// // Use a single buffer up front that is reused until the call site is found.
// let buffer = Buffer.allocUnsafe(bytesPerRead);
// while (maxReads-- !== 0) {
// // Only allocate a new buffer in case the needed line is found. All data
// // before that can be discarded.
// buffer = lines < line ? buffer : Buffer.allocUnsafe(bytesPerRead);
// bytesRead = readSync(fd, buffer, 0, bytesPerRead);
// // Read the buffer until the required code line is found.
// for (let i = 0; i < bytesRead; i++) {
// if (buffer[i] === 10 && ++lines === line) {
// // If the end of file is reached, directly parse the code and return.
// if (bytesRead < bytesPerRead) {
// return parseCode(buffer.toString('utf8', i + 1, bytesRead), column);
// }
// // Check if the read code is sufficient or read more until the whole
// // expression is read. Make sure multi byte characters are preserved
// // properly by using the decoder.
// const code = decoder.write(buffer.slice(i + 1, bytesRead));
// return findColumn(fd, column, code);
// }
// }
// }
// }
// TODO: parse source to get assertion message
// function parseCode(code, offset) {
// // Lazy load acorn.
// if (parseExpressionAt === undefined) {
// const Parser = require('internal/deps/acorn/acorn/dist/acorn').Parser;
// ({ findNodeAround } = require('internal/deps/acorn/acorn-walk/dist/walk'));
// parseExpressionAt = FunctionPrototypeBind(Parser.parseExpressionAt, Parser);
// tokenizer = FunctionPrototypeBind(Parser.tokenizer, Parser);
// }
// let node;
// let start;
// // Parse the read code until the correct expression is found.
// for (const token of tokenizer(code, { ecmaVersion: 'latest' })) {
// start = token.start;
// if (start > offset) {
// // No matching expression found. This could happen if the assert
// // expression is bigger than the provided buffer.
// break;
// }
// try {
// node = parseExpressionAt(code, start, { ecmaVersion: 'latest' });
// // Find the CallExpression in the tree.
// node = findNodeAround(node, offset, 'CallExpression');
// if (node?.node.end >= offset) {
// return [
// node.node.start,
// StringPrototypeReplace(StringPrototypeSlice(code,
// node.node.start, node.node.end),
// escapeSequencesRegExp, escapeFn),
// ];
// }
// // eslint-disable-next-line no-unused-vars
// } catch (err) {
// continue;
// }
// }
// // eslint-disable-next-line no-throw-literal
// throw null;
// }
function getErrMessage(message: string, value: unknown, fn: Function): string | undefined {
// const tmpLimit = Error.stackTraceLimit;
// const errorStackTraceLimitIsWritable = isErrorStackTraceLimitWritable();
// Make sure the limit is set to 1. Otherwise it could fail (<= 0) or it
// does to much work.
// if (errorStackTraceLimitIsWritable) Error.stackTraceLimit = 1;
// We only need the stack trace. To minimize the overhead use an object
// instead of an error.
// const err = {};
// ErrorCaptureStackTrace(err, fn);
// if (errorStackTraceLimitIsWritable) Error.stackTraceLimit = tmpLimit;
// overrideStackTrace.set(err, (_, stack) => stack);
// const call = err.stack[0];
//
// if (fn.name === "ok") {
// return `The expression evaluated to a falsy value:\n\n assert.ok(${value})\n`;
// }
// let filename = call.getFileName();
// const line = call.getLineNumber() - 1;
// let column = call.getColumnNumber() - 1;
// let identifier;
// let code;
// if (filename) {
// identifier = `${filename}${line}${column}`;
// // Skip Node.js modules!
// if (StringPrototypeStartsWith(filename, 'node:') &&
// BuiltinModule.exists(StringPrototypeSlice(filename, 5))) {
// errorCache.set(identifier, undefined);
// return;
// }
// } else {
// return message;
// }
// if (errorCache.has(identifier)) {
// return errorCache.get(identifier);
// }
// let fd;
// try {
// // Set the stack trace limit to zero. This makes sure unexpected token
// // errors are handled faster.
// if (errorStackTraceLimitIsWritable) Error.stackTraceLimit = 0;
// if (filename) {
// if (decoder === undefined) {
// const { StringDecoder } = require('string_decoder');
// decoder = new StringDecoder('utf8');
// }
// // ESM file prop is a file proto. Convert that to path.
// // This ensure opensync will not throw ENOENT for ESM files.
// const fileProtoPrefix = 'file://';
// if (StringPrototypeStartsWith(filename, fileProtoPrefix)) {
// filename = Bun.fileURLToPath(filename);
// }
// fd = openSync(filename, 'r', 0o666);
// // Reset column and message.
// ({ 0: column, 1: message } = getCode(fd, line, column));
// // Flush unfinished multi byte characters.
// decoder.end();
// } else {
// for (let i = 0; i < line; i++) {
// code = StringPrototypeSlice(code,
// StringPrototypeIndexOf(code, '\n') + 1);
// }
// // ({ 0: column, 1: message } = parseCode(code, column));
// throw new Error("todo: parseCode");
// }
// // Always normalize indentation, otherwise the message could look weird.
// if (StringPrototypeIncludes(message, '\n')) {
// if (process.platform === 'win32') {
// message = RegExpPrototypeSymbolReplace(/\r\n/g, message, '\n');
// }
// const frames = StringPrototypeSplit(message, '\n');
// message = ArrayPrototypeShift(frames);
// for (const frame of frames) {
// let pos = 0;
// while (pos < column && (frame[pos] === ' ' || frame[pos] === '\t')) {
// pos++;
// }
// message += `\n ${StringPrototypeSlice(frame, pos)}`;
// }
// }
// message = `The expression evaluated to a falsy value:\n\n ${message}\n`;
// // Make sure to always set the cache! No matter if the message is
// // undefined or not
// errorCache.set(identifier, message);
// return message;
// } catch {
// // Invalidate cache to prevent trying to read this part again.
// errorCache.set(identifier, undefined);
// } finally {
// // Reset limit.
// if (errorStackTraceLimitIsWritable) Error.stackTraceLimit = tmpLimit;
// if (fd !== undefined)
// closeSync(fd);
// }
}
export function innerOk(fn, argLen, value, message) {
if (!value) {
let generatedMessage = false;
if (argLen === 0) {
generatedMessage = true;
message = "No value argument passed to `assert.ok()`";
} else if (message == null) {
generatedMessage = true;
message = getErrMessage(message, value, fn);
// TODO: message
} else if (Error.isError(message)) {
throw message;
}
if (AssertionError === undefined) loadAssertionError();
const err = new AssertionError({
actual: value,
expected: true,
message,
operator: "==",
stackStartFn: fn,
});
err.generatedMessage = generatedMessage;
throw err;
}
}

View File

@@ -76,10 +76,13 @@ const StringIterator = uncurryThis(String.prototype[Symbol.iterator]);
const StringIteratorPrototype = Reflect.getPrototypeOf(StringIterator(""));
const ArrayPrototypeForEach = uncurryThis(Array.prototype.forEach);
function ErrorCaptureStackTrace(targetObject) {
const stack = new Error().stack;
function ErrorCaptureStackTrace(targetObject, maybeStartStackFn) {
Error.captureStackTrace(targetObject, maybeStartStackFn);
if (maybeStartStackFn === undefined) {
// Remove the second line, which is this function
targetObject.stack = stack.replace(/.*\n.*/, "$1");
targetObject.stack = targetObject.stack.replace(/.*\n.*/, "$1");
}
}
const arrayProtoPush = Array.prototype.push;
@@ -94,6 +97,7 @@ export default {
ArrayPrototypeFlat: uncurryThis(Array.prototype.flat),
ArrayPrototypeFilter: uncurryThis(Array.prototype.filter),
ArrayPrototypeForEach,
ArrayPrototypeFill: uncurryThis(Array.prototype.fill),
ArrayPrototypeIncludes: uncurryThis(Array.prototype.includes),
ArrayPrototypeIndexOf: uncurryThis(Array.prototype.indexOf),
ArrayPrototypeJoin: uncurryThis(Array.prototype.join),
@@ -110,9 +114,15 @@ export default {
DatePrototypeGetTime: uncurryThis(Date.prototype.getTime),
DatePrototypeToISOString: uncurryThis(Date.prototype.toISOString),
DatePrototypeToString: uncurryThis(Date.prototype.toString),
Error,
ErrorCaptureStackTrace,
ErrorPrototypeToString: uncurryThis(Error.prototype.toString),
FunctionPrototypeBind: uncurryThis(Function.prototype.bind),
FunctionPrototypeCall: uncurryThis(Function.prototype["call"]),
FunctionPrototypeToString: uncurryThis(Function.prototype.toString),
JSONStringify: JSON.stringify,
MapPrototypeDelete: uncurryThis(Map.prototype.delete),
MapPrototypeSet: uncurryThis(Map.prototype.set),
MapPrototypeGetSize: getGetter(Map, "size"),
MapPrototypeEntries: uncurryThis(Map.prototype.entries),
MapPrototypeValues: uncurryThis(Map.prototype.values),
@@ -144,10 +154,12 @@ export default {
ObjectIs: Object.is,
ObjectKeys: Object.keys,
ObjectPrototypeHasOwnProperty: uncurryThis(Object.prototype.hasOwnProperty),
ObjectPrototypeIsPrototypeOf: uncurryThis(Object.prototype.isPrototypeOf),
ObjectPrototypePropertyIsEnumerable: uncurryThis(Object.prototype.propertyIsEnumerable),
ObjectPrototypeToString: uncurryThis(Object.prototype.toString),
ObjectSeal: Object.seal,
ObjectSetPrototypeOf: Object.setPrototypeOf,
ReflectHas: Reflect.has,
ReflectOwnKeys: Reflect.ownKeys,
RegExp,
RegExpPrototypeExec: uncurryThis(RegExp.prototype.exec),
@@ -172,12 +184,21 @@ export default {
}
},
),
SafeWeakSet: makeSafe(
WeakSet,
class SafeWeakSet extends WeakSet {
constructor(i) {
super(i);
}
},
),
DatePrototypeGetMilliseconds: uncurryThis(Date.prototype.getMilliseconds),
DatePrototypeToUTCString: uncurryThis(Date.prototype.toUTCString),
SetPrototypeGetSize: getGetter(Set, "size"),
SetPrototypeEntries: uncurryThis(Set.prototype.entries),
SetPrototypeValues: uncurryThis(Set.prototype.values),
String,
StringPrototypeAt: uncurryThis(String.prototype.at),
StringPrototypeCharCodeAt: uncurryThis(String.prototype.charCodeAt),
StringPrototypeCodePointAt: uncurryThis(String.prototype.codePointAt),
StringPrototypeEndsWith: uncurryThis(String.prototype.endsWith),
@@ -200,8 +221,6 @@ export default {
StringPrototypeValueOf: uncurryThis(String.prototype.valueOf),
SymbolPrototypeToString: uncurryThis(Symbol.prototype.toString),
SymbolPrototypeValueOf: uncurryThis(Symbol.prototype.valueOf),
FunctionPrototypeToString: uncurryThis(Function.prototype.toString),
FunctionPrototypeBind: uncurryThis(Function.prototype.bind),
SymbolDispose: Symbol.dispose,
SymbolAsyncDispose: Symbol.asyncDispose,
SymbolIterator: Symbol.iterator,

View File

@@ -0,0 +1,59 @@
// Taken from Node - lib/internal/util/colors.js
"use strict";
type WriteStream = import("node:tty").WriteStream;
type GetColorDepth = (this: import("node:tty").WriteStream, env?: NodeJS.ProcessEnv) => number;
let getColorDepth: undefined | GetColorDepth;
const lazyGetColorDepth = (): GetColorDepth =>
(getColorDepth ??= require("node:tty").WriteStream.prototype.getColorDepth);
let exports = {
blue: "",
green: "",
white: "",
yellow: "",
red: "",
gray: "",
clear: "",
reset: "",
hasColors: false,
shouldColorize(stream: WriteStream) {
if (stream?.isTTY) {
const depth = lazyGetColorDepth().$call(stream);
console.error("stream is a tty with color depth", depth);
return depth > 2;
}
// do not cache these since users may update them as the process runs
const { NO_COLOR, NODE_DISABLE_COLORS, FORCE_COLOR } = process.env;
return NO_COLOR === undefined && NODE_DISABLE_COLORS === undefined && FORCE_COLOR !== "0";
},
refresh(): void {
if (exports.shouldColorize(process.stderr)) {
exports.blue = "\u001b[34m";
exports.green = "\u001b[32m";
exports.white = "\u001b[39m";
exports.yellow = "\u001b[33m";
exports.red = "\u001b[31m";
exports.gray = "\u001b[90m";
exports.clear = "\u001bc";
exports.reset = "\u001b[0m";
exports.hasColors = true;
} else {
exports.blue = "";
exports.green = "";
exports.white = "";
exports.yellow = "";
exports.red = "";
exports.gray = "";
exports.clear = "";
exports.reset = "";
exports.hasColors = false;
}
},
};
exports.refresh();
export default exports;

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"esModuleInterop": true,
// Path remapping
"baseUrl": ".",
"paths": {

View File

@@ -994,6 +994,7 @@ pub const String = extern struct {
return ZigString.Slice.empty;
}
/// use `byteSlice` to get a `[]const u8`.
pub fn toSlice(this: String, allocator: std.mem.Allocator) SliceWithUnderlyingString {
return SliceWithUnderlyingString{
.utf8 = this.toUTF8(allocator),

View File

@@ -6,7 +6,7 @@ test("doesNotMatch does not throw when not matching", () => {
test("doesNotMatch throws when argument is not string", () => {
expect(() => assert.doesNotMatch(123, /pass/)).toThrow(
'The "actual" argument must be of type string. Received type number',
'The "string" argument must be of type string. Received type number',
);
});

View File

@@ -5,7 +5,7 @@ test("match does not throw when matching", () => {
});
test("match throws when argument is not string", () => {
expect(() => assert.match(123, /pass/)).toThrow('The "actual" argument must be of type string. Received type number');
expect(() => assert.match(123, /pass/)).toThrow('The "string" argument must be of type string. Received type number');
});
test("match throws when not matching", () => {

View File

@@ -1 +1,2 @@
preload = ["./harness.ts"]
[test]
preload = ["./harness.ts", "../../preload.ts"]

View File

@@ -659,7 +659,7 @@ describe("fork", () => {
code: "ERR_INVALID_ARG_TYPE",
name: "TypeError",
message: expect.stringContaining(
`The "modulePath" argument must be of type string, Buffer, or URL. Received `,
`The "modulePath" argument must be of type string, Buffer or URL. Received `,
),
}),
);

View File

@@ -2,6 +2,7 @@
* @note this file patches `node:test` via the require cache.
*/
import { AnyFunction } from "bun";
import os from "node:os";
import { hideFromStackTrace } from "harness";
import assertNode from "node:assert";
@@ -266,9 +267,9 @@ declare namespace Bun {
function jest(path: string): typeof import("bun:test");
}
if (Bun.main.includes("node/test/parallel")) {
const normalized = os.platform() === "win32" ? Bun.main.replaceAll("\\", "/") : Bun.main;
if (normalized.includes("node/test/parallel")) {
function createMockNodeTestModule() {
interface TestError extends Error {
testStack: string[];
}
@@ -279,8 +280,8 @@ if (Bun.main.includes("node/test/parallel")) {
successes: number;
addFailure(err: unknown): TestError;
recordSuccess(): void;
}
const contexts: Record</* requiring file */ string, Context> = {}
};
const contexts: Record</* requiring file */ string, Context> = {};
// @ts-ignore
let activeSuite: Context = undefined;
@@ -305,13 +306,13 @@ if (Bun.main.includes("node/test/parallel")) {
const fullname = this.testStack.join(" > ");
console.log("✅ Test passed:", fullname);
this.successes++;
}
}
},
};
}
function getContext() {
const key: string = Bun.main; // module.parent?.filename ?? require.main?.filename ?? __filename;
return activeSuite = (contexts[key] ??= createContext(key));
return (activeSuite = contexts[key] ??= createContext(key));
}
async function test(label: string | Function, fn?: Function | undefined) {
@@ -333,7 +334,7 @@ if (Bun.main.includes("node/test/parallel")) {
}
function describe(labelOrFn: string | Function, maybeFn?: Function) {
const [label, fn] = (typeof labelOrFn == "function" ? [labelOrFn.name, labelOrFn] : [labelOrFn, maybeFn]);
const [label, fn] = typeof labelOrFn == "function" ? [labelOrFn.name, labelOrFn] : [labelOrFn, maybeFn];
if (typeof fn !== "function") throw new TypeError("Second argument to describe() must be a function.");
getContext().testStack.push(label);
@@ -341,7 +342,7 @@ if (Bun.main.includes("node/test/parallel")) {
fn();
} catch (e) {
getContext().addFailure(e);
throw e
throw e;
} finally {
getContext().testStack.pop();
}
@@ -352,14 +353,12 @@ if (Bun.main.includes("node/test/parallel")) {
if (failures > 0) {
throw new Error(`${failures} tests failed.`);
}
}
return {
test,
describe,
}
};
}
require.cache["node:test"] ??= {

View File

@@ -107,7 +107,7 @@ function parseTestFlags(filename = process.argv[1]) {
// `worker_threads`) and child processes.
// If the binary was built without-ssl then the crypto flags are
// invalid (bad option). The test itself should handle this case.
if (process.argv.length === 2 &&
if ((process.argv.length === 2 || process.argv.length === 3) &&
!process.env.NODE_SKIP_FLAG_CHECK &&
isMainThread &&
hasCrypto &&

View File

@@ -0,0 +1,8 @@
A good deal of parallel test cases can be run directly via `bun <filename>`.
However, some newer cases use `node:test`.
Files in this directory need to be run with `bun test <filename>`. The
`node:test` module is shimmed via a require cache hack in
`test/js/node/harness.js` to use `bun:test`. Note that our test runner
(`scripts/runner.node.mjs`) checks for `needs-test` in the names of test files,
so don't rename this folder without updating that code.

File diff suppressed because it is too large Load Diff

View File

@@ -27,8 +27,8 @@ const cmd = `"${process.execPath}" "${__filename}" child`;
cp.exec(cmd, {
timeout: kTimeoutNotSupposedToExpire
}, common.mustSucceed((stdout, stderr) => {
assert.strictEqual(stdout.trim(), 'child stdout');
assert.strictEqual(stderr.trim(), 'child stderr');
assert.strict(stdout.trim().includes('child stdout'));
assert.strict(stderr.trim().includes('child stderr'));
}));
cleanupStaleProcess(__filename);

View File

@@ -48,7 +48,9 @@ function checkSpawnSyncRet(ret) {
function verifyBufOutput(ret) {
checkSpawnSyncRet(ret);
assert.deepStrictEqual(ret.stdout.toString('utf8'), msgOutBuf.toString('utf8'));
assert.deepStrictEqual(ret.stdout, msgOutBuf);
assert.deepStrictEqual(ret.stderr.toString('utf8'), msgErrBuf.toString('utf8'));
assert.deepStrictEqual(ret.stderr, msgErrBuf);
}