Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 42f2deb

Browse filesBrowse files
LiviaMedeirosdanielleadams
authored andcommitted
test: add common.mustNotMutateObjectDeep()
This function returns a Proxy object that throws on attempt to mutate it Functions and primitives are returned directly PR-URL: #43196 Reviewed-By: Darshan Sen <raisinten@gmail.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent a057510 commit 42f2deb
Copy full SHA for 42f2deb

File tree

Expand file treeCollapse file tree

4 files changed

+309
-0
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

4 files changed

+309
-0
lines changed
Open diff view settings
Collapse file

‎test/common/README.md‎

Copy file name to clipboardExpand all lines: test/common/README.md
+35Lines changed: 35 additions & 0 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,40 @@ If `fn` is not provided, an empty function will be used.
299299
Returns a function that triggers an `AssertionError` if it is invoked. `msg` is
300300
used as the error message for the `AssertionError`.
301301

302+
### `mustNotMutateObjectDeep([target])`
303+
304+
* `target` [\<any>][<any>] default = `undefined`
305+
* return [\<any>][<any>]
306+
307+
If `target` is an Object, returns a proxy object that triggers
308+
an `AssertionError` on mutation attempt, including mutation of deeply nested
309+
Objects. Otherwise, it returns `target` directly.
310+
311+
Use of this function is encouraged for relevant regression tests.
312+
313+
```mjs
314+
import { open } from 'node:fs/promises';
315+
import { mustNotMutateObjectDeep } from '../common/index.mjs';
316+
317+
const _mutableOptions = { length: 4, position: 8 };
318+
const options = mustNotMutateObjectDeep(_mutableOptions);
319+
320+
// In filehandle.read or filehandle.write, attempt to mutate options will throw
321+
// In the test code, options can still be mutated via _mutableOptions
322+
const fh = await open('/path/to/file', 'r+');
323+
const { buffer } = await fh.read(options);
324+
_mutableOptions.position = 4;
325+
await fh.write(buffer, options);
326+
327+
// Inline usage
328+
const stats = await fh.stat(mustNotMutateObjectDeep({ bigint: true }));
329+
console.log(stats.size);
330+
```
331+
332+
Caveats: built-in objects that make use of their internal slots (for example,
333+
`Map`s and `Set`s) might not work with this function. It returns Functions
334+
directly, not preventing their mutation.
335+
302336
### `mustSucceed([fn])`
303337

304338
* `fn` [\<Function>][<Function>] default = () => {}
@@ -1024,6 +1058,7 @@ See [the WPT tests README][] for details.
10241058
[<Function>]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function
10251059
[<Object>]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object
10261060
[<RegExp>]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp
1061+
[<any>]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Data_types
10271062
[<bigint>]: https://github.com/tc39/proposal-bigint
10281063
[<boolean>]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Boolean_type
10291064
[<number>]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type
Collapse file

‎test/common/index.js‎

Copy file name to clipboardExpand all lines: test/common/index.js
+47Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,52 @@ function mustNotCall(msg) {
519519
};
520520
}
521521

