mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
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:
4
bunfig.node-test.toml
Normal file
4
bunfig.node-test.toml
Normal 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"]
|
||||
@@ -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,9 +283,9 @@ async function runTests() {
|
||||
stdoutPreview: stdoutPreview,
|
||||
};
|
||||
});
|
||||
continue;
|
||||
} else {
|
||||
await runTest(title, async () => spawnBunTest(execPath, join("test", testPath)));
|
||||
}
|
||||
await runTest(title, async () => spawnBunTest(execPath, join("test", testPath)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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");
|
||||
},
|
||||
}
|
||||
|
||||
@@ -351,10 +351,10 @@ 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(" or "_s);
|
||||
result.append(expected_types.at(length - 1).toWTFString(globalObject));
|
||||
}
|
||||
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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,
|
||||
|
||||
631
src/bun.js/node/assert/myers_diff.zig
Normal file
631
src/bun.js/node/assert/myers_diff.zig
Normal 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;
|
||||
}
|
||||
132
src/bun.js/node/node_assert.zig
Normal file
132
src/bun.js/node/node_assert.zig
Normal 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.", .{}),
|
||||
};
|
||||
}
|
||||
86
src/bun.js/node/node_assert_binding.zig
Normal file
86
src/bun.js/node/node_assert_binding.zig
Normal 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;
|
||||
}
|
||||
@@ -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
10
src/js/builtins.d.ts
vendored
@@ -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;
|
||||
|
||||
|
||||
425
src/js/internal/assert/assertion_error.ts
Normal file
425
src/js/internal/assert/assertion_error.ts
Normal 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;
|
||||
141
src/js/internal/assert/calltracker.ts
Normal file
141
src/js/internal/assert/calltracker.ts
Normal 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;
|
||||
111
src/js/internal/assert/myers_diff.ts
Normal file
111
src/js/internal/assert/myers_diff.ts
Normal 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 };
|
||||
288
src/js/internal/assert/utils.ts
Normal file
288
src/js/internal/assert/utils.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
// Remove the second line, which is this function
|
||||
targetObject.stack = stack.replace(/.*\n.*/, "$1");
|
||||
function ErrorCaptureStackTrace(targetObject, maybeStartStackFn) {
|
||||
Error.captureStackTrace(targetObject, maybeStartStackFn);
|
||||
|
||||
if (maybeStartStackFn === undefined) {
|
||||
// Remove the second line, which is this function
|
||||
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,
|
||||
|
||||
59
src/js/internal/util/colors.ts
Normal file
59
src/js/internal/util/colors.ts
Normal 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
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"esModuleInterop": true,
|
||||
// Path remapping
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
preload = ["./harness.ts"]
|
||||
[test]
|
||||
preload = ["./harness.ts", "../../preload.ts"]
|
||||
|
||||
@@ -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 `,
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
/**
|
||||
* @note this file patches `node:test` via the require cache.
|
||||
*/
|
||||
import {AnyFunction} from "bun";
|
||||
import {hideFromStackTrace} from "harness";
|
||||
import { AnyFunction } from "bun";
|
||||
import os from "node:os";
|
||||
import { hideFromStackTrace } from "harness";
|
||||
import assertNode from "node:assert";
|
||||
|
||||
type DoneCb = (err?: Error) => any;
|
||||
function noop() {}
|
||||
export function createTest(path: string) {
|
||||
const {expect, test, it, describe, beforeAll, afterAll, beforeEach, afterEach, mock} = Bun.jest(path);
|
||||
const { expect, test, it, describe, beforeAll, afterAll, beforeEach, afterEach, mock } = Bun.jest(path);
|
||||
|
||||
hideFromStackTrace(expect);
|
||||
|
||||
@@ -204,11 +205,11 @@ export function createTest(path: string) {
|
||||
let completed = 0;
|
||||
const globalTimer = globalTimeout
|
||||
? (timers.push(
|
||||
setTimeout(() => {
|
||||
console.log("Global Timeout");
|
||||
done(new Error("Timed out!"));
|
||||
}, globalTimeout),
|
||||
),
|
||||
setTimeout(() => {
|
||||
console.log("Global Timeout");
|
||||
done(new Error("Timed out!"));
|
||||
}, globalTimeout),
|
||||
),
|
||||
timers[timers.length - 1])
|
||||
: undefined;
|
||||
function createDoneCb(timeout?: number) {
|
||||
@@ -216,11 +217,11 @@ export function createTest(path: string) {
|
||||
const timer =
|
||||
timeout !== undefined
|
||||
? (timers.push(
|
||||
setTimeout(() => {
|
||||
console.log("Timeout");
|
||||
done(new Error("Timed out!"));
|
||||
}, timeout),
|
||||
),
|
||||
setTimeout(() => {
|
||||
console.log("Timeout");
|
||||
done(new Error("Timed out!"));
|
||||
}, timeout),
|
||||
),
|
||||
timers[timers.length - 1])
|
||||
: timeout;
|
||||
return (result?: Error) => {
|
||||
@@ -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));
|
||||
const key: string = Bun.main; // module.parent?.filename ?? require.main?.filename ?? __filename;
|
||||
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"] ??= {
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
8
test/js/node/test/parallel/needs-test/README.md
Normal file
8
test/js/node/test/parallel/needs-test/README.md
Normal 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.
|
||||
1601
test/js/node/test/parallel/needs-test/test-assert.js
Normal file
1601
test/js/node/test/parallel/needs-test/test-assert.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user