fix(node/assert): port more test cases from node (#16895)

Co-authored-by: DonIsaac <22823424+DonIsaac@users.noreply.github.com>
Co-authored-by: Dylan Conway <35280289+dylan-conway@users.noreply.github.com>
This commit is contained in:
Don Isaac
2025-02-06 14:29:59 -08:00
committed by GitHub
parent 253faed1cf
commit 146ec7791b
25 changed files with 993 additions and 93 deletions

13
.vscode/launch.json generated vendored
View File

@@ -7,6 +7,19 @@
// - "cppvsdbg" is used instead of "lldb" on Windows, because "lldb" is too slow
"version": "0.2.0",
"configurations": [
{
"type": "bun",
"request": "launch",
"name": "[js] bun test [file]",
"runtime": "${workspaceFolder}/build/debug/bun-debug",
"args": ["test", "${file}"],
"cwd": "${workspaceFolder}",
"env": {
"BUN_DEBUG_QUIET_LOGS": "1",
"BUN_DEBUG_jest": "1",
"BUN_GARBAGE_COLLECTOR_LEVEL": "1",
},
},
// bun test [file]
{
"type": "lldb",

View File

@@ -492,12 +492,12 @@ JSC_DEFINE_HOST_FUNCTION(functionBunDeepEquals, (JSGlobalObject * globalObject,
JSC::JSValue arg1 = callFrame->uncheckedArgument(0);
JSC::JSValue arg2 = callFrame->uncheckedArgument(1);
JSC::JSValue arg3 = callFrame->argument(2);
JSC::JSValue strict = callFrame->argument(2);
Vector<std::pair<JSValue, JSValue>, 16> stack;
MarkedArgumentBuffer gcBuffer;
if (arg3.isBoolean() && arg3.asBoolean()) {
if (strict.isBoolean() && strict.asBoolean()) {
bool isEqual = Bun__deepEquals<true, false>(globalObject, arg1, arg2, gcBuffer, stack, &scope, true);
RETURN_IF_EXCEPTION(scope, {});

View File

@@ -672,6 +672,10 @@ template<bool isStrict, bool enableAsymmetricMatchers>
bool Bun__deepEquals(JSC__JSGlobalObject* globalObject, JSValue v1, JSValue v2, MarkedArgumentBuffer& gcBuffer, Vector<std::pair<JSC::JSValue, JSC::JSValue>, 16>& stack, ThrowScope* scope, bool addToStack)
{
VM& vm = globalObject->vm();
if (UNLIKELY(!vm.isSafeToRecurse())) {
throwStackOverflowError(globalObject, *scope);
return false;
}
// need to check this before primitives, asymmetric matchers
// can match against any type of value.
@@ -754,7 +758,7 @@ bool Bun__deepEquals(JSC__JSGlobalObject* globalObject, JSValue v1, JSValue v2,
if (v1Array != v2Array)
return false;
if (v1Array && v2Array) {
if (v1Array && v2Array && !(o1->isProxy() || o2->isProxy())) {
JSC::JSArray* array1 = JSC::jsCast<JSC::JSArray*>(v1);
JSC::JSArray* array2 = JSC::jsCast<JSC::JSArray*>(v2);
@@ -1070,6 +1074,7 @@ std::optional<bool> specialObjectsDequal(JSC__JSGlobalObject* globalObject, Mark
foundMatchingKey = true;
break;
}
RETURN_IF_EXCEPTION(*scope, false);
}
if (!foundMatchingKey) {
@@ -1108,6 +1113,7 @@ std::optional<bool> specialObjectsDequal(JSC__JSGlobalObject* globalObject, Mark
foundMatchingKey = true;
break;
}
RETURN_IF_EXCEPTION(*scope, false);
}
if (!foundMatchingKey) {
@@ -1187,6 +1193,7 @@ std::optional<bool> specialObjectsDequal(JSC__JSGlobalObject* globalObject, Mark
return false;
}
// NOTE(@DonIsaac): could `left` ever _not_ be a JSC::ErrorInstance?
if (JSC::ErrorInstance* left = jsDynamicCast<JSC::ErrorInstance*, JSCell>(c1)) {
JSC::ErrorInstance* right = jsDynamicCast<JSC::ErrorInstance*, JSCell>(c2);
@@ -1194,9 +1201,109 @@ std::optional<bool> specialObjectsDequal(JSC__JSGlobalObject* globalObject, Mark
return false;
}
return (
left->sanitizedNameString(globalObject) == right->sanitizedNameString(globalObject) && left->sanitizedMessageString(globalObject) == right->sanitizedMessageString(globalObject));
if (
left->errorType() != right->errorType() || // quick check on ctors (does not handle subclasses)
left->sanitizedNameString(globalObject) != right->sanitizedNameString(globalObject) || // manual `.name` changes (usually in subclasses)
left->sanitizedMessageString(globalObject) != right->sanitizedMessageString(globalObject) // `.message`
) {
return false;
}
if constexpr (isStrict) {
if (left->runtimeTypeForCause() != right->runtimeTypeForCause()) {
return false;
}
}
VM& vm = globalObject->vm();
// `.cause` is non-enumerable, so it must be checked explicitly.
// note that an undefined cause is different than a missing cause in
// strict mode.
const PropertyName cause(vm.propertyNames->cause);
if constexpr (isStrict) {
if (left->hasProperty(globalObject, cause) != right->hasProperty(globalObject, cause)) {
return false;
}
}
auto leftCause = left->get(globalObject, cause);
RETURN_IF_EXCEPTION(*scope, false);
auto rightCause = right->get(globalObject, cause);
RETURN_IF_EXCEPTION(*scope, false);
if (!Bun__deepEquals<isStrict, enableAsymmetricMatchers>(globalObject, leftCause, rightCause, gcBuffer, stack, scope, true)) {
return false;
}
RETURN_IF_EXCEPTION(*scope, false);
// check arbitrary enumerable properties. `.stack` is not checked.
left->materializeErrorInfoIfNeeded(vm);
right->materializeErrorInfoIfNeeded(vm);
JSC::PropertyNameArray a1(vm, PropertyNameMode::StringsAndSymbols, PrivateSymbolMode::Exclude);
JSC::PropertyNameArray a2(vm, PropertyNameMode::StringsAndSymbols, PrivateSymbolMode::Exclude);
left->getPropertyNames(globalObject, a1, DontEnumPropertiesMode::Exclude);
RETURN_IF_EXCEPTION(*scope, false);
right->getPropertyNames(globalObject, a2, DontEnumPropertiesMode::Exclude);
RETURN_IF_EXCEPTION(*scope, false);
const size_t propertyArrayLength1 = a1.size();
const size_t propertyArrayLength2 = a2.size();
if constexpr (isStrict) {
if (propertyArrayLength1 != propertyArrayLength2) {
return false;
}
}
// take a property name from one, try to get it from both
size_t i;
for (i = 0; i < propertyArrayLength1; i++) {
Identifier i1 = a1[i];
if (i1 == vm.propertyNames->stack) continue;
PropertyName propertyName1 = PropertyName(i1);
JSValue prop1 = left->get(globalObject, propertyName1);
RETURN_IF_EXCEPTION(*scope, false);
if (UNLIKELY(!prop1)) {
return false;
}
JSValue prop2 = right->getIfPropertyExists(globalObject, propertyName1);
RETURN_IF_EXCEPTION(*scope, false);
if constexpr (!isStrict) {
if (prop1.isUndefined() && prop2.isEmpty()) {
continue;
}
}
if (!prop2) {
return false;
}
if (!Bun__deepEquals<isStrict, enableAsymmetricMatchers>(globalObject, prop1, prop2, gcBuffer, stack, scope, true)) {
return false;
}
RETURN_IF_EXCEPTION(*scope, false);
}
// for the remaining properties in the other object, make sure they are undefined
for (; i < propertyArrayLength2; i++) {
Identifier i2 = a2[i];
if (i2 == vm.propertyNames->stack) continue;
PropertyName propertyName2 = PropertyName(i2);
JSValue prop2 = right->getIfPropertyExists(globalObject, propertyName2);
RETURN_IF_EXCEPTION(*scope, false);
if (!prop2.isUndefined()) {
return false;
}
}
return true;
}
break;
}
case Int8ArrayType:
case Uint8ArrayType:
@@ -1213,6 +1320,11 @@ std::optional<bool> specialObjectsDequal(JSC__JSGlobalObject* globalObject, Mark
if (!isTypedArrayType(static_cast<JSC::JSType>(c2Type)) || c1Type != c2Type) {
return false;
}
auto info = c1->classInfo();
auto info2 = c2->classInfo();
if (!info || !info2) {
return false;
}
JSC::JSArrayBufferView* left = jsCast<JSArrayBufferView*, JSCell>(c1);
JSC::JSArrayBufferView* right = jsCast<JSArrayBufferView*, JSCell>(c2);
@@ -1373,7 +1485,20 @@ std::optional<bool> specialObjectsDequal(JSC__JSGlobalObject* globalObject, Mark
compareAsNormalValue:
break;
}
// globalThis is only equal to globalThis
// NOTE: Zig::GlobalObject is tagged as GlobalProxyType
case GlobalObjectType: {
if (c1Type != c2Type) return false;
auto* g1 = jsDynamicCast<JSC::JSGlobalObject*, JSCell>(c1);
auto* g2 = jsDynamicCast<JSC::JSGlobalObject*, JSCell>(c2);
return g1->m_globalThis == g2->m_globalThis;
}
case GlobalProxyType: {
if (c1Type != c2Type) return false;
auto* gp1 = jsDynamicCast<JSC::JSGlobalProxy*, JSCell>(c1);
auto* gp2 = jsDynamicCast<JSC::JSGlobalProxy*, JSCell>(c2);
return gp1->target()->m_globalThis == gp2->target()->m_globalThis;
}
default: {
break;
}
@@ -1528,6 +1653,19 @@ bool Bun__deepMatch(
return true;
}
// anonymous namespace to avoid name collision
namespace {
template<bool isStrict, bool enableAsymmetricMatchers>
inline bool deepEqualsWrapperImpl(JSC__JSValue a, JSC__JSValue b, JSC__JSGlobalObject* global)
{
auto& vm = global->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
Vector<std::pair<JSC::JSValue, JSC::JSValue>, 16> stack;
MarkedArgumentBuffer args;
return Bun__deepEquals<isStrict, enableAsymmetricMatchers>(global, JSC::JSValue::decode(a), JSC::JSValue::decode(b), args, stack, &scope, true);
}
}
extern "C" {
bool WebCore__FetchHeaders__isEmpty(WebCore__FetchHeaders* arg0)
@@ -2427,35 +2565,6 @@ size_t JSC__VM__heapSize(JSC__VM* arg0)
return arg0->heap.size();
}
// This is very naive!
JSC__JSInternalPromise* JSC__VM__reloadModule(JSC__VM* vm, JSC__JSGlobalObject* arg1,
ZigString arg2)
{
return nullptr;
// JSC::JSMap *map = JSC::jsDynamicCast<JSC::JSMap *>(
// arg1->vm(), arg1->moduleLoader()->getDirect(
// arg1->vm(), JSC::Identifier::fromString(arg1->vm(), "registry"_s)));
// const JSC::Identifier identifier = Zig::toIdentifier(arg2, arg1);
// JSC::JSValue val = JSC::identifierToJSValue(arg1->vm(), identifier);
// if (!map->has(arg1, val)) return nullptr;
// if (JSC::JSObject *registryEntry =
// JSC::jsDynamicCast<JSC::JSObject *>(arg1-> map->get(arg1, val))) {
// auto moduleIdent = JSC::Identifier::fromString(arg1->vm(), "module");
// if (JSC::JSModuleRecord *record = JSC::jsDynamicCast<JSC::JSModuleRecord *>(
// arg1->vm(), registryEntry->getDirect(arg1->vm(), moduleIdent))) {
// registryEntry->putDirect(arg1->vm(), moduleIdent, JSC::jsUndefined());
// JSC::JSModuleRecord::destroy(static_cast<JSC::JSCell *>(record));
// }
// map->remove(arg1, val);
// return JSC__JSModuleLoader__loadAndEvaluateModule(arg1, arg2);
// }
// return nullptr;
}
bool JSC__JSValue__isSameValue(JSC__JSValue JSValue0, JSC__JSValue JSValue1,
JSC__JSGlobalObject* globalObject)
{
@@ -2466,50 +2575,26 @@ bool JSC__JSValue__isSameValue(JSC__JSValue JSValue0, JSC__JSValue JSValue1,
bool JSC__JSValue__deepEquals(JSC__JSValue JSValue0, JSC__JSValue JSValue1, JSC__JSGlobalObject* globalObject)
{
ASSERT_NO_PENDING_EXCEPTION(globalObject);
JSValue v1 = JSValue::decode(JSValue0);
JSValue v2 = JSValue::decode(JSValue1);
ThrowScope scope = DECLARE_THROW_SCOPE(globalObject->vm());
Vector<std::pair<JSValue, JSValue>, 16> stack;
MarkedArgumentBuffer args;
return Bun__deepEquals<false, false>(globalObject, v1, v2, args, stack, &scope, true);
return deepEqualsWrapperImpl<false, false>(JSValue0, JSValue1, globalObject);
}
bool JSC__JSValue__jestDeepEquals(JSC__JSValue JSValue0, JSC__JSValue JSValue1, JSC__JSGlobalObject* globalObject)
{
JSValue v1 = JSValue::decode(JSValue0);
JSValue v2 = JSValue::decode(JSValue1);
ThrowScope scope = DECLARE_THROW_SCOPE(globalObject->vm());
Vector<std::pair<JSValue, JSValue>, 16> stack;
MarkedArgumentBuffer args;
return Bun__deepEquals<false, true>(globalObject, v1, v2, args, stack, &scope, true);
return deepEqualsWrapperImpl<false, true>(JSValue0, JSValue1, globalObject);
}
bool JSC__JSValue__strictDeepEquals(JSC__JSValue JSValue0, JSC__JSValue JSValue1, JSC__JSGlobalObject* globalObject)
{
JSValue v1 = JSValue::decode(JSValue0);
JSValue v2 = JSValue::decode(JSValue1);
ThrowScope scope = DECLARE_THROW_SCOPE(globalObject->vm());
Vector<std::pair<JSValue, JSValue>, 16> stack;
MarkedArgumentBuffer args;
return Bun__deepEquals<true, false>(globalObject, v1, v2, args, stack, &scope, true);
return deepEqualsWrapperImpl<true, false>(JSValue0, JSValue1, globalObject);
}
bool JSC__JSValue__jestStrictDeepEquals(JSC__JSValue JSValue0, JSC__JSValue JSValue1, JSC__JSGlobalObject* globalObject)
{
JSValue v1 = JSValue::decode(JSValue0);
JSValue v2 = JSValue::decode(JSValue1);
ThrowScope scope = DECLARE_THROW_SCOPE(globalObject->vm());
Vector<std::pair<JSValue, JSValue>, 16> stack;
MarkedArgumentBuffer args;
return Bun__deepEquals<true, true>(globalObject, v1, v2, args, stack, &scope, true);
return deepEqualsWrapperImpl<true, true>(JSValue0, JSValue1, globalObject);
}
#undef IMPL_DEEP_EQUALS_WRAPPER
bool JSC__JSValue__deepMatch(JSC__JSValue JSValue0, JSC__JSValue JSValue1, JSC__JSGlobalObject* globalObject, bool replacePropsWithAsymmetricMatchers)
{
JSValue obj = JSValue::decode(JSValue0);

View File

@@ -5642,38 +5642,51 @@ pub const JSValue = enum(i64) {
}
/// Object.is()
///
/// This algorithm differs from the IsStrictlyEqual Algorithm by treating all NaN values as equivalent and by differentiating +0𝔽 from -0𝔽.
/// https://tc39.es/ecma262/#sec-samevalue
pub fn isSameValue(this: JSValue, other: JSValue, global: *JSGlobalObject) bool {
return @intFromEnum(this) == @intFromEnum(other) or cppFn("isSameValue", .{ this, other, global });
}
pub fn deepEquals(this: JSValue, other: JSValue, global: *JSGlobalObject) bool {
return cppFn("deepEquals", .{ this, other, global });
pub fn deepEquals(this: JSValue, other: JSValue, global: *JSGlobalObject) JSError!bool {
// JSC__JSValue__deepEquals
const result = cppFn("deepEquals", .{ this, other, global });
if (global.hasException()) return error.JSError;
return result;
}
/// same as `JSValue.deepEquals`, but with jest asymmetric matchers enabled
pub fn jestDeepEquals(this: JSValue, other: JSValue, global: *JSGlobalObject) bun.JSError!bool {
pub fn jestDeepEquals(this: JSValue, other: JSValue, global: *JSGlobalObject) JSError!bool {
const result = cppFn("jestDeepEquals", .{ this, other, global });
if (global.hasException()) return error.JSError;
return result;
}
pub fn strictDeepEquals(this: JSValue, other: JSValue, global: *JSGlobalObject) bool {
return cppFn("strictDeepEquals", .{ this, other, global });
pub fn strictDeepEquals(this: JSValue, other: JSValue, global: *JSGlobalObject) JSError!bool {
// JSC__JSValue__strictDeepEquals
const result = cppFn("strictDeepEquals", .{ this, other, global });
if (global.hasException()) return error.JSError;
return result;
}
/// same as `JSValue.strictDeepEquals`, but with jest asymmetric matchers enabled
pub fn jestStrictDeepEquals(this: JSValue, other: JSValue, global: *JSGlobalObject) bool {
return cppFn("jestStrictDeepEquals", .{ this, other, global });
pub fn jestStrictDeepEquals(this: JSValue, other: JSValue, global: *JSGlobalObject) JSError!bool {
// JSC__JSValue__jestStrictDeepEquals
const result = cppFn("jestStrictDeepEquals", .{ this, other, global });
if (global.hasException()) return error.JSError;
return result;
}
/// NOTE: can throw. Check for exceptions.
pub fn deepMatch(this: JSValue, subset: JSValue, global: *JSGlobalObject, replace_props_with_asymmetric_matchers: bool) bool {
// JSC__JSValue__deepMatch
return cppFn("deepMatch", .{ this, subset, global, replace_props_with_asymmetric_matchers });
}
/// same as `JSValue.deepMatch`, but with jest asymmetric matchers enabled
pub fn jestDeepMatch(this: JSValue, subset: JSValue, global: *JSGlobalObject, replace_props_with_asymmetric_matchers: bool) bool {
// JSC__JSValue__jestDeepMatch
return cppFn("jestDeepMatch", .{ this, subset, global, replace_props_with_asymmetric_matchers });
}

View File

@@ -385,6 +385,7 @@ extern "C" size_t Bun__encoding__byteLengthUTF16(const UChar* ptr, size_t len, E
extern "C" int64_t Bun__encoding__constructFromLatin1(void*, const unsigned char* ptr, size_t len, Encoding encoding);
extern "C" int64_t Bun__encoding__constructFromUTF16(void*, const UChar* ptr, size_t len, Encoding encoding);
/// @note throws a JS exception and returns false if a stack overflow occurs
template<bool isStrict, bool enableAsymmetricMatchers>
bool Bun__deepEquals(JSC::JSGlobalObject* globalObject, JSC::JSValue v1, JSC::JSValue v2, JSC::MarkedArgumentBuffer&, Vector<std::pair<JSC::JSValue, JSC::JSValue>, 16>& stack, JSC::ThrowScope* scope, bool addToStack);

View File

@@ -528,7 +528,7 @@ pub const Expect = struct {
}
const signature = comptime getSignature("toBe", "<green>expected<r>", false);
if (left.deepEquals(right, globalThis) or left.strictDeepEquals(right, globalThis)) {
if (try left.deepEquals(right, globalThis) or try left.strictDeepEquals(right, globalThis)) {
const fmt =
(if (!has_custom_label) "\n\n<d>If this test should pass, replace \"toBe\" with \"toEqual\" or \"toStrictEqual\"<r>" else "") ++
"\n\nExpected: <green>{any}<r>\n" ++
@@ -1629,7 +1629,7 @@ pub const Expect = struct {
const value: JSValue = try this.getValue(globalThis, thisValue, "toStrictEqual", "<green>expected<r>");
const not = this.flags.not;
var pass = value.jestStrictDeepEquals(expected, globalThis);
var pass = try value.jestStrictDeepEquals(expected, globalThis);
if (not) pass = !pass;
if (pass) return .undefined;
@@ -2351,7 +2351,7 @@ pub const Expect = struct {
if (Expect.isAsymmetricMatcher(expected_value)) {
const signature = comptime getSignature("toThrow", "<green>expected<r>", false);
const is_equal = result.jestStrictDeepEquals(expected_value, globalThis);
const is_equal = try result.jestStrictDeepEquals(expected_value, globalThis);
if (globalThis.hasException()) {
return .zero;

View File

@@ -377,6 +377,11 @@ class AssertionError extends Error {
this.operator = operator;
}
ErrorCaptureStackTrace(this, stackStartFn || stackStartFunction);
// JSC::Interpreter::getStackTrace() sometimes short-circuits without creating a .stack property.
// e.g.: https://github.com/oven-sh/WebKit/blob/e32c6356625cfacebff0c61d182f759abf6f508a/Source/JavaScriptCore/interpreter/Interpreter.cpp#L501
if ($isUndefinedOrNull(this.stack)) {
ErrorCaptureStackTrace(this, AssertionError);
}
// Create error message including the error code in the name.
this.stack; // eslint-disable-line no-unused-expressions
// Reset the name.

View File

@@ -129,6 +129,14 @@ export default {
}
},
),
SafeWeakMap: makeSafe(
WeakMap,
class SafeWeakMap extends WeakMap {
constructor(i) {
super(i);
}
},
),
SetPrototypeGetSize: getGetter(Set, "size"),
String,
TypedArrayPrototypeGetLength: getGetter(Uint8Array, "length"),

View File

@@ -189,9 +189,8 @@ assert.equal = function equal(actual: unknown, expected: unknown, message?: stri
if (arguments.length < 2) {
throw $ERR_MISSING_ARGS("actual", "expected");
}
// eslint-disable-next-line eqeqeq
// if (actual != expected && (!NumberIsNaN(actual) || !NumberIsNaN(expected))) {
if (actual != expected && !(isNaN(actual) && isNaN(expected))) {
if (actual != expected && (!NumberIsNaN(actual) || !NumberIsNaN(expected))) {
innerFail({
actual,
expected,
@@ -672,7 +671,7 @@ function getActual(fn) {
return NO_EXCEPTION_SENTINEL;
}
function checkIsPromise(obj) {
function checkIsPromise(obj): obj is Promise<unknown> {
// Accept native ES6 promises and promises that are implemented in a similar
// way. Do not accept thenables that use a function as `obj` and that have no
// `catch` handler.

View File

@@ -1571,7 +1571,7 @@ export class VerdaccioRegistry {
await rm(join(this.packagesPath, "private-pkg-dont-touch"), { force: true });
const packageDir = tmpdirSync();
const packageJson = join(packageDir, "package.json");
this.writeBunfig(packageDir, bunfigOpts);
await this.writeBunfig(packageDir, bunfigOpts);
this.users = {};
return { packageDir, packageJson };
}

View File

@@ -1,3 +1,5 @@
import vm from "node:vm";
describe.each([true, false])("Bun.deepEquals(a, b, strict: %p)", strict => {
const deepEquals = (a: unknown, b: unknown) => Bun.deepEquals(a, b, strict);
it.each([
@@ -55,4 +57,23 @@ describe.each([true, false])("Bun.deepEquals(a, b, strict: %p)", strict => {
expect(deepEquals(foo, bar)).toBe(false);
expect(deepEquals(foo, baz)).toBe(false);
});
describe("global object", () => {
let contexts: [vm.Context, vm.Context];
beforeEach(() => {
contexts = [vm.createContext(), vm.createContext()];
});
afterEach(() => {});
// TODO: re-enable when https://github.com/oven-sh/bun/issues/17080 is resolved
it.skip("main global object is not equal to vm global objects", () => {
const [ctx] = contexts;
expect(deepEquals(global, ctx)).toBe(false);
ctx.mainGlobal = global;
const areEqual = vm.runInContext("Bun.deepEquals(globalThis, mainGlobal)", ctx);
expect(areEqual).toBe(false);
});
});
});

View File

@@ -43,6 +43,47 @@ describe("expect()", () => {
}
});
};
describe("toBe()", () => {
let obj = {};
it.each([
[0, 0.0],
[+0, +0],
[0, +0],
[-0, -0],
[1, 1],
[1, 1.0],
[NaN, NaN],
[Infinity, Infinity],
[obj, obj],
[Symbol.for("a"), Symbol.for("a")],
])("expect(%p).toBe(%p) == true", (a, b) => {
expect(a).toBe(b);
expect(b).toBe(a);
});
it.each([
[0, false],
[0, ""],
[0, -0],
[+0, -0],
[1, 2],
[1, true],
[1, "1"],
[Infinity, -Infinity],
["foo", "Foo"],
["foo", "bar"],
["", " "],
["", " "],
["", true],
[{}, {}], //
[new Set(), new Set()], //
[function a() {}, function a() {}], //
[Symbol.for("a"), Symbol.for("b")],
[Symbol("a"), Symbol("a")],
])("expect(%p).toBe(%p) == false", (a, b) => {
expect(a).not.toBe(b);
expect(b).not.toBe(a);
});
});
test("rejects", async () => {
await expect(Promise.reject(4)).rejects.toBe(4);

View File

@@ -3,3 +3,7 @@ declare var describe: typeof import("bun:test").describe;
declare var test: typeof import("bun:test").test;
declare var expect: typeof import("bun:test").expect;
declare var it: typeof import("bun:test").it;
declare var beforeEach: typeof import("bun:test").beforeEach;
declare var afterEach: typeof import("bun:test").afterEach;
declare var beforeAll: typeof import("bun:test").beforeAll;
declare var afterAll: typeof import("bun:test").afterAll;

View File

@@ -0,0 +1,96 @@
import { describe, beforeEach, it, expect } from "bun:test";
import assert, { AssertionError } from "assert";
describe("assert(expr)", () => {
// https://github.com/oven-sh/bun/issues/941
it.each([true, 1, "foo"])(`assert(%p) does not throw`, expr => {
expect(() => assert(expr)).not.toThrow();
});
it.each([false, 0, "", null, undefined])(`assert(%p) throws`, expr => {
expect(() => assert(expr)).toThrow(AssertionError);
});
it("is an alias for assert.ok", () => {
expect(assert as Function).toBe(assert.ok);
});
});
describe("assert.equal(actual, expected)", () => {
it.each([
["foo", "foo"],
[1, 1],
[1, true],
[0, ""],
[0, false],
[Symbol.for("foo"), Symbol.for("foo")],
])(`%p == %p`, (actual, expected) => {
expect(() => assert.equal(actual, expected)).not.toThrow();
});
it.each([
//
["foo", "bar"],
[1, 0],
[true, false],
[{}, {}],
[Symbol("foo"), Symbol("foo")],
[new Error("oops"), new Error("oops")],
])("%p != %p", (actual, expected) => {
expect(() => assert.equal(actual, expected)).toThrow(AssertionError);
});
});
describe("assert.deepEqual(actual, expected)", () => {
describe("error instances", () => {
let e1: Error & Record<string, any>, e2: Error & Record<string, any>;
beforeEach(() => {
e1 = new Error("oops");
e2 = new Error("oops");
});
it("errors with the same message and constructor are equal", () => {
expect(() => assert.deepEqual(e1, e2)).not.toThrow();
});
it("errors with different messages are not equal", () => {
e2.message = "nope";
expect(() => assert.deepEqual(e1, e2)).toThrow(AssertionError);
});
it("errors with different constructors are not equal", () => {
e2 = new TypeError("oops");
expect(() => assert.deepEqual(e1, e2)).toThrow(AssertionError);
});
it("errors with different names are not equal", () => {
e2.name = "SpecialError";
expect(() => assert.deepEqual(e1, e2)).toThrow(AssertionError);
});
it("errors with different causes are not equal", () => {
e1.cause = { property: "value" };
expect(() => assert.deepEqual(e1, e2)).toThrow(AssertionError);
e2.cause = { property: "another value" };
expect(() => assert.deepEqual(e1, e2)).toThrow(AssertionError);
});
it("errors with the same cause are equal", () => {
e1.cause = { property: "value" };
e2.cause = { property: "value" };
expect(() => assert.deepEqual(e1, e2)).not.toThrow();
});
it("adding different arbitrary properties makes errors unequal", () => {
expect(() => assert.deepEqual(e1, e2)).not.toThrow();
e1.a = 1;
expect(() => assert.deepEqual(e1, e2)).toThrow(AssertionError);
e2.a = 1;
expect(() => assert.deepEqual(e1, e2)).not.toThrow();
e2.a = { foo: "bar" };
expect(() => assert.deepEqual(e1, e2)).toThrow(AssertionError);
e1.a = { foo: "baz" };
expect(() => assert.deepEqual(e1, e2)).toThrow(AssertionError);
});
});
});

View File

@@ -1,11 +0,0 @@
import assert from "assert";
import { expect, test } from "bun:test";
// https://github.com/oven-sh/bun/issues/941
test("assert as a function does not throw", () => assert(true));
test("assert as a function does throw", () => {
try {
assert(false);
expect.unreachable();
} catch (e) {}
});

View File

@@ -348,8 +348,9 @@ if (normalized.includes("node/test/parallel")) {
}
}
function describe(labelOrFn: string | Function, maybeFn?: Function) {
const [label, fn] = typeof labelOrFn == "function" ? [labelOrFn.name, labelOrFn] : [labelOrFn, maybeFn];
function describe(labelOrFn: string | Function, maybeFnOrOptions?: Function, maybeFn?: Function) {
const [label, fn] =
typeof labelOrFn == "function" ? [labelOrFn.name, labelOrFn] : [labelOrFn, maybeFn ?? maybeFnOrOptions];
if (typeof fn !== "function") throw new TypeError("Second argument to describe() must be a function.");
getContext().testStack.push(label);
@@ -372,7 +373,9 @@ if (normalized.includes("node/test/parallel")) {
return {
test,
it: test,
describe,
suite: describe,
};
}

View File

@@ -0,0 +1,74 @@
'use strict';
require('../../common');
const assert = require('assert');
const { describe, it } = require('node:test');
describe('assert.CallTracker.getCalls()', { concurrency: !process.env.TEST_PARALLEL }, () => {
const tracker = new assert.CallTracker();
it('should return empty list when no calls', () => {
const fn = tracker.calls();
assert.deepStrictEqual(tracker.getCalls(fn), []);
});
it('should return calls', () => {
const fn = tracker.calls(() => {});
const arg1 = {};
const arg2 = {};
fn(arg1, arg2);
fn.call(arg2, arg2);
assert.deepStrictEqual(tracker.getCalls(fn), [
{ arguments: [arg1, arg2], thisArg: undefined },
{ arguments: [arg2], thisArg: arg2 }]);
});
it('should throw when getting calls of a non-tracked function', () => {
[() => {}, 1, true, null, undefined, {}, []].forEach((fn) => {
assert.throws(() => tracker.getCalls(fn), { code: 'ERR_INVALID_ARG_VALUE' });
});
});
it('should return a frozen object', () => {
const fn = tracker.calls();
fn();
const calls = tracker.getCalls(fn);
// NOTE: v8 and jsc use different error messages for mutating frozen objects.
assert.throws(() => calls.push(1), /Attempted to assign to readonly property/);
assert.throws(() => Object.assign(calls[0], { foo: 'bar' }), /object that is not extensible/);
assert.throws(() => calls[0].arguments.push(1), /Attempted to assign to readonly property/);
});
});
describe('assert.CallTracker.reset()', () => {
const tracker = new assert.CallTracker();
it('should reset calls', () => {
const fn = tracker.calls();
fn();
fn();
fn();
assert.strictEqual(tracker.getCalls(fn).length, 3);
tracker.reset(fn);
assert.deepStrictEqual(tracker.getCalls(fn), []);
});
it('should reset all calls', () => {
const fn1 = tracker.calls();
const fn2 = tracker.calls();
fn1();
fn2();
assert.strictEqual(tracker.getCalls(fn1).length, 1);
assert.strictEqual(tracker.getCalls(fn2).length, 1);
tracker.reset();
assert.deepStrictEqual(tracker.getCalls(fn1), []);
assert.deepStrictEqual(tracker.getCalls(fn2), []);
});
it('should throw when resetting a non-tracked function', () => {
[() => {}, 1, true, null, {}, []].forEach((fn) => {
assert.throws(() => tracker.reset(fn), { code: 'ERR_INVALID_ARG_VALUE' });
});
});
});

View File

@@ -0,0 +1,74 @@
'use strict';
const { hasCrypto } = require('../../common');
const { test } = require('node:test');
const assert = require('assert');
// Turn off no-restricted-properties because we are testing deepEqual!
/* eslint-disable no-restricted-properties */
// Disable colored output to prevent color codes from breaking assertion
// message comparisons. This should only be an issue when process.stdout
// is a TTY.
if (process.stdout.isTTY)
process.env.NODE_DISABLE_COLORS = '1';
test('', { skip: !hasCrypto }, () => {
// See https://github.com/nodejs/node/issues/10258
{
const date = new Date('2016');
function FakeDate() {}
FakeDate.prototype = Date.prototype;
const fake = new FakeDate();
assert.notDeepEqual(date, fake);
assert.notDeepEqual(fake, date);
// For deepStrictEqual we check the runtime type,
// then reveal the fakeness of the fake date
assert.throws(
() => assert.deepStrictEqual(date, fake),
{
message: 'Expected values to be strictly deep-equal:\n' +
'+ actual - expected\n' +
'\n' +
'+ 2016-01-01T00:00:00.000Z\n' +
'- Date {}\n'
}
);
assert.throws(
() => assert.deepStrictEqual(fake, date),
{
message: 'Expected values to be strictly deep-equal:\n' +
'+ actual - expected\n' +
'\n' +
'+ Date {}\n' +
'- 2016-01-01T00:00:00.000Z\n'
}
);
}
{ // At the moment global has its own type tag
const fakeGlobal = {};
Object.setPrototypeOf(fakeGlobal, Object.getPrototypeOf(globalThis));
for (const prop of Object.keys(globalThis)) {
fakeGlobal[prop] = global[prop];
}
assert.notDeepEqual(fakeGlobal, globalThis);
// Message will be truncated anyway, don't validate
assert.throws(() => assert.deepStrictEqual(fakeGlobal, globalThis),
assert.AssertionError);
}
{ // At the moment process has its own type tag
const fakeProcess = {};
Object.setPrototypeOf(fakeProcess, Object.getPrototypeOf(process));
for (const prop of Object.keys(process)) {
fakeProcess[prop] = process[prop];
}
assert.notDeepEqual(fakeProcess, process);
// Message will be truncated anyway, don't validate
assert.throws(() => assert.deepStrictEqual(fakeProcess, process),
assert.AssertionError);
}
});
/* eslint-enable */

View File

@@ -0,0 +1,82 @@
'use strict';
require('../../common');
const assert = require('assert');
const { test } = require('node:test');
const defaultStartMessage = 'Expected values to be strictly deep-equal:\n' +
'+ actual - expected\n' +
'\n';
test('Handle error causes', () => {
assert.deepStrictEqual(new Error('a', { cause: new Error('x') }), new Error('a', { cause: new Error('x') }));
assert.deepStrictEqual(
new Error('a', { cause: new RangeError('x') }),
new Error('a', { cause: new RangeError('x') }),
);
assert.throws(() => {
assert.deepStrictEqual(new Error('a', { cause: new Error('x') }), new Error('a', { cause: new Error('y') }));
}, { message: defaultStartMessage + ' [Error: a] {\n' +
'+ [cause]: [Error: x]\n' +
'- [cause]: [Error: y]\n' +
' }\n' });
assert.throws(() => {
assert.deepStrictEqual(new Error('a', { cause: new Error('x') }), new Error('a', { cause: new TypeError('x') }));
}, { message: defaultStartMessage + ' [Error: a] {\n' +
'+ [cause]: [Error: x]\n' +
'- [cause]: [TypeError: x]\n' +
' }\n' });
assert.throws(() => {
assert.deepStrictEqual(new Error('a'), new Error('a', { cause: new Error('y') }));
}, { message: defaultStartMessage + '+ [Error: a]\n' +
'- [Error: a] {\n' +
'- [cause]: [Error: y]\n' +
'- }\n' });
assert.throws(() => {
assert.deepStrictEqual(new Error('a'), new Error('a', { cause: { prop: 'value' } }));
}, { message: defaultStartMessage + '+ [Error: a]\n' +
'- [Error: a] {\n' +
'- [cause]: {\n' +
'- prop: \'value\'\n' +
'- }\n' +
'- }\n' });
assert.notDeepStrictEqual(new Error('a', { cause: new Error('x') }), new Error('a', { cause: new Error('y') }));
assert.notDeepStrictEqual(
new Error('a', { cause: { prop: 'value' } }),
new Error('a', { cause: { prop: 'a different value' } })
);
});
test('Handle undefined causes', () => {
assert.deepStrictEqual(new Error('a', { cause: undefined }), new Error('a', { cause: undefined }));
assert.notDeepStrictEqual(new Error('a', { cause: 'undefined' }), new Error('a', { cause: undefined }));
assert.notDeepStrictEqual(new Error('a', { cause: undefined }), new Error('a'));
assert.notDeepStrictEqual(new Error('a'), new Error('a', { cause: undefined }));
assert.throws(() => {
assert.deepStrictEqual(new Error('a'), new Error('a', { cause: undefined }));
}, { message: defaultStartMessage +
'+ [Error: a]\n' +
'- [Error: a] {\n' +
'- [cause]: undefined\n' +
'- }\n' });
assert.throws(() => {
assert.deepStrictEqual(new Error('a', { cause: undefined }), new Error('a'));
}, { message: defaultStartMessage +
'+ [Error: a] {\n' +
'+ [cause]: undefined\n' +
'+ }\n' +
'- [Error: a]\n' });
assert.throws(() => {
assert.deepStrictEqual(new Error('a', { cause: undefined }), new Error('a', { cause: 'undefined' }));
}, { message: defaultStartMessage + ' [Error: a] {\n' +
'+ [cause]: undefined\n' +
'- [cause]: \'undefined\'\n' +
' }\n' });
});

View File

@@ -0,0 +1,31 @@
'use strict';
const { spawnPromisified } = require('../../common');
const assert = require('node:assert');
const { describe, it } = require('node:test');
const fileImports = {
commonjs: 'const assert = require("assert");',
module: 'import assert from "assert";',
};
describe('ensure the assert.ok throwing similar error messages for esm and cjs files', () => {
it('should return code 1 for each command', async () => {
const errorsMessages = [];
for (const [inputType, header] of Object.entries(fileImports)) {
const { stderr, code } = await spawnPromisified(process.execPath, [
'--input-type',
inputType,
'--eval',
`${header}\nassert.ok(0 === 2);\n`,
]);
assert.strictEqual(code, 1);
// For each error message, filter the lines which will starts with AssertionError
errorsMessages.push(
stderr.split('\n').find((s) => s.startsWith('AssertionError'))
);
}
assert.strictEqual(errorsMessages.length, 2);
assert.deepStrictEqual(errorsMessages[0], errorsMessages[1]);
});
});

View File

@@ -0,0 +1,50 @@
'use strict';
require('../../common');
const assert = require('assert');
const { test } = require('node:test');
test('No args', () => {
assert.throws(
() => { assert.fail(); },
{
code: 'ERR_ASSERTION',
name: 'AssertionError',
message: 'Failed',
operator: 'fail',
actual: undefined,
expected: undefined,
generatedMessage: true,
stack: /Failed/
}
);
});
test('One arg = message', () => {
assert.throws(() => {
assert.fail('custom message');
}, {
code: 'ERR_ASSERTION',
name: 'AssertionError',
message: 'custom message',
operator: 'fail',
actual: undefined,
expected: undefined,
generatedMessage: false
});
});
test('One arg = Error', () => {
assert.throws(() => {
assert.fail(new TypeError('custom message'));
}, {
name: 'TypeError',
message: 'custom message'
});
});
test('Object prototype get', () => {
Object.prototype.get = () => { throw new Error('failed'); };
assert.throws(() => assert.fail(''), { code: 'ERR_ASSERTION' });
delete Object.prototype.get;
});

View File

@@ -0,0 +1,104 @@
'use strict';
require('../../common');
const assert = require('assert');
const { test } = require('node:test');
test('Test that assert.ifError has the correct stack trace of both stacks', () => {
let err;
// Create some random error frames.
(function a() {
(function b() {
(function c() {
err = new Error('test error');
})();
})();
})();
const msg = err.message;
const stack = err.stack;
(function x() {
(function y() {
(function z() {
let threw = false;
try {
assert.ifError(err);
} catch (e) {
assert.strictEqual(e.message,
'ifError got unwanted exception: test error');
assert.strictEqual(err.message, msg);
assert.strictEqual(e.actual, err);
assert.strictEqual(e.actual.stack, stack);
assert.strictEqual(e.expected, null);
assert.strictEqual(e.operator, 'ifError');
threw = true;
}
assert(threw);
})();
})();
})();
});
test('General ifError tests', () => {
assert.throws(
() => {
const error = new Error();
error.stack = 'Error: containing weird stack\nYes!\nI am part of a stack.';
assert.ifError(error);
},
(error) => {
assert(!error.stack.includes('Yes!'));
return true;
}
);
assert.throws(
() => assert.ifError(new TypeError()),
{
message: 'ifError got unwanted exception: TypeError'
}
);
assert.throws(
() => assert.ifError({ stack: false }),
{
message: 'ifError got unwanted exception: { stack: false }'
}
);
assert.throws(
() => assert.ifError({ constructor: null, message: '' }),
{
message: 'ifError got unwanted exception: '
}
);
assert.throws(
() => { assert.ifError(false); },
{
message: 'ifError got unwanted exception: false'
}
);
});
test('Should not throw', () => {
assert.ifError(null);
assert.ifError();
assert.ifError(undefined);
});
test('https://github.com/nodejs/node-v0.x-archive/issues/2893', () => {
let threw = false;
try {
// eslint-disable-next-line no-restricted-syntax
assert.throws(() => {
assert.ifError(null);
});
} catch (e) {
threw = true;
assert.strictEqual(e.message, 'Missing expected exception.');
assert(!e.stack.includes('throws'), e);
}
assert(threw);
});

View File

@@ -0,0 +1,128 @@
'use strict';
const common = require('../common');
const assert = require('assert');
// This test ensures that assert.CallTracker.calls() works as intended.
const tracker = new assert.CallTracker();
function bar() {}
const err = {
code: 'ERR_INVALID_ARG_TYPE',
};
// Ensures calls() throws on invalid input types.
assert.throws(() => {
const callsbar = tracker.calls(bar, '1');
callsbar();
}, err
);
assert.throws(() => {
const callsbar = tracker.calls(bar, 0.1);
callsbar();
}, { code: 'ERR_OUT_OF_RANGE' }
);
assert.throws(() => {
const callsbar = tracker.calls(bar, true);
callsbar();
}, err
);
assert.throws(() => {
const callsbar = tracker.calls(bar, () => {});
callsbar();
}, err
);
assert.throws(() => {
const callsbar = tracker.calls(bar, null);
callsbar();
}, err
);
// Expects an error as tracker.calls() cannot be called within a process exit
// handler.
process.on('exit', () => {
assert.throws(() => tracker.calls(bar, 1), {
code: 'ERR_UNAVAILABLE_DURING_EXIT',
});
});
const msg = 'Expected to throw';
function func() {
throw new Error(msg);
}
const callsfunc = tracker.calls(func, 1);
// Expects callsfunc() to call func() which throws an error.
assert.throws(
() => callsfunc(),
{ message: msg }
);
{
const tracker = new assert.CallTracker();
const callsNoop = tracker.calls(1);
callsNoop();
tracker.verify();
}
{
const tracker = new assert.CallTracker();
const callsNoop = tracker.calls(undefined, 1);
callsNoop();
tracker.verify();
}
{
function func() {}
const tracker = new assert.CallTracker();
const callsfunc = tracker.calls(func);
assert.strictEqual(callsfunc.length, 0);
}
{
function func(a, b, c = 2) {}
const tracker = new assert.CallTracker();
const callsfunc = tracker.calls(func);
assert.strictEqual(callsfunc.length, 2);
}
{
function func(a, b, c = 2) {}
delete func.length;
const tracker = new assert.CallTracker();
const callsfunc = tracker.calls(func);
assert.strictEqual(Object.hasOwn(callsfunc, 'length'), false);
}
{
const ArrayIteratorPrototype = Reflect.getPrototypeOf(
Array.prototype.values()
);
const { next } = ArrayIteratorPrototype;
ArrayIteratorPrototype.next = common.mustNotCall(
'%ArrayIteratorPrototype%.next'
);
Object.prototype.get = common.mustNotCall('%Object.prototype%.get');
const customPropertyValue = Symbol();
function func(a, b, c = 2) {
return a + b + c;
}
func.customProperty = customPropertyValue;
Object.defineProperty(func, 'length', { get: common.mustNotCall() });
const tracker = new assert.CallTracker();
const callsfunc = tracker.calls(func);
assert.strictEqual(Object.hasOwn(callsfunc, 'length'), true);
assert.strictEqual(callsfunc.customProperty, customPropertyValue);
assert.strictEqual(callsfunc(1, 2, 3), 6);
ArrayIteratorPrototype.next = next;
delete Object.prototype.get;
}

View File

@@ -0,0 +1,26 @@
'use strict';
require('../common');
const assert = require('assert');
// This test ensures that the assert.CallTracker.report() works as intended.
const tracker = new assert.CallTracker();
function foo() {}
const callsfoo = tracker.calls(foo, 1);
// Ensures that foo was added to the callChecks array.
assert.strictEqual(tracker.report()[0].operator, 'foo');
callsfoo();
// Ensures that foo was removed from the callChecks array after being called the
// expected number of times.
assert.strictEqual(typeof tracker.report()[0], 'undefined');
callsfoo();
// Ensures that foo was added back to the callChecks array after being called
// more than the expected number of times.
assert.strictEqual(tracker.report()[0].operator, 'foo');

View File

@@ -0,0 +1,53 @@
'use strict';
require('../common');
const assert = require('assert');
// This test ensures that assert.CallTracker.verify() works as intended.
const tracker = new assert.CallTracker();
const generic_msg = 'Functions were not called the expected number of times';
function foo() {}
function bar() {}
const callsfoo = tracker.calls(foo, 1);
const callsbar = tracker.calls(bar, 1);
// Expects an error as callsfoo() and callsbar() were called less than one time.
assert.throws(
() => tracker.verify(),
{ message: generic_msg }
);
callsfoo();
// Expects an error as callsbar() was called less than one time.
assert.throws(
() => tracker.verify(),
{ message: 'Expected the bar function to be executed 1 time(s) but was executed 0 time(s).' }
);
callsbar();
// Will throw an error if callsfoo() and callsbar isn't called exactly once.
tracker.verify();
const callsfoobar = tracker.calls(foo, 1);
callsfoo();
// Expects an error as callsfoo() was called more than once and callsfoobar() was called less than one time.
assert.throws(
() => tracker.verify(),
{ message: generic_msg }
);
callsfoobar();
// Expects an error as callsfoo() was called more than once
assert.throws(
() => tracker.verify(),
{ message: 'Expected the foo function to be executed 1 time(s) but was executed 2 time(s).' }
);