Skip to content

Commit 10b2820

Browse files
author
Ralph Küpper
committed
test(runtime): #5763 regression — e2e guards for setPrototypeOf chain-end fixes
Four cases vs node --experimental-strip-types: acyclic chains of length 2/4, the __extends statics link (3-link function proto chain), the comment-json {__proto__: []} feature-test evaluation (throw-only guard; the instanceof gap is separate and pre-existing), and a genuine-cycle negative.
1 parent 897e9c0 commit 10b2820

1 file changed

Lines changed: 181 additions & 0 deletions

File tree

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
//! Regression tests for the setPrototypeOf cycle-walk chain-end bug
2+
//! (regression introduced by 1f4a2c5bd / #5763, bisected via the Next.js
3+
//! boot failure).
4+
//!
5+
//! The OrdinarySetPrototypeOf cycle check uses Floyd's tortoise-and-hare.
6+
//! Two chain-end bugs made `Object.setPrototypeOf(obj, proto)` throw
7+
//! "Cannot convert undefined or null to object" on perfectly ordinary,
8+
//! acyclic prototype chains:
9+
//!
10+
//! 1. Only the hare's SECOND step was null-guarded. On any acyclic chain
11+
//! longer than one link (every function/class proto:
12+
//! fn -> Function.prototype -> Object.prototype -> null) the hare reached
13+
//! null first and the next iteration called advance(null) ->
14+
//! js_object_get_prototype_of(null) -> TypeError. comment-json's
15+
//! `__extends` hit this on every transpiled subclass, killing Next.js
16+
//! server boot.
17+
//!
18+
//! 2. `js_object_get_prototype_of` returns undefined (not spec's
19+
//! object-or-null) for some exotic receivers; the walk fed that back into
20+
//! the next advance and threw. comment-json's `__extends` feature-test
21+
//! `{ __proto__: [] }` hit this at boot. Undefined can never be part of a
22+
//! genuine cycle, so it must end the walk like null.
23+
//!
24+
//! All expected outputs are byte-for-byte what `node
25+
//! --experimental-strip-types` prints.
26+
27+
use std::path::PathBuf;
28+
use std::process::Command;
29+
30+
fn perry_bin() -> PathBuf {
31+
PathBuf::from(env!("CARGO_BIN_EXE_perry"))
32+
}
33+
34+
fn compile_and_run(dir: &std::path::Path, source: &str) -> String {
35+
let entry = dir.join("main.ts");
36+
let output = dir.join("main_bin");
37+
std::fs::write(&entry, source).expect("write entry");
38+
39+
let compile = Command::new(perry_bin())
40+
.current_dir(dir)
41+
.arg("compile")
42+
.arg(&entry)
43+
.arg("-o")
44+
.arg(&output)
45+
.arg("--no-cache")
46+
.output()
47+
.expect("run perry compile");
48+
assert!(
49+
compile.status.success(),
50+
"perry compile failed\nstdout:\n{}\nstderr:\n{}",
51+
String::from_utf8_lossy(&compile.stdout),
52+
String::from_utf8_lossy(&compile.stderr)
53+
);
54+
55+
let run = Command::new(&output)
56+
.current_dir(dir)
57+
.output()
58+
.expect("run compiled binary");
59+
assert!(
60+
run.status.success(),
61+
"compiled binary failed (exit {:?})\nstdout:\n{}\nstderr:\n{}",
62+
run.status.code(),
63+
String::from_utf8_lossy(&run.stdout),
64+
String::from_utf8_lossy(&run.stderr)
65+
);
66+
String::from_utf8_lossy(&run.stdout).into_owned()
67+
}
68+
69+
/// Bug 1 guard: setPrototypeOf onto acyclic chains of length 2 and 4 must
70+
/// succeed and establish inheritance. Pre-fix the hare walked past the chain
71+
/// end and every one of these threw "Cannot convert undefined or null to
72+
/// object".
73+
#[test]
74+
fn set_prototype_of_acyclic_chain_longer_than_one_link() {
75+
let dir = tempfile::tempdir().expect("tempdir");
76+
let stdout = compile_and_run(
77+
dir.path(),
78+
r#"
79+
const base = {
80+
greet() {
81+
return "hi";
82+
},
83+
};
84+
85+
// Chain length 2: base -> Object.prototype -> null.
86+
const obj: any = {};
87+
Object.setPrototypeOf(obj, base);
88+
console.log(obj.greet());
89+
90+
// Chain length 4: deep2 -> deep1 -> base -> Object.prototype -> null.
91+
const deep1 = Object.create(base);
92+
const deep2 = Object.create(deep1);
93+
const target: any = {};
94+
Object.setPrototypeOf(target, deep2);
95+
console.log(target.greet());
96+
console.log("ok");
97+
"#,
98+
);
99+
assert_eq!(
100+
stdout, "hi\nhi\nok\n",
101+
"setPrototypeOf onto an acyclic chain of length >= 2 must not throw \
102+
(hare must freeze at chain end)"
103+
);
104+
}
105+
106+
/// The `__extends` statics link that killed Next.js boot: setPrototypeOf of
107+
/// one function onto another. The proto chain of a function is
108+
/// fn -> Function.prototype -> Object.prototype -> null (three links), so
109+
/// pre-fix this threw on every transpiled subclass.
110+
#[test]
111+
fn set_prototype_of_function_statics_link() {
112+
let dir = tempfile::tempdir().expect("tempdir");
113+
let stdout = compile_and_run(
114+
dir.path(),
115+
r#"
116+
function Base() {}
117+
function Derived() {}
118+
Object.setPrototypeOf(Derived, Base);
119+
console.log(Object.getPrototypeOf(Derived) === Base);
120+
console.log("statics linked");
121+
"#,
122+
);
123+
assert_eq!(
124+
stdout, "true\nstatics linked\n",
125+
"setPrototypeOf(fn, fn) — a 3-link proto chain — must not throw"
126+
);
127+
}
128+
129+
/// Bug 2 guard: comment-json's `__extends` feature test. The object-literal
130+
/// `{ __proto__: [] }` routes through the same cycle walk with an exotic
131+
/// receiver whose getPrototypeOf reports undefined mid-walk; undefined must
132+
/// terminate the walk like null instead of being fed back into the next
133+
/// advance. Pre-fix, merely EVALUATING the literal threw and killed boot.
134+
///
135+
/// NOTE: this deliberately does not assert `probe instanceof Array` — that
136+
/// is `true` in node but still `false` in perry (a separate, pre-existing
137+
/// instanceof/literal-`__proto__` gap unchanged by this fix). This test
138+
/// guards only the throw.
139+
#[test]
140+
fn object_literal_proto_array_feature_test() {
141+
let dir = tempfile::tempdir().expect("tempdir");
142+
let stdout = compile_and_run(
143+
dir.path(),
144+
r#"
145+
const probe: any = { __proto__: [] };
146+
console.log(typeof probe);
147+
console.log("feature test ok");
148+
"#,
149+
);
150+
assert_eq!(
151+
stdout, "object\nfeature test ok\n",
152+
"evaluating {{ __proto__: [] }} must not throw \
153+
(undefined getPrototypeOf ends the walk)"
154+
);
155+
}
156+
157+
/// The cycle check itself must keep working: setting a prototype that would
158+
/// form a cycle still throws a TypeError, and the object stays usable.
159+
#[test]
160+
fn cycle_detection_still_throws() {
161+
let dir = tempfile::tempdir().expect("tempdir");
162+
let stdout = compile_and_run(
163+
dir.path(),
164+
r#"
165+
const a: any = {};
166+
const b: any = {};
167+
Object.setPrototypeOf(b, a);
168+
let threw = false;
169+
try {
170+
Object.setPrototypeOf(a, b);
171+
} catch (e) {
172+
threw = true;
173+
}
174+
console.log("cycle detected:", threw);
175+
"#,
176+
);
177+
assert_eq!(
178+
stdout, "cycle detected: true\n",
179+
"a genuine prototype cycle must still be rejected with a TypeError"
180+
);
181+
}

0 commit comments

Comments
 (0)