522+
const _mustNotMutateObjectDeepProxies = new WeakMap();
523+
524+
function mustNotMutateObjectDeep(original) {
525+
// Return primitives and functions directly. Primitives are immutable, and
526+
// proxied functions are impossible to compare against originals, e.g. with
527+
// `assert.deepEqual()`.
528+
if (original === null || typeof original !== 'object') {
529+
return original;
530+
}
531+
532+
const cachedProxy = _mustNotMutateObjectDeepProxies.get(original);
533+
if (cachedProxy) {
534+
return cachedProxy;
535+
}
536+
537+
const _mustNotMutateObjectDeepHandler = {
538+
__proto__: null,
539+
defineProperty(target, property, descriptor) {
540+
assert.fail(`Expected no side effects, got ${inspect(property)} ` +
541+
'defined');
542+
},
543+
deleteProperty(target, property) {
544+
assert.fail(`Expected no side effects, got ${inspect(property)} ` +
545+
'deleted');
546+
},
547+
get(target, prop, receiver) {
548+
return mustNotMutateObjectDeep(Reflect.get(target, prop, receiver));
549+
},
550+
preventExtensions(target) {
551+
assert.fail('Expected no side effects, got extensions prevented on ' +
552+
inspect(target));
553+
},
554+
set(target, property, value, receiver) {
555+
assert.fail(`Expected no side effects, got ${inspect(value)} ` +
556+
`assigned to ${inspect(property)}`);
557+
},
558+
setPrototypeOf(target, prototype) {
559+
assert.fail(`Expected no side effects, got set prototype to ${prototype}`);
560+
}
561+
};
562+
563+
const proxy = new Proxy(original, _mustNotMutateObjectDeepHandler);
564+
_mustNotMutateObjectDeepProxies.set(original, proxy);
565+
return proxy;
566+
}
567+
522568
function printSkipMessage(msg) {
523569
console.log(`1..0 # Skipped: ${msg}`);
524570
}
@@ -827,6 +873,7 @@ const common = {
827873
mustCall,
828874
mustCallAtLeast,
829875
mustNotCall,
876+
mustNotMutateObjectDeep,
830877
mustSucceed,
831878
nodeProcessAborted,
832879
PIPE,
Collapse file

‎test/common/index.mjs‎

Copy file name to clipboardExpand all lines: test/common/index.mjs
+2Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const {
3535
canCreateSymLink,
3636
getCallSite,
3737
mustNotCall,
38+
mustNotMutateObjectDeep,
3839
printSkipMessage,
3940
skip,
4041
nodeProcessAborted,
@@ -81,6 +82,7 @@ export {
8182
canCreateSymLink,
8283
getCallSite,
8384
mustNotCall,
85+
mustNotMutateObjectDeep,
8486
printSkipMessage,
8587
skip,
8688
nodeProcessAborted,
Collapse file
+225Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
import { mustNotMutateObjectDeep } from '../common/index.mjs';
2+
import assert from 'node:assert';
3+
import { promisify } from 'node:util';
4+
5+
// Test common.mustNotMutateObjectDeep()
6+
7+
const original = {
8+
foo: { bar: 'baz' },
9+
qux: null,
10+
quux: [
11+
'quuz',
12+
{ corge: 'grault' },
13+
],
14+
};
15+
16+
// Make a copy to make sure original doesn't get altered by the function itself.
17+
const backup = structuredClone(original);
18+
19+
// Wrapper for convenience:
20+
const obj = () => mustNotMutateObjectDeep(original);
21+
22+
function testOriginal(root) {
23+
assert.deepStrictEqual(root, backup);
24+
return root.foo.bar === 'baz' && root.quux[1].corge.length === 6;
25+
}
26+
27+
function definePropertyOnRoot(root) {
28+
Object.defineProperty(root, 'xyzzy', {});
29+
}
30+
31+
function definePropertyOnFoo(root) {
32+
Object.defineProperty(root.foo, 'xyzzy', {});
33+
}
34+
35+
function deletePropertyOnRoot(root) {
36+
delete root.foo;
37+
}
38+
39+
function deletePropertyOnFoo(root) {
40+
delete root.foo.bar;
41+
}
42+
43+
function preventExtensionsOnRoot(root) {
44+
Object.preventExtensions(root);
45+
}
46+
47+
function preventExtensionsOnFoo(root) {
48+
Object.preventExtensions(root.foo);
49+
}
50+
51+
function preventExtensionsOnRootViaSeal(root) {
52+
Object.seal(root);
53+
}
54+
55+
function preventExtensionsOnFooViaSeal(root) {
56+
Object.seal(root.foo);
57+
}
58+
59+
function preventExtensionsOnRootViaFreeze(root) {
60+
Object.freeze(root);
61+
}
62+
63+
function preventExtensionsOnFooViaFreeze(root) {
64+
Object.freeze(root.foo);
65+
}
66+
67+
function setOnRoot(root) {
68+
root.xyzzy = 'gwak';
69+
}
70+
71+
function setOnFoo(root) {
72+
root.foo.xyzzy = 'gwak';
73+
}
74+
75+
function setQux(root) {
76+
root.qux = 'gwak';
77+
}
78+
79+
function setQuux(root) {
80+
root.quux.push('gwak');
81+
}
82+
83+
function setQuuxItem(root) {
84+
root.quux[0] = 'gwak';
85+
}
86+
87+
function setQuuxProperty(root) {
88+
root.quux[1].corge = 'gwak';
89+
}
90+
91+
function setPrototypeOfRoot(root) {
92+
Object.setPrototypeOf(root, Array);
93+
}
94+
95+
function setPrototypeOfFoo(root) {
96+
Object.setPrototypeOf(root.foo, Array);
97+
}
98+
99+
function setPrototypeOfQuux(root) {
100+
Object.setPrototypeOf(root.quux, Array);
101+
}
102+
103+
104+
{
105+
assert.ok(testOriginal(obj()));
106+
107+
assert.throws(
108+
() => definePropertyOnRoot(obj()),
109+
{ code: 'ERR_ASSERTION' }
110+
);
111+
assert.throws(
112+
() => definePropertyOnFoo(obj()),
113+
{ code: 'ERR_ASSERTION' }
114+
);
115+
assert.throws(
116+
() => deletePropertyOnRoot(obj()),
117+
{ code: 'ERR_ASSERTION' }
118+
);
119+
assert.throws(
120+
() => deletePropertyOnFoo(obj()),
121+
{ code: 'ERR_ASSERTION' }
122+
);
123+
assert.throws(
124+
() => preventExtensionsOnRoot(obj()),
125+
{ code: 'ERR_ASSERTION' }
126+
);
127+
assert.throws(
128+
() => preventExtensionsOnFoo(obj()),
129+
{ code: 'ERR_ASSERTION' }
130+
);
131+
assert.throws(
132+
() => preventExtensionsOnRootViaSeal(obj()),
133+
{ code: 'ERR_ASSERTION' }
134+
);
135+
assert.throws(
136+
() => preventExtensionsOnFooViaSeal(obj()),
137+
{ code: 'ERR_ASSERTION' }
138+
);
139+
assert.throws(
140+
() => preventExtensionsOnRootViaFreeze(obj()),
141+
{ code: 'ERR_ASSERTION' }
142+
);
143+
assert.throws(
144+
() => preventExtensionsOnFooViaFreeze(obj()),
145+
{ code: 'ERR_ASSERTION' }
146+
);
147+
assert.throws(
148+
() => setOnRoot(obj()),
149+
{ code: 'ERR_ASSERTION' }
150+
);
151+
assert.throws(
152+
() => setOnFoo(obj()),
153+
{ code: 'ERR_ASSERTION' }
154+
);
155+
assert.throws(
156+
() => setQux(obj()),
157+
{ code: 'ERR_ASSERTION' }
158+
);
159+
assert.throws(
160+
() => setQuux(obj()),
161+
{ code: 'ERR_ASSERTION' }
162+
);
163+
assert.throws(
164+
() => setQuux(obj()),
165+
{ code: 'ERR_ASSERTION' }
166+
);
167+
assert.throws(
168+
() => setQuuxItem(obj()),
169+
{ code: 'ERR_ASSERTION' }
170+
);
171+
assert.throws(
172+
() => setQuuxProperty(obj()),
173+
{ code: 'ERR_ASSERTION' }
174+
);
175+
assert.throws(
176+
() => setPrototypeOfRoot(obj()),
177+
{ code: 'ERR_ASSERTION' }
178+
);
179+
assert.throws(
180+
() => setPrototypeOfFoo(obj()),
181+
{ code: 'ERR_ASSERTION' }
182+
);
183+
assert.throws(
184+
() => setPrototypeOfQuux(obj()),
185+
{ code: 'ERR_ASSERTION' }
186+
);
187+
188+
// Test that no mutation happened:
189+
assert.ok(testOriginal(obj()));
190+
}
191+
192+
// Test various supported types, directly and nested:
193+
[
194+
undefined, null, false, true, 42, 42n, Symbol('42'), NaN, Infinity, {}, [],
195+
() => {}, async () => {}, Promise.resolve(), Math, Object.create(null),
196+
].forEach((target) => {
197+
assert.deepStrictEqual(mustNotMutateObjectDeep(target), target);
198+
assert.deepStrictEqual(mustNotMutateObjectDeep({ target }), { target });
199+
assert.deepStrictEqual(mustNotMutateObjectDeep([ target ]), [ target ]);
200+
});
201+
202+
// Test that passed functions keep working correctly:
203+
{
204+
const fn = () => 'blep';
205+
fn.foo = {};
206+
const fnImmutableView = mustNotMutateObjectDeep(fn);
207+
assert.deepStrictEqual(fnImmutableView, fn);
208+
209+
// Test that the function still works:
210+
assert.strictEqual(fn(), 'blep');
211+
assert.strictEqual(fnImmutableView(), 'blep');
212+
213+
// Test that the original function is not deeply frozen:
214+
fn.foo.bar = 'baz';
215+
assert.strictEqual(fn.foo.bar, 'baz');
216+
assert.strictEqual(fnImmutableView.foo.bar, 'baz');
217+
218+
// Test the original function is not frozen:
219+
fn.qux = 'quux';
220+
assert.strictEqual(fn.qux, 'quux');
221+
assert.strictEqual(fnImmutableView.qux, 'quux');
222+
223+
// Redefining util.promisify.custom also works:
224+
promisify(mustNotMutateObjectDeep(promisify(fn)));
225+
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.