Read inherited keys in assert.throws with a shared expected object (#2610)

This commit is contained in:
Chris Tate
2026-10-30 03:40:57 -05:00
committed by GitHub
3 changed files with 66 additions and 3 deletions
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/assert-throws-shared-expected.ts"
],
"diags": []
}
+14 -3
View File
@@ -2425,18 +2425,29 @@ void scr_assert_loose_deep_result(bool equal, bool negated, ScrStr *msg, bool ha
/* Node's expectsError over an error-INSTANCE expected (assert.throws/
* rejects second argument): every key of the expected dyn error (the
* %error marker skipped; name/message/code is the encoding's surface)
* must deep-strict-equal the caught value's — extra ACTUAL keys are fine
* (Node walks the expected's keys only). A mismatch fails through the
* must be in the caught value (own or inherited) and deep-strict-equal
* it — extra ACTUAL keys are fine (Node walks the expected's keys only). A mismatch fails through the
* deep-equal report (a mismatched key implies the full comparison
* differs, so the diff is honest); a non-object pair falls back to the
* full deep comparison outright. */
/* actual[key] where `key in actual` holds: an own property or one on the
* prototype chain (an error's name, and its message when it is not own,
* sit on the prototype). NULL when the key is absent. */
static ScrDyn *scr_assert_dyn_key(const ScrDyn *actual, const char *key, size_t key_len) {
for (const ScrDyn *o = actual; o != NULL && o->kind == SCR_DYN_OBJ; o = o->prototype) {
ScrDyn *v = scr_dyn_obj_get(o, key, key_len);
if (v != NULL) return v;
}
return NULL;
}
void scr_assert_expects_err_dyn(ScrDyn *actual, ScrDyn *expected, ScrStr *msg, bool has_msg) {
if (actual->kind == SCR_DYN_OBJ && expected->kind == SCR_DYN_OBJ) {
bool ok = true;
for (size_t i = 0; i < scr_dyn_obj_count(expected) && ok; i++) {
ScrDynProp e = scr_dyn_obj_prop_at(expected, i);
if (e.key_len == 6 && memcmp(e.key, "%error", 6) == 0) continue;
ScrDyn *av = scr_dyn_obj_get(actual, e.key, e.key_len);
ScrDyn *av = scr_assert_dyn_key(actual, e.key, e.key_len);
if (av == NULL || !scr_assert_dyn_deep_eq(av, e.value)) ok = false;
}
if (ok) return;
@@ -0,0 +1,46 @@
// assert.throws and assert.rejects with an expected object held in a
// variable compare each of its keys with the thrown error's, own or
// inherited: an error's name is on its prototype, so { name, message, code }
// matches errors the runtime throws as well as errors the program throws.
import assert from "node:assert";
const invalidUrl = { code: "ERR_INVALID_URL", name: "TypeError", message: "Invalid URL" };
const outOfRange = { code: "ERR_OUT_OF_RANGE", name: "RangeError" };
const thrownType = { name: "TypeError", message: "bad input" };
const wrongName = { name: "RangeError", message: "bad input" };
function check(label: string, fn: () => void): void {
try {
fn();
console.log(label, "passed");
} catch (error) {
const e = error as NodeJS.ErrnoException;
console.log(label, "failed", e.name, e.code);
}
}
check("url", () => assert.throws(() => new URL("not a url"), invalidUrl));
check("buffer", () => assert.throws(() => Buffer.alloc(-1), outOfRange));
check("thrown", () =>
assert.throws(() => {
throw new TypeError("bad input");
}, thrownType),
);
check("mismatch", () =>
assert.throws(() => {
throw new TypeError("bad input");
}, wrongName),
);
async function main(): Promise<void> {
await assert.rejects(Promise.reject(new TypeError("bad input")), thrownType);
console.log("rejects passed");
try {
await assert.rejects(Promise.reject(new TypeError("bad input")), wrongName);
console.log("rejects mismatch passed");
} catch (error) {
console.log("rejects mismatch failed", (error as NodeJS.ErrnoException).code);
}
}
void main();