Skip to content

Commit 9beb363

Browse files
committed
test: add common.mustNotMutate()
1 parent 3d575a4 commit 9beb363

4 files changed

Lines changed: 266 additions & 0 deletions

File tree

test/common/README.md

Lines changed: 35 additions & 0 deletions
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+
### `mustNotMutate([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 and for new
312+
APIs that work with user-provided objects.
313+
314+
```mjs
315+
import { open } from 'node:fs/promises';
316+
import { mustNotMutate } from '../common/index.mjs';
317+
318+
const _mutableOptions = { length: 4, position: 8 };
319+
const options = mustNotMutate(_mutableOptions);
320+
321+
// In filehandle.read or filehandle.write, attempt to mutate options will throw
322+
// In the test code, options can still be mutated via _mutableOptions
323+
const fh = await open('/path/to/file', 'r+');
324+
const { buffer } = await fh.read(options);
325+
_mutableOptions.position = 4;
326+
await fh.write(buffer, options);
327+
328+
// Inline usage
329+
const stats = await fh.stat(mustNotMutate({ bigint: true }));
330+
console.log(stats.size);
331+
```
332+
333+
Caveat: built-in objects that make use of their internal slots (for example,
334+
`Map`s and `Set`s) might not work with this function.
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

test/common/index.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,48 @@ function mustNotCall(msg) {
519519
};
520520
}
521521

