From 146ec7791bb72b02b00c200218366b8e7ce54810 Mon Sep 17 00:00:00 2001 From: Don Isaac Date: Thu, 6 Feb 2025 14:29:59 -0800 Subject: [PATCH] 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> --- .vscode/launch.json | 13 ++ src/bun.js/bindings/BunObject.cpp | 4 +- src/bun.js/bindings/bindings.cpp | 211 ++++++++++++------ src/bun.js/bindings/bindings.zig | 27 ++- src/bun.js/bindings/headers-handwritten.h | 1 + src/bun.js/test/expect.zig | 6 +- src/js/internal/assert/assertion_error.ts | 5 + src/js/internal/primordials.js | 8 + src/js/node/assert.ts | 7 +- test/harness.ts | 2 +- test/js/bun/bun-object/deep-equals.spec.ts | 21 ++ test/js/bun/test/expect.test.js | 41 ++++ test/js/bun/test/jest.d.ts | 4 + test/js/node/assert/assert.spec.ts | 96 ++++++++ test/js/node/assert/assert.test.ts | 11 - test/js/node/harness.ts | 7 +- .../test-assert-calltracker-getCalls.js | 74 ++++++ .../needs-test/test-assert-checktag.js | 74 ++++++ .../needs-test/test-assert-deep-with-error.js | 82 +++++++ .../test-assert-esm-cjs-message-verify.js | 31 +++ .../parallel/needs-test/test-assert-fail.js | 50 +++++ .../needs-test/test-assert-if-error.js | 104 +++++++++ .../parallel/test-assert-calltracker-calls.js | 128 +++++++++++ .../test-assert-calltracker-report.js | 26 +++ .../test-assert-calltracker-verify.js | 53 +++++ 25 files changed, 993 insertions(+), 93 deletions(-) create mode 100644 test/js/node/assert/assert.spec.ts delete mode 100644 test/js/node/assert/assert.test.ts create mode 100644 test/js/node/test/parallel/needs-test/test-assert-calltracker-getCalls.js create mode 100644 test/js/node/test/parallel/needs-test/test-assert-checktag.js create mode 100644 test/js/node/test/parallel/needs-test/test-assert-deep-with-error.js create mode 100644 test/js/node/test/parallel/needs-test/test-assert-esm-cjs-message-verify.js create mode 100644 test/js/node/test/parallel/needs-test/test-assert-fail.js create mode 100644 test/js/node/test/parallel/needs-test/test-assert-if-error.js create mode 100644 test/js/node/test/parallel/test-assert-calltracker-calls.js create mode 100644 test/js/node/test/parallel/test-assert-calltracker-report.js create mode 100644 test/js/node/test/parallel/test-assert-calltracker-verify.js diff --git a/.vscode/launch.json b/.vscode/launch.json index b4149c3020b375..c5c33d5d986fa8 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -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", diff --git a/src/bun.js/bindings/BunObject.cpp b/src/bun.js/bindings/BunObject.cpp index 640e901799902f..53c5e121937d2a 100644 --- a/src/bun.js/bindings/BunObject.cpp +++ b/src/bun.js/bindings/BunObject.cpp @@ -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, 16> stack; MarkedArgumentBuffer gcBuffer; - if (arg3.isBoolean() && arg3.asBoolean()) { + if (strict.isBoolean() && strict.asBoolean()) { bool isEqual = Bun__deepEquals(globalObject, arg1, arg2, gcBuffer, stack, &scope, true); RETURN_IF_EXCEPTION(scope, {}); diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index c7ae9f663dd6b0..929e5666835950 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -672,6 +672,10 @@ template bool Bun__deepEquals(JSC__JSGlobalObject* globalObject, JSValue v1, JSValue v2, MarkedArgumentBuffer& gcBuffer, Vector, 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(v1); JSC::JSArray* array2 = JSC::jsCast(v2); @@ -1070,6 +1074,7 @@ std::optional specialObjectsDequal(JSC__JSGlobalObject* globalObject, Mark foundMatchingKey = true; break; } + RETURN_IF_EXCEPTION(*scope, false); } if (!foundMatchingKey) { @@ -1108,6 +1113,7 @@ std::optional specialObjectsDequal(JSC__JSGlobalObject* globalObject, Mark foundMatchingKey = true; break; } + RETURN_IF_EXCEPTION(*scope, false); } if (!foundMatchingKey) { @@ -1187,6 +1193,7 @@ std::optional specialObjectsDequal(JSC__JSGlobalObject* globalObject, Mark return false; } + // NOTE(@DonIsaac): could `left` ever _not_ be a JSC::ErrorInstance? if (JSC::ErrorInstance* left = jsDynamicCast(c1)) { JSC::ErrorInstance* right = jsDynamicCast(c2); @@ -1194,9 +1201,109 @@ std::optional 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(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(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 specialObjectsDequal(JSC__JSGlobalObject* globalObject, Mark if (!isTypedArrayType(static_cast(c2Type)) || c1Type != c2Type) { return false; } + auto info = c1->classInfo(); + auto info2 = c2->classInfo(); + if (!info || !info2) { + return false; + } JSC::JSArrayBufferView* left = jsCast(c1); JSC::JSArrayBufferView* right = jsCast(c2); @@ -1373,7 +1485,20 @@ std::optional 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(c1); + auto* g2 = jsDynamicCast(c2); + return g1->m_globalThis == g2->m_globalThis; + } + case GlobalProxyType: { + if (c1Type != c2Type) return false; + auto* gp1 = jsDynamicCast(c1); + auto* gp2 = jsDynamicCast(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 +inline bool deepEqualsWrapperImpl(JSC__JSValue a, JSC__JSValue b, JSC__JSGlobalObject* global) +{ + auto& vm = global->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + Vector, 16> stack; + MarkedArgumentBuffer args; + return Bun__deepEquals(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( - // 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(arg1-> map->get(arg1, val))) { - // auto moduleIdent = JSC::Identifier::fromString(arg1->vm(), "module"); - // if (JSC::JSModuleRecord *record = JSC::jsDynamicCast( - // arg1->vm(), registryEntry->getDirect(arg1->vm(), moduleIdent))) { - // registryEntry->putDirect(arg1->vm(), moduleIdent, JSC::jsUndefined()); - // JSC::JSModuleRecord::destroy(static_cast(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, 16> stack; - MarkedArgumentBuffer args; - return Bun__deepEquals(globalObject, v1, v2, args, stack, &scope, true); + return deepEqualsWrapperImpl(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, 16> stack; - MarkedArgumentBuffer args; - return Bun__deepEquals(globalObject, v1, v2, args, stack, &scope, true); + return deepEqualsWrapperImpl(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, 16> stack; - MarkedArgumentBuffer args; - return Bun__deepEquals(globalObject, v1, v2, args, stack, &scope, true); + return deepEqualsWrapperImpl(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, 16> stack; - MarkedArgumentBuffer args; - - return Bun__deepEquals(globalObject, v1, v2, args, stack, &scope, true); + return deepEqualsWrapperImpl(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); diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index f0a0fa6cc4f1d0..8509951cad4465 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -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 }); } diff --git a/src/bun.js/bindings/headers-handwritten.h b/src/bun.js/bindings/headers-handwritten.h index 9a81eda9783d81..e6bb4e2de7bfc0 100644 --- a/src/bun.js/bindings/headers-handwritten.h +++ b/src/bun.js/bindings/headers-handwritten.h @@ -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 Bun__deepEquals(JSC::JSGlobalObject* globalObject, JSC::JSValue v1, JSC::JSValue v2, JSC::MarkedArgumentBuffer&, Vector, 16>& stack, JSC::ThrowScope* scope, bool addToStack); diff --git a/src/bun.js/test/expect.zig b/src/bun.js/test/expect.zig index f55568b2e3e27f..66ed282c532714 100644 --- a/src/bun.js/test/expect.zig +++ b/src/bun.js/test/expect.zig @@ -528,7 +528,7 @@ pub const Expect = struct { } const signature = comptime getSignature("toBe", "expected", 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\nIf this test should pass, replace \"toBe\" with \"toEqual\" or \"toStrictEqual\"" else "") ++ "\n\nExpected: {any}\n" ++ @@ -1629,7 +1629,7 @@ pub const Expect = struct { const value: JSValue = try this.getValue(globalThis, thisValue, "toStrictEqual", "expected"); 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", "expected", false); - const is_equal = result.jestStrictDeepEquals(expected_value, globalThis); + const is_equal = try result.jestStrictDeepEquals(expected_value, globalThis); if (globalThis.hasException()) { return .zero; diff --git a/src/js/internal/assert/assertion_error.ts b/src/js/internal/assert/assertion_error.ts index 780e3489152f57..b7722209613554 100644 --- a/src/js/internal/assert/assertion_error.ts +++ b/src/js/internal/assert/assertion_error.ts @@ -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. diff --git a/src/js/internal/primordials.js b/src/js/internal/primordials.js index 046f44fa659e48..d478237597af6b 100644 --- a/src/js/internal/primordials.js +++ b/src/js/internal/primordials.js @@ -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"), diff --git a/src/js/node/assert.ts b/src/js/node/assert.ts index 4c12a84626e6ac..b32ee2ba246a7a 100644 --- a/src/js/node/assert.ts +++ b/src/js/node/assert.ts @@ -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 { // 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. diff --git a/test/harness.ts b/test/harness.ts index 5619b1095caa9e..98206adb79984e 100644 --- a/test/harness.ts +++ b/test/harness.ts @@ -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 }; } diff --git a/test/js/bun/bun-object/deep-equals.spec.ts b/test/js/bun/bun-object/deep-equals.spec.ts index 323280114b151f..6d69941761922d 100644 --- a/test/js/bun/bun-object/deep-equals.spec.ts +++ b/test/js/bun/bun-object/deep-equals.spec.ts @@ -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); + }); + }); }); diff --git a/test/js/bun/test/expect.test.js b/test/js/bun/test/expect.test.js index cc3ff40dce80a1..d9e272937c3228 100644 --- a/test/js/bun/test/expect.test.js +++ b/test/js/bun/test/expect.test.js @@ -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); diff --git a/test/js/bun/test/jest.d.ts b/test/js/bun/test/jest.d.ts index fccb87b409e6ea..2d21e87e051282 100644 --- a/test/js/bun/test/jest.d.ts +++ b/test/js/bun/test/jest.d.ts @@ -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; diff --git a/test/js/node/assert/assert.spec.ts b/test/js/node/assert/assert.spec.ts new file mode 100644 index 00000000000000..d38946f805fb6e --- /dev/null +++ b/test/js/node/assert/assert.spec.ts @@ -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, e2: Error & Record; + + 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); + }); + }); +}); diff --git a/test/js/node/assert/assert.test.ts b/test/js/node/assert/assert.test.ts deleted file mode 100644 index 2ba97531ab0c8e..00000000000000 --- a/test/js/node/assert/assert.test.ts +++ /dev/null @@ -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) {} -}); diff --git a/test/js/node/harness.ts b/test/js/node/harness.ts index 52bfb3c33a8039..88b4378bf129d1 100644 --- a/test/js/node/harness.ts +++ b/test/js/node/harness.ts @@ -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, }; } diff --git a/test/js/node/test/parallel/needs-test/test-assert-calltracker-getCalls.js b/test/js/node/test/parallel/needs-test/test-assert-calltracker-getCalls.js new file mode 100644 index 00000000000000..5332be4abfeee0 --- /dev/null +++ b/test/js/node/test/parallel/needs-test/test-assert-calltracker-getCalls.js @@ -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' }); + }); + }); +}); diff --git a/test/js/node/test/parallel/needs-test/test-assert-checktag.js b/test/js/node/test/parallel/needs-test/test-assert-checktag.js new file mode 100644 index 00000000000000..04f78f737a2442 --- /dev/null +++ b/test/js/node/test/parallel/needs-test/test-assert-checktag.js @@ -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 */ diff --git a/test/js/node/test/parallel/needs-test/test-assert-deep-with-error.js b/test/js/node/test/parallel/needs-test/test-assert-deep-with-error.js new file mode 100644 index 00000000000000..125d16cc93302d --- /dev/null +++ b/test/js/node/test/parallel/needs-test/test-assert-deep-with-error.js @@ -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' }); +}); diff --git a/test/js/node/test/parallel/needs-test/test-assert-esm-cjs-message-verify.js b/test/js/node/test/parallel/needs-test/test-assert-esm-cjs-message-verify.js new file mode 100644 index 00000000000000..fa5cdc03409755 --- /dev/null +++ b/test/js/node/test/parallel/needs-test/test-assert-esm-cjs-message-verify.js @@ -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]); + }); +}); diff --git a/test/js/node/test/parallel/needs-test/test-assert-fail.js b/test/js/node/test/parallel/needs-test/test-assert-fail.js new file mode 100644 index 00000000000000..1d9fbc5784eb4c --- /dev/null +++ b/test/js/node/test/parallel/needs-test/test-assert-fail.js @@ -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; +}); diff --git a/test/js/node/test/parallel/needs-test/test-assert-if-error.js b/test/js/node/test/parallel/needs-test/test-assert-if-error.js new file mode 100644 index 00000000000000..32d7456bae8ab5 --- /dev/null +++ b/test/js/node/test/parallel/needs-test/test-assert-if-error.js @@ -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); +}); diff --git a/test/js/node/test/parallel/test-assert-calltracker-calls.js b/test/js/node/test/parallel/test-assert-calltracker-calls.js new file mode 100644 index 00000000000000..7b73f3fefaf6ab --- /dev/null +++ b/test/js/node/test/parallel/test-assert-calltracker-calls.js @@ -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; +} diff --git a/test/js/node/test/parallel/test-assert-calltracker-report.js b/test/js/node/test/parallel/test-assert-calltracker-report.js new file mode 100644 index 00000000000000..87ef0bff17fc0b --- /dev/null +++ b/test/js/node/test/parallel/test-assert-calltracker-report.js @@ -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'); diff --git a/test/js/node/test/parallel/test-assert-calltracker-verify.js b/test/js/node/test/parallel/test-assert-calltracker-verify.js new file mode 100644 index 00000000000000..118f04f7352780 --- /dev/null +++ b/test/js/node/test/parallel/test-assert-calltracker-verify.js @@ -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).' } +);