Skip to content

Commit 413a099

Browse files
test: refactor test-assert-lage-object-diff-oom to be minimal
1 parent 5bb2cdc commit 413a099

File tree

1 file changed

+59
-158
lines changed

1 file changed

+59
-158
lines changed
Lines changed: 59 additions & 158 deletions
Original file line numberDiff line numberDiff line change
@@ -1,184 +1,85 @@
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.
19-
20-
function createBase() {
21-
const base = {
22-
id: 'base',
23-
models: {},
24-
schemas: {},
25-
types: {},
26-
};
27-
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-
}
12+
{
13+
const { doc1, doc2 } = createTestObjects();
4314

44-
return base;
15+
// Should throw AssertionError, not OOM
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+
);
4525
}
4626

47-
function createSchema(base, name) {
48-
const schema = {
49-
name,
50-
base,
51-
paths: {},
52-
tree: {},
53-
virtuals: {},
54-
};
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 = { types: {}, schemas: {} };
5531

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,
32+
for (let i = 0; i < 4; i++) {
33+
base.types['t' + i] = {
34+
base,
35+
caster: { base },
36+
opts: { base, validators: [{ base }, { base }] }
6937
};
7038
}
7139

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-
}
85-
86-
return schema;
87-
}
40+
const s1 = createSchema(base, 's1');
41+
const s2 = createSchema(base, 's2');
42+
base.schemas.s1 = s1;
43+
base.schemas.s2 = s2;
8844

89-
function createDocument(schema, base) {
90-
const doc = {
91-
$__: { activePaths: {}, pathsToScopes: {}, populated: {} },
92-
_doc: { name: 'test' },
93-
_schema: schema,
94-
_base: base,
95-
};
45+
const doc1 = createDoc(s1, base);
46+
const doc2 = createDoc(s2, base);
9647

97-
for (let i = 0; i < 10; i++) {
98-
doc.$__.pathsToScopes[`path_${i}`] = {
99-
schema,
100-
base,
101-
type: base.types[`type_${i % 5}`],
102-
};
48+
for (let i = 0; i < 2; i++) {
49+
const ps = createSchema(base, 'p' + i);
50+
base.schemas['p' + i] = ps;
51+
doc1.$__.pop['r' + i] = { value: createDoc(ps, base), opts: { base, schema: ps } };
10352
}
10453

105-
for (let i = 0; i < 3; i++) {
106-
const populatedSchema = createSchema(base, `Populated_${i}`);
107-
base.schemas[`Populated_${i}`] = populatedSchema;
54+
doc1.$__.pop.r0.value.$__parent = doc2;
10855

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,
118-
};
56+
return { doc1, doc2 };
11957

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}`],
58+
function createSchema(base, name) {
59+
const schema = { name, base, paths: {}, children: [] };
60+
for (let i = 0; i < 6; i++) {
61+
schema.paths['f' + i] = {
62+
schema, base,
63+
type: base.types['t' + (i % 4)],
64+
caster: base.types['t' + (i % 4)].caster,
65+
opts: { schema, base, validators: [{ schema, base }] }
12566
};
12667
}
68+
for (let i = 0; i < 2; i++) {
69+
const child = { name: name + '_c' + i, base, parent: schema, paths: {} };
70+
for (let j = 0; j < 3; j++) {
71+
child.paths['cf' + j] = { schema: child, base, type: base.types['t' + (j % 4)] };
72+
}
73+
schema.children.push(child);
74+
}
75+
return schema;
12776
}
12877

129-
return doc;
130-
}
131-
132-
class Document {
133-
constructor(schema, base) {
134-
Object.assign(this, createDocument(schema, base));
135-
}
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;
78+
function createDoc(schema, base) {
79+
const doc = { schema, base, $__: { scopes: {}, pop: {} } };
80+
for (let i = 0; i < 6; i++) {
81+
doc.$__.scopes['p' + i] = { schema, base, type: base.types['t' + (i % 4)] };
18282
}
183-
);
83+
return doc;
84+
}
18485
}

0 commit comments

Comments
 (0)