Skip to content

Commit 8905178

Browse files
test: add test for assert OOM on large object diff
1 parent 5bb2cdc commit 8905178

File tree

1 file changed

+63
-156
lines changed

1 file changed

+63
-156
lines changed
Lines changed: 63 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -1,184 +1,91 @@
11
// Flags: --max-old-space-size=512
22
'use strict';
33

4-
// Test that assert.strictEqual does not OOM when comparing objects
5-
// that produce large util.inspect output.
6-
//
7-
// This is a regression test for an issue where objects with many unique
8-
// paths converging on shared objects can cause exponential growth in
9-
// util.inspect output, leading to OOM during assertion error generation.
10-
//
11-
// The fix adds a 2MB limit to inspect output in assertion_error.js
4+
// Regression test: assert.strictEqual should not OOM when comparing objects
5+
// with many converging paths to shared objects. Such objects cause exponential
6+
// growth in util.inspect output, which previously led to OOM during error
7+
// message generation.
128

139
require('../common');
1410
const assert = require('assert');
1511

16-
// Create an object graph where many unique paths converge on shared objects.
17-
// This delays circular reference detection and creates exponential growth
18-
// in util.inspect output at high depths.
12+
// Test: should throw AssertionError, not OOM
13+
{
14+
const { doc1, doc2 } = createTestObjects();
1915

20-
function createBase() {
21-
const base = {
22-
id: 'base',
23-
models: {},
24-
schemas: {},
25-
types: {},
26-
};
16+
assert.throws(
17+
() => assert.strictEqual(doc1, doc2),
18+
(err) => {
19+
assert.ok(err instanceof assert.AssertionError);
20+
// Message should be bounded (fix truncates inspect output at 2MB)
21+
assert.ok(err.message.length < 5 * 1024 * 1024);
22+
return true;
23+
}
24+
);
25+
}
2726

28-
for (let i = 0; i < 5; i++) {
29-
base.types[`type_${i}`] = {
30-
name: `type_${i}`,
31-
base,
32-
caster: { base, name: `type_${i}_caster` },
33-
options: {
34-
base,
35-
validators: [
36-
{ base, name: 'v1' },
37-
{ base, name: 'v2' },
38-
{ base, name: 'v3' },
39-
],
40-
},
41-
};
42-
}
27+
// Creates objects where many paths converge on shared objects, causing
28+
// exponential growth in util.inspect output at high depths.
29+
function createTestObjects() {
30+
const base = createBase();
4331

44-
return base;
45-
}
32+
const s1 = createSchema(base, 's1');
33+
const s2 = createSchema(base, 's2');
34+
base.schemas.s1 = s1;
35+
base.schemas.s2 = s2;
4636

47-
function createSchema(base, name) {
48-
const schema = {
49-
name,
50-
base,
51-
paths: {},
52-
tree: {},
53-
virtuals: {},
54-
};
37+
const doc1 = createDoc(s1, base);
38+
const doc2 = createDoc(s2, base);
5539

56-
for (let i = 0; i < 10; i++) {
57-
schema.paths[`field_${i}`] = {
58-
path: `field_${i}`,
59-
schema,
60-
instance: base.types[`type_${i % 5}`],
61-
options: {
62-
type: base.types[`type_${i % 5}`],
63-
validators: [
64-
{ validator: () => true, base, schema },
65-
{ validator: () => true, base, schema },
66-
],
67-
},
68-
caster: base.types[`type_${i % 5}`].caster,
69-
};
40+
// Populated refs create additional converging paths
41+
for (let i = 0; i < 2; i++) {
42+
const ps = createSchema(base, 'p' + i);
43+
base.schemas['p' + i] = ps;
44+
doc1.$__.pop['r' + i] = { value: createDoc(ps, base), opts: { base, schema: ps } };
7045
}
7146

72-
schema.childSchemas = [];
73-
for (let i = 0; i < 3; i++) {
74-
const child = { name: `${name}_child_${i}`, base, schema, paths: {} };
75-
for (let j = 0; j < 5; j++) {
76-
child.paths[`child_field_${j}`] = {
77-
path: `child_field_${j}`,
78-
schema: child,
79-
instance: base.types[`type_${j % 5}`],
80-
options: { base, schema: child },
81-
};
82-
}
83-
schema.childSchemas.push(child);
84-
}
47+
// Cross-link creates more converging paths
48+
doc1.$__.pop.r0.value.$__parent = doc2;
8549

86-
return schema;
50+
return { doc1, doc2 };
8751
}
8852

89-
function createDocument(schema, base) {
90-
const doc = {
91-
$__: { activePaths: {}, pathsToScopes: {}, populated: {} },
92-
_doc: { name: 'test' },
93-
_schema: schema,
94-
_base: base,
95-
};
96-
97-
for (let i = 0; i < 10; i++) {
98-
doc.$__.pathsToScopes[`path_${i}`] = {
99-
schema,
53+
function createBase() {
54+
const base = { types: {}, schemas: {} };
55+
for (let i = 0; i < 4; i++) {
56+
base.types['t' + i] = {
10057
base,
101-
type: base.types[`type_${i % 5}`],
58+
caster: { base },
59+
opts: { base, validators: [{ base }, { base }] }
10260
};
10361
}
62+
return base;
63+
}
10464

105-
for (let i = 0; i < 3; i++) {
106-
const populatedSchema = createSchema(base, `Populated_${i}`);
107-
base.schemas[`Populated_${i}`] = populatedSchema;
108-
109-
doc.$__.populated[`ref_${i}`] = {
110-
value: {
111-
$__: { pathsToScopes: {}, populated: {} },
112-
_doc: { id: i },
113-
_schema: populatedSchema,
114-
_base: base,
115-
},
116-
options: { path: `ref_${i}`, model: `Model_${i}`, base },
117-
schema: populatedSchema,
65+
function createSchema(base, name) {
66+
const schema = { name, base, paths: {}, children: [] };
67+
for (let i = 0; i < 6; i++) {
68+
schema.paths['f' + i] = {
69+
schema, base,
70+
type: base.types['t' + (i % 4)],
71+
caster: base.types['t' + (i % 4)].caster,
72+
opts: { schema, base, validators: [{ schema, base }] }
11873
};
119-
120-
for (let j = 0; j < 5; j++) {
121-
doc.$__.populated[`ref_${i}`].value.$__.pathsToScopes[`field_${j}`] = {
122-
schema: populatedSchema,
123-
base,
124-
type: base.types[`type_${j % 5}`],
125-
};
74+
}
75+
for (let i = 0; i < 2; i++) {
76+
const child = { name: name + '_c' + i, base, parent: schema, paths: {} };
77+
for (let j = 0; j < 3; j++) {
78+
child.paths['cf' + j] = { schema: child, base, type: base.types['t' + (j % 4)] };
12679
}
80+
schema.children.push(child);
12781
}
128-
129-
return doc;
82+
return schema;
13083
}
13184

132-
class Document {
133-
constructor(schema, base) {
134-
Object.assign(this, createDocument(schema, base));
85+
function createDoc(schema, base) {
86+
const doc = { schema, base, $__: { scopes: {}, pop: {} } };
87+
for (let i = 0; i < 6; i++) {
88+
doc.$__.scopes['p' + i] = { schema, base, type: base.types['t' + (i % 4)] };
13589
}
136-
}
137-
138-
Object.defineProperty(Document.prototype, 'schema', {
139-
get() { return this._schema; },
140-
enumerable: true,
141-
});
142-
143-
Object.defineProperty(Document.prototype, 'base', {
144-
get() { return this._base; },
145-
enumerable: true,
146-
});
147-
148-
// Setup test objects
149-
const base = createBase();
150-
const schema1 = createSchema(base, 'Schema1');
151-
const schema2 = createSchema(base, 'Schema2');
152-
base.schemas.Schema1 = schema1;
153-
base.schemas.Schema2 = schema2;
154-
155-
const doc1 = new Document(schema1, base);
156-
const doc2 = new Document(schema2, base);
157-
doc2.$__.populated.ref_0.value.$__parent = doc1;
158-
159-
// The actual OOM test: assert.strictEqual should NOT crash
160-
// when comparing objects with large inspect output.
161-
// It should throw an AssertionError with a reasonable message size.
162-
{
163-
const actual = doc2.$__.populated.ref_0.value.$__parent;
164-
const expected = doc2;
165-
166-
// This assertion is expected to fail (they are different objects)
167-
// but it should NOT cause an OOM crash
168-
assert.throws(
169-
() => assert.strictEqual(actual, expected, 'Objects should be equal'),
170-
(err) => {
171-
// Should get an AssertionError, not an OOM crash
172-
assert.ok(err instanceof assert.AssertionError,
173-
'Expected AssertionError');
174-
175-
// Message should exist and be reasonable (not hundreds of MB)
176-
// The fix limits inspect output to 2MB, so message should be bounded
177-
const maxExpectedSize = 5 * 1024 * 1024; // 5MB (2MB * 2 + overhead)
178-
assert.ok(err.message.length < maxExpectedSize,
179-
`Error message too large: ${(err.message.length / 1024 / 1024).toFixed(2)} MB`);
180-
181-
return true;
182-
}
183-
);
90+
return doc;
18491
}

0 commit comments

Comments
 (0)