|
1 | 1 | // Flags: --max-old-space-size=512 |
2 | 2 | 'use strict'; |
3 | 3 |
|
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. |
12 | 8 |
|
13 | 9 | require('../common'); |
14 | 10 | const assert = require('assert'); |
15 | 11 |
|
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(); |
19 | 15 |
|
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 | +} |
27 | 26 |
|
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(); |
43 | 31 |
|
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; |
46 | 36 |
|
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); |
55 | 39 |
|
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 } }; |
70 | 45 | } |
71 | 46 |
|
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; |
85 | 49 |
|
86 | | - return schema; |
| 50 | + return { doc1, doc2 }; |
87 | 51 | } |
88 | 52 |
|
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] = { |
100 | 57 | base, |
101 | | - type: base.types[`type_${i % 5}`], |
| 58 | + caster: { base }, |
| 59 | + opts: { base, validators: [{ base }, { base }] } |
102 | 60 | }; |
103 | 61 | } |
| 62 | + return base; |
| 63 | +} |
104 | 64 |
|
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 }] } |
118 | 73 | }; |
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)] }; |
126 | 79 | } |
| 80 | + schema.children.push(child); |
127 | 81 | } |
128 | | - |
129 | | - return doc; |
| 82 | + return schema; |
130 | 83 | } |
131 | 84 |
|
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)] }; |
135 | 89 | } |
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; |
184 | 91 | } |
0 commit comments