Skip to content

Commit cbbbf44

Browse files
committed
test(oracle): consolidate object graph helpers
1 parent fd0a68c commit cbbbf44

11 files changed

Lines changed: 254 additions & 793 deletions

scripts/check-oracle-helper-duplication.mjs

Lines changed: 106 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const scriptDirectory = dirname(fileURLToPath(import.meta.url));
99
const root = resolve(scriptDirectory, "..");
1010
const supportDirectory = resolve(root, "tests/support");
1111
const providerFiles = [
12+
resolve(supportDirectory, "object_graph_observation.rs"),
1213
resolve(supportDirectory, "runtime_completion_oracle.rs"),
1314
resolve(supportDirectory, "runtime_observation.rs"),
1415
];
@@ -155,21 +156,76 @@ const tombstoneSources = [
155156
}
156157
Err(error) => panic!("Rust engine failure for {description}: {error}"),
157158
}
159+
}`,
160+
},
161+
{
162+
label: "Object graph descriptor-bit formatter without a trailing argument comma",
163+
origin: "tests/oracle/object/oracle_object_descriptors.rs",
164+
replacement: "object_graph_observation::data_bits",
165+
source: String.raw`fn data_bits(writable: bool, enumerable: bool, configurable: bool) -> String {
166+
format!(
167+
"D:{}{}{}",
168+
Number(writable),
169+
Number(enumerable),
170+
Number(configurable)
171+
)
172+
}`,
173+
},
174+
{
175+
label: "Object graph integer-property reader",
176+
origin: "tests/oracle/object/oracle_object_assign.rs",
177+
replacement: "object_graph_observation::int_property",
178+
source: String.raw`fn int_property(runtime: &Runtime, context: &mut Context, object: &ObjectRef, name: &str) -> i32 {
179+
let Value::Int(value) = context
180+
.get_property(object, &runtime.intern_property_key(name).unwrap())
181+
.unwrap()
182+
else {
183+
panic!("{name} was not an Int property");
184+
};
185+
value
158186
}`,
159187
},
160188
];
161189

162-
// All exact copies of the shared providers were retired with this gate. Keep
163-
// the mechanism explicit so a narrowly justified exception remains a reviewed
164-
// path-and-fingerprint decision instead of a global clone-count waiver.
165-
const providerAllowlist = new Set();
190+
// The Object graph cleanup was deliberately bounded to eight oracle modules.
191+
// Adjacent pre-existing copies stay explicit as path-and-fingerprint
192+
// exceptions, so the gate still rejects any new copy and detects stale entries.
193+
const helperAllowlist = new Set([
194+
"tests/oracle/collections/oracle_map.rs\u00005d9661cb5d6d4945fe9108896091b79c50bcf99ec24a535d745088a4bd150a86",
195+
"tests/oracle/collections/oracle_set.rs\u00005d9661cb5d6d4945fe9108896091b79c50bcf99ec24a535d745088a4bd150a86",
196+
"tests/oracle/collections/oracle_weak_collections.rs\u00005d9661cb5d6d4945fe9108896091b79c50bcf99ec24a535d745088a4bd150a86",
197+
"tests/oracle/errors/oracle_errors.rs\u0000decb1b0e66595c263bc68421bb9797396e381256f18cfb9bf9892c576822ad4a",
198+
"tests/oracle/function_semantics/oracle_function_prototype_prefix.rs\u0000decb1b0e66595c263bc68421bb9797396e381256f18cfb9bf9892c576822ad4a",
199+
"tests/oracle/global/oracle_global_numeric_predicates.rs\u00005d9661cb5d6d4945fe9108896091b79c50bcf99ec24a535d745088a4bd150a86",
200+
"tests/oracle/global/oracle_global_uri_codecs.rs\u00005d9661cb5d6d4945fe9108896091b79c50bcf99ec24a535d745088a4bd150a86",
201+
"tests/oracle/number/oracle_number_constructor_conversion.rs\u00005d9661cb5d6d4945fe9108896091b79c50bcf99ec24a535d745088a4bd150a86",
202+
"tests/oracle/object/oracle_object_group_by.rs\u00002ecb9b416736102f1170cfa102c85182007491633a58badf2c1677354d8fa244",
203+
"tests/oracle/object/oracle_object_group_by.rs\u00003da3ed15c59afc7ed8c28f61354fa7f1ad406d948f45faa8d6989b5805fbeec7",
204+
"tests/oracle/object/oracle_object_group_by.rs\u00005d9661cb5d6d4945fe9108896091b79c50bcf99ec24a535d745088a4bd150a86",
205+
"tests/oracle/object/oracle_object_group_by.rs\u0000174e66d9def3034c785ba13ce8818d1a4b6132fa1aa62f3956f8a870a18c6690",
206+
"tests/oracle/object/oracle_object_group_by.rs\u0000ae3af224efe82f1d4d7b2adf0ac80991c51681123f90bc2ed647e15b29c3832d",
207+
"tests/oracle/object/oracle_object_group_by.rs\u0000decb1b0e66595c263bc68421bb9797396e381256f18cfb9bf9892c576822ad4a",
208+
"tests/oracle/object/oracle_object_intrinsic.rs\u00002ecb9b416736102f1170cfa102c85182007491633a58badf2c1677354d8fa244",
209+
"tests/oracle/object/oracle_object_intrinsic.rs\u00005d9661cb5d6d4945fe9108896091b79c50bcf99ec24a535d745088a4bd150a86",
210+
"tests/oracle/object/oracle_object_intrinsic.rs\u0000decb1b0e66595c263bc68421bb9797396e381256f18cfb9bf9892c576822ad4a",
211+
"tests/oracle/string/oracle_string_index_search.rs\u0000ae3af224efe82f1d4d7b2adf0ac80991c51681123f90bc2ed647e15b29c3832d",
212+
"tests/oracle/string/oracle_string_index_search.rs\u0000174e66d9def3034c785ba13ce8818d1a4b6132fa1aa62f3956f8a870a18c6690",
213+
"tests/oracle/string/oracle_string_index_search.rs\u0000c09ce90549ab8e65e6a88c4f7261c5ec6e7aa8a11bee43106b827b8823bc1377",
214+
"tests/oracle/string/oracle_string_index_search.rs\u0000decb1b0e66595c263bc68421bb9797396e381256f18cfb9bf9892c576822ad4a",
215+
"tests/oracle/string/oracle_string_intrinsic.rs\u00003da3ed15c59afc7ed8c28f61354fa7f1ad406d948f45faa8d6989b5805fbeec7",
216+
"tests/oracle/string/oracle_string_intrinsic.rs\u0000c09ce90549ab8e65e6a88c4f7261c5ec6e7aa8a11bee43106b827b8823bc1377",
217+
"tests/oracle/string/oracle_string_intrinsic.rs\u0000decb1b0e66595c263bc68421bb9797396e381256f18cfb9bf9892c576822ad4a",
218+
"tests/oracle/string/oracle_string_split.rs\u0000174e66d9def3034c785ba13ce8818d1a4b6132fa1aa62f3956f8a870a18c6690",
219+
"tests/oracle_math_intrinsic.rs\u00002ecb9b416736102f1170cfa102c85182007491633a58badf2c1677354d8fa244",
220+
]);
166221

167222
runCanaries();
168223

169224
const providers = loadProviders();
170225
const tombstones = loadTombstones();
171226
const consumerFiles = collectConsumerFiles();
172227
const functions = consumerFiles.flatMap((path) => scanFile(path));
228+
runAllowlistMultiplicityCanary(functions, providers, tombstones);
173229
const failures = checkFunctions(functions, providers, tombstones);
174230

175231
if (options.report) {
@@ -283,15 +339,16 @@ function scanFile(path) {
283339

284340
function checkFunctions(functions, providers, tombstones) {
285341
const failures = [];
286-
const usedAllowlist = new Set();
342+
const allowlistUseCounts = new Map();
287343
for (const helper of functions) {
288344
const displayPath = display(helper.path);
345+
const allowlistKey = `${displayPath}\0${helper.fingerprint}`;
346+
const allowlisted = helperAllowlist.has(allowlistKey);
347+
let protectedHelper = false;
289348
const providersForFingerprint = providers.get(helper.fingerprint);
290349
if (providersForFingerprint !== undefined) {
291-
const allowlistKey = `${displayPath}\0${helper.fingerprint}`;
292-
if (providerAllowlist.has(allowlistKey)) {
293-
usedAllowlist.add(allowlistKey);
294-
} else {
350+
protectedHelper = true;
351+
if (!allowlisted) {
295352
const providerNames = providersForFingerprint
296353
.map((provider) => provider.name)
297354
.sort()
@@ -305,22 +362,56 @@ function checkFunctions(functions, providers, tombstones) {
305362
}
306363
const tombstone = tombstones.get(helper.fingerprint);
307364
if (tombstone !== undefined) {
308-
failures.push(
309-
`${displayPath}:${helper.line} restores retired ${tombstone.label} ` +
310-
`(from ${tombstone.origin}); use ${tombstone.replacement}`,
365+
protectedHelper = true;
366+
if (!allowlisted) {
367+
failures.push(
368+
`${displayPath}:${helper.line} restores retired ${tombstone.label} ` +
369+
`(from ${tombstone.origin}); use ${tombstone.replacement}`,
370+
);
371+
}
372+
}
373+
if (allowlisted && protectedHelper) {
374+
allowlistUseCounts.set(
375+
allowlistKey,
376+
(allowlistUseCounts.get(allowlistKey) ?? 0) + 1,
311377
);
312378
}
313379
}
314-
for (const allowlistKey of providerAllowlist) {
315-
if (!usedAllowlist.has(allowlistKey)) {
380+
for (const allowlistKey of helperAllowlist) {
381+
const useCount = allowlistUseCounts.get(allowlistKey) ?? 0;
382+
if (useCount === 0) {
383+
failures.push(
384+
`stale shared-helper allowlist entry: ${allowlistKey.replace("\0", " ")}`,
385+
);
386+
} else if (useCount !== 1) {
316387
failures.push(
317-
`stale shared-provider allowlist entry: ${allowlistKey.replace("\0", " ")}`,
388+
`shared-helper allowlist multiplicity drift: ` +
389+
`${allowlistKey.replace("\0", " ")} expected=1 actual=${useCount}`,
318390
);
319391
}
320392
}
321393
return failures.sort();
322394
}
323395

396+
function runAllowlistMultiplicityCanary(functions, providers, tombstones) {
397+
const [allowlistKey] = helperAllowlist;
398+
const helper = functions.find(
399+
(candidate) =>
400+
`${display(candidate.path)}\0${candidate.fingerprint}` === allowlistKey,
401+
);
402+
if (helper === undefined) {
403+
fail("allowlist multiplicity canary could not find its protected helper");
404+
}
405+
const failures = checkFunctions([helper, helper], providers, tombstones);
406+
if (
407+
!failures.some((failure) =>
408+
failure.startsWith("shared-helper allowlist multiplicity drift:"),
409+
)
410+
) {
411+
fail("allowlist multiplicity canary accepted a second protected helper copy");
412+
}
413+
}
414+
324415
function printCensus(functions, consumerFiles, providers, tombstones) {
325416
const groups = new Map();
326417
for (const helper of functions) {

tests/oracle.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// Keep this registry sorted by wrapper path; the test262 aliases intentionally
22
// differ from their filenames so one filter selects the feature-gated host tests.
3+
#[path = "support/object_graph_observation.rs"]
4+
mod object_graph_observation;
35
#[path = "support/quickjs_argv_completion_oracle.rs"]
46
mod quickjs_argv_completion_oracle;
57
#[path = "support/quickjs_array_completion_oracle.rs"]

tests/oracle/object/oracle_object_assign.rs

Lines changed: 5 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
use crate::object_graph_observation::{
2+
data_bits, data_descriptor, eval_object, global_callable, int_property, intrinsic_prototype,
3+
oracle_lines, own_key_names,
4+
};
15
use crate::quickjs_oracle::observe_completion as observe_oracle;
26
use crate::runtime_completion_oracle::compare_eval_completion_cases as compare_cases;
37
use crate::runtime_observation::{
@@ -6,10 +10,7 @@ use crate::runtime_observation::{
610
use crate::runtime_oracle::value_type;
711
use std::ffi::OsStr;
812

9-
use quickjs_oxide::{
10-
CallableRef, CompleteOrdinaryPropertyDescriptor, Context, JsString, ObjectRef, PropertyKey,
11-
Runtime, RuntimeError, Value,
12-
};
13+
use quickjs_oxide::{JsString, Runtime, RuntimeError, Value};
1314

1415
// Pins QuickJS 2026-06-04 `js_object_assign` and `JS_CopyDataProperties`.
1516
// Ordinary sources use QuickJS's enumerable-at-snapshot optimization; Proxy
@@ -707,98 +708,3 @@ fn rust_graph_observations() -> Vec<String> {
707708
fn oracle_graph_observations(oracle: &OsStr) -> Vec<String> {
708709
oracle_lines(oracle, GRAPH_ORACLE, "Object.assign graph")
709710
}
710-
711-
fn oracle_lines(oracle: &OsStr, source: &str, description: &str) -> Vec<String> {
712-
super::quickjs_oracle::eval_std_lines(oracle, source, description)
713-
}
714-
715-
fn global_callable(runtime: &Runtime, context: &mut Context, name: &str) -> CallableRef {
716-
let global = context.global_object().unwrap();
717-
property_callable(runtime, context, &global, name)
718-
}
719-
720-
fn intrinsic_prototype(
721-
runtime: &Runtime,
722-
context: &mut Context,
723-
constructor_name: &str,
724-
) -> ObjectRef {
725-
let constructor = global_callable(runtime, context, constructor_name);
726-
let Value::Object(prototype) = context
727-
.get_property(
728-
constructor.as_object(),
729-
&runtime.intern_property_key("prototype").unwrap(),
730-
)
731-
.unwrap()
732-
else {
733-
panic!("{constructor_name}.prototype was not an object");
734-
};
735-
prototype
736-
}
737-
738-
fn eval_object(context: &mut Context, source: &str) -> ObjectRef {
739-
let Value::Object(object) = context.eval(source).unwrap() else {
740-
panic!("{source:?} did not evaluate to an object");
741-
};
742-
object
743-
}
744-
745-
fn own_key_names(runtime: &Runtime, object: &ObjectRef) -> Vec<String> {
746-
runtime
747-
.own_property_keys(object)
748-
.unwrap()
749-
.into_iter()
750-
.map(|key| {
751-
runtime
752-
.property_key_to_js_string(&key)
753-
.unwrap()
754-
.to_utf8_lossy()
755-
})
756-
.collect()
757-
}
758-
759-
fn data_descriptor(
760-
runtime: &Runtime,
761-
object: &ObjectRef,
762-
key: &PropertyKey,
763-
) -> (Value, bool, bool, bool) {
764-
let CompleteOrdinaryPropertyDescriptor::Data {
765-
value,
766-
writable,
767-
enumerable,
768-
configurable,
769-
} = runtime
770-
.get_own_property(object, key)
771-
.unwrap()
772-
.expect("missing data descriptor")
773-
else {
774-
panic!("property was not a data descriptor");
775-
};
776-
(value, writable, enumerable, configurable)
777-
}
778-
779-
fn int_property(runtime: &Runtime, context: &mut Context, object: &ObjectRef, name: &str) -> i32 {
780-
let Value::Int(value) = context
781-
.get_property(object, &runtime.intern_property_key(name).unwrap())
782-
.unwrap()
783-
else {
784-
panic!("{name} was not an Int property");
785-
};
786-
value
787-
}
788-
789-
fn data_bits(writable: bool, enumerable: bool, configurable: bool) -> String {
790-
format!(
791-
"D:{}{}{}",
792-
Number(writable),
793-
Number(enumerable),
794-
Number(configurable),
795-
)
796-
}
797-
798-
struct Number(bool);
799-
800-
impl std::fmt::Display for Number {
801-
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
802-
formatter.write_str(if self.0 { "1" } else { "0" })
803-
}
804-
}

0 commit comments

Comments
 (0)