522+
const _mustNotMutateProxies = new WeakMap();
523+
524+
function mustNotMutate(original) {
525+
if (original === null || typeof original !== 'object') {
526+
return original;
527+
}
528+
529+
const cachedProxy = _mustNotMutateProxies.get(original);
530+
if (cachedProxy) {
531+
return cachedProxy;
532+
}
533+
534+
const _mustNotMutateHandler = {
535+
defineProperty(target, property, descriptor) {
536+
assert.fail(`Expected no side effects, got ${util.inspect(property)} ` +
537+
'defined');
538+
},
539+
deleteProperty(target, property) {
540+
assert.fail(`Expected no side effects, got ${util.inspect(property)} ` +
541+
'deleted');
542+
},
543+
get(target, prop, receiver) {
544+
return mustNotMutate(Reflect.get(target, prop, receiver));
545+
},
546+
preventExtensions(target) {
547+
assert.fail('Expected no side effects, got extensions prevented on ' +
548+
util.inspect(target));
549+
},
550+
set(target, property, value, receiver) {
551+
assert.fail(`Expected no side effects, got ${util.inspect(value)} ` +
552+
`assigned to ${util.inspect(property)}`);
553+
},
554+
setPrototypeOf(target, prototype) {
555+
assert.fail(`Expected no side effects, got set prototype to ${prototype}`);
556+
}
557+
};
558+
559+
const proxy = new Proxy(original, _mustNotMutateHandler);
560+
_mustNotMutateProxies.set(original, proxy);
561+
return proxy;
562+
}
563+
522564
function printSkipMessage(msg) {
523565
console.log(`1..0 # Skipped: ${msg}`);
524566
}
@@ -827,6 +869,7 @@ const common = {
827869
mustCall,
828870
mustCallAtLeast,
829871
mustNotCall,
872+
mustNotMutate,
830873
mustSucceed,
831874
nodeProcessAborted,
832875
PIPE,

test/common/index.mjs

Lines 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+
mustNotMutate,
3839
printSkipMessage,
3940
skip,
4041
nodeProcessAborted,
@@ -81,6 +82,7 @@ export {
8182
canCreateSymLink,
8283
getCallSite,
8384
mustNotCall,
85+
mustNotMutate,
8486
printSkipMessage,
8587
skip,
8688
nodeProcessAborted,
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import { mustNotMutate } from '../common/index.mjs';
2+
import assert from 'node:assert';
3+
4+
// Test common.mustNotMutate()
5+
6+
const original = {
7+
foo: { bar: 'baz' },
8+
qux: null,
9+
quux: [
10+
'quuz',
11+
{ corge: 'grault' },
12+
],
13+
};
14+
15+
// Wrapper for convenience
16+
const obj = () => mustNotMutate(original);
17+
18+
function testOriginal(root) {
19+
assert.deepStrictEqual(root, original);
20+
return root.foo.bar === 'baz' && root.quux[1].corge.length === 6;
21+
}
22+
23+
function definePropertyOnRoot(root) {
24+
Object.defineProperty(root, 'xyzzy', {});
25+
}
26+
27+
function definePropertyOnFoo(root) {
28+
Object.defineProperty(root.foo, 'xyzzy', {});
29+
}
30+
31+
function deletePropertyOnRoot(root) {
32+
delete root.foo;
33+
}
34+
35+
function deletePropertyOnFoo(root) {
36+
delete root.foo.bar;
37+
}
38+
39+
function preventExtensionsOnRoot(root) {
40+
Object.preventExtensions(root);
41+
}
42+
43+
function preventExtensionsOnFoo(root) {
44+
Object.preventExtensions(root.foo);
45+
}
46+
47+
function preventExtensionsOnRootViaSeal(root) {
48+
Object.seal(root);
49+
}
50+
51+
function preventExtensionsOnFooViaSeal(root) {
52+
Object.seal(root.foo);
53+
}
54+
55+
function preventExtensionsOnRootViaFreeze(root) {
56+
Object.freeze(root);
57+
}
58+
59+
function preventExtensionsOnFooViaFreeze(root) {
60+
Object.freeze(root.foo);
61+
}
62+
63+
function setOnRoot(root) {
64+
root.xyzzy = 'gwak';
65+
}
66+
67+
function setOnFoo(root) {
68+
root.foo.xyzzy = 'gwak';
69+
}
70+
71+
function setQux(root) {
72+
root.qux = 'gwak';
73+
}
74+
75+
function setQuux(root) {
76+
root.quux.push('gwak');
77+
}
78+
79+
function setQuuxItem(root) {
80+
root.quux[0] = 'gwak';
81+
}
82+
83+
function setQuuxProperty(root) {
84+
root.quux[1].corge = 'gwak';
85+
}
86+
87+
function setPrototypeOfRoot(root) {
88+
Object.setPrototypeOf(root, Array);
89+
}
90+
91+
function setPrototypeOfFoo(root) {
92+
Object.setPrototypeOf(root.foo, Array);
93+
}
94+
95+
function setPrototypeOfQuux(root) {
96+
Object.setPrototypeOf(root.quux, Array);
97+
}
98+
99+
100+
{
101+
assert.ok(testOriginal(obj()));
102+
103+
assert.throws(
104+
() => definePropertyOnRoot(obj()),
105+
{ code: 'ERR_ASSERTION' }
106+
);
107+
assert.throws(
108+
() => definePropertyOnFoo(obj()),
109+
{ code: 'ERR_ASSERTION' }
110+
);
111+
assert.throws(
112+
() => deletePropertyOnRoot(obj()),
113+
{ code: 'ERR_ASSERTION' }
114+
);
115+
assert.throws(
116+
() => deletePropertyOnFoo(obj()),
117+
{ code: 'ERR_ASSERTION' }
118+
);
119+
assert.throws(
120+
() => preventExtensionsOnRoot(obj()),
121+
{ code: 'ERR_ASSERTION' }
122+
);
123+
assert.throws(
124+
() => preventExtensionsOnFoo(obj()),
125+
{ code: 'ERR_ASSERTION' }
126+
);
127+
assert.throws(
128+
() => preventExtensionsOnRootViaSeal(obj()),
129+
{ code: 'ERR_ASSERTION' }
130+
);
131+
assert.throws(
132+
() => preventExtensionsOnFooViaSeal(obj()),
133+
{ code: 'ERR_ASSERTION' }
134+
);
135+
assert.throws(
136+
() => preventExtensionsOnRootViaFreeze(obj()),
137+
{ code: 'ERR_ASSERTION' }
138+
);
139+
assert.throws(
140+
() => preventExtensionsOnFooViaFreeze(obj()),
141+
{ code: 'ERR_ASSERTION' }
142+
);
143+
assert.throws(
144+
() => setOnRoot(obj()),
145+
{ code: 'ERR_ASSERTION' }
146+
);
147+
assert.throws(
148+
() => setOnFoo(obj()),
149+
{ code: 'ERR_ASSERTION' }
150+
);
151+
assert.throws(
152+
() => setQux(obj()),
153+
{ code: 'ERR_ASSERTION' }
154+
);
155+
assert.throws(
156+
() => setQuux(obj()),
157+
{ code: 'ERR_ASSERTION' }
158+
);
159+
assert.throws(
160+
() => setQuux(obj()),
161+
{ code: 'ERR_ASSERTION' }
162+
);
163+
assert.throws(
164+
() => setQuuxItem(obj()),
165+
{ code: 'ERR_ASSERTION' }
166+
);
167+
assert.throws(
168+
() => setQuuxProperty(obj()),
169+
{ code: 'ERR_ASSERTION' }
170+
);
171+
assert.throws(
172+
() => setPrototypeOfRoot(obj()),
173+
{ code: 'ERR_ASSERTION' }
174+
);
175+
assert.throws(
176+
() => setPrototypeOfFoo(obj()),
177+
{ code: 'ERR_ASSERTION' }
178+
);
179+
assert.throws(
180+
() => setPrototypeOfQuux(obj()),
181+
{ code: 'ERR_ASSERTION' }
182+
);
183+
184+
// Test that no mutation happened
185+
assert.ok(testOriginal(obj()));
186+
}

0 commit comments

Comments
 (0)