Skip to content

Commit d88cb01

Browse files
committed
test: fix flaky test-policy-integrity
Split the test into three tests so that it doesn't time out. Fixes: #40694 Fixes: #38088
1 parent 8d6a025 commit d88cb01

File tree

4 files changed

+763
-48
lines changed

4 files changed

+763
-48
lines changed

test/pummel/pummel.status

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ prefix pummel
77
[true] # This section applies to all platforms
88

99
[$system==win32]
10-
# https://github.com/nodejs/node/issues/40694
11-
test-policy-integrity: PASS,FLAKY
1210

1311
[$system==linux]
1412
# https://github.com/nodejs/node/issues/38226
Lines changed: 365 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,365 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
5+
if (!common.hasCrypto) {
6+
common.skip('missing crypto');
7+
}
8+
9+
if (process.config.variables.arm_version === '7') {
10+
common.skip('Too slow for armv7 bots');
11+
}
12+
13+
common.requireNoPackageJSONAbove();
14+
15+
const { debuglog } = require('util');
16+
const debug = debuglog('test');
17+
const tmpdir = require('../common/tmpdir');
18+
const assert = require('assert');
19+
const { spawnSync, spawn } = require('child_process');
20+
const crypto = require('crypto');
21+
const fs = require('fs');
22+
const path = require('path');
23+
const { pathToFileURL } = require('url');
24+
25+
const cpus = require('os').cpus().length;
26+
27+
function hash(algo, body) {
28+
const values = [];
29+
{
30+
const h = crypto.createHash(algo);
31+
h.update(body);
32+
values.push(`${algo}-${h.digest('base64')}`);
33+
}
34+
{
35+
const h = crypto.createHash(algo);
36+
h.update(body.replace('\n', '\r\n'));
37+
values.push(`${algo}-${h.digest('base64')}`);
38+
}
39+
return values;
40+
}
41+
42+
const policyPath = './policy.json';
43+
const parentBody = {
44+
commonjs: `
45+
if (!process.env.DEP_FILE) {
46+
console.error(
47+
'missing required DEP_FILE env to determine dependency'
48+
);
49+
process.exit(33);
50+
}
51+
require(process.env.DEP_FILE)
52+
`,
53+
module: `
54+
if (!process.env.DEP_FILE) {
55+
console.error(
56+
'missing required DEP_FILE env to determine dependency'
57+
);
58+
process.exit(33);
59+
}
60+
import(process.env.DEP_FILE)
61+
`,
62+
};
63+
64+
let nextTestId = 1;
65+
function newTestId() {
66+
return nextTestId++;
67+
}
68+
tmpdir.refresh();
69+
common.requireNoPackageJSONAbove(tmpdir.path);
70+
71+
let spawned = 0;
72+
const toSpawn = [];
73+
function queueSpawn(opts) {
74+
toSpawn.push(opts);
75+
drainQueue();
76+
}
77+
78+
function drainQueue() {
79+
if (spawned > cpus) {
80+
return;
81+
}
82+
if (toSpawn.length) {
83+
const config = toSpawn.shift();
84+
const {
85+
shouldSucceed,
86+
preloads,
87+
entryPath,
88+
onError,
89+
resources,
90+
parentPath,
91+
depPath,
92+
} = config;
93+
const testId = newTestId();
94+
const configDirPath = path.join(
95+
tmpdir.path,
96+
`test-policy-integrity-permutation-${testId}`
97+
);
98+
const tmpPolicyPath = path.join(
99+
tmpdir.path,
100+
`deletable-policy-${testId}.json`
101+
);
102+
103+
fs.rmSync(configDirPath, { maxRetries: 3, recursive: true, force: true });
104+
fs.mkdirSync(configDirPath, { recursive: true });
105+
const manifest = {
106+
onerror: onError,
107+
resources: {},
108+
};
109+
const manifestPath = path.join(configDirPath, policyPath);
110+
for (const [resourcePath, { body, integrities }] of Object.entries(
111+
resources
112+
)) {
113+
const filePath = path.join(configDirPath, resourcePath);
114+
if (integrities !== null) {
115+
manifest.resources[pathToFileURL(filePath).href] = {
116+
integrity: integrities.join(' '),
117+
dependencies: true,
118+
};
119+
}
120+
fs.writeFileSync(filePath, body, 'utf8');
121+
}
122+
const manifestBody = JSON.stringify(manifest);
123+
fs.writeFileSync(manifestPath, manifestBody);
124+
if (policyPath === tmpPolicyPath) {
125+
fs.writeFileSync(tmpPolicyPath, manifestBody);
126+
}
127+
const spawnArgs = [
128+
process.execPath,
129+
[
130+
'--unhandled-rejections=strict',
131+
'--experimental-policy',
132+
policyPath,
133+
...preloads.flatMap((m) => ['-r', m]),
134+
entryPath,
135+
'--',
136+
testId,
137+
configDirPath,
138+
],
139+
{
140+
env: {
141+
...process.env,
142+
DELETABLE_POLICY_FILE: tmpPolicyPath,
143+
PARENT_FILE: parentPath,
144+
DEP_FILE: depPath,
145+
},
146+
cwd: configDirPath,
147+
stdio: 'pipe',
148+
},
149+
];
150+
spawned++;
151+
const stdout = [];
152+
const stderr = [];
153+
const child = spawn(...spawnArgs);
154+
child.stdout.on('data', (d) => stdout.push(d));
155+
child.stderr.on('data', (d) => stderr.push(d));
156+
child.on('exit', (status, signal) => {
157+
spawned--;
158+
try {
159+
if (shouldSucceed) {
160+
assert.strictEqual(status, 0);
161+
} else {
162+
assert.notStrictEqual(status, 0);
163+
}
164+
} catch (e) {
165+
console.log(
166+
'permutation',
167+
testId,
168+
'failed'
169+
);
170+
console.dir(
171+
{ config, manifest },
172+
{ depth: null }
173+
);
174+
console.log('exit code:', status, 'signal:', signal);
175+
console.log(`stdout: ${Buffer.concat(stdout)}`);
176+
console.log(`stderr: ${Buffer.concat(stderr)}`);
177+
throw e;
178+
}
179+
fs.rmSync(configDirPath, { maxRetries: 3, recursive: true, force: true });
180+
drainQueue();
181+
});
182+
}
183+
}
184+
185+
{
186+
const { status } = spawnSync(
187+
process.execPath,
188+
['--experimental-policy', policyPath, '--experimental-policy', policyPath],
189+
{
190+
stdio: 'pipe',
191+
}
192+
);
193+
assert.notStrictEqual(status, 0, 'Should not allow multiple policies');
194+
}
195+
{
196+
const enoentFilepath = path.join(tmpdir.path, 'enoent');
197+
try {
198+
fs.unlinkSync(enoentFilepath);
199+
} catch { }
200+
const { status } = spawnSync(
201+
process.execPath,
202+
['--experimental-policy', enoentFilepath, '-e', ''],
203+
{
204+
stdio: 'pipe',
205+
}
206+
);
207+
assert.notStrictEqual(status, 0, 'Should not allow missing policies');
208+
}
209+
210+
/**
211+
* @template {Record<string, Array<string | string[] | boolean>>} T
212+
* @param {T} configurations
213+
* @param {object} path
214+
* @returns {Array<{[key: keyof T]: T[keyof configurations]}>}
215+
*/
216+
function permutations(configurations, path = {}) {
217+
const keys = Object.keys(configurations);
218+
if (keys.length === 0) {
219+
return path;
220+
}
221+
const config = keys[0];
222+
const { [config]: values, ...otherConfigs } = configurations;
223+
return values.flatMap((value) => {
224+
return permutations(otherConfigs, { ...path, [config]: value });
225+
});
226+
}
227+
const tests = new Set();
228+
function fileExtensionFormat(extension, packageType) {
229+
if (extension === '.js') {
230+
return packageType === 'module' ? 'module' : 'commonjs';
231+
} else if (extension === '.mjs') {
232+
return 'module';
233+
} else if (extension === '.cjs') {
234+
return 'commonjs';
235+
}
236+
throw new Error('unknown format ' + extension);
237+
}
238+
for (const permutation of permutations({
239+
preloads: [[], ['parent'], ['dep']],
240+
onError: ['log', 'exit'],
241+
parentExtension: ['.js', '.mjs', '.cjs'],
242+
parentIntegrity: ['match', 'invalid', 'missing'],
243+
depExtension: ['.js', '.mjs', '.cjs'],
244+
depIntegrity: ['match', 'invalid', 'missing'],
245+
packageType: ['no-package-json', 'module', 'commonjs'],
246+
packageIntegrity: ['match', 'invalid', 'missing'],
247+
})) {
248+
let shouldSucceed = true;
249+
const parentPath = `./parent${permutation.parentExtension}`;
250+
const effectivePackageType =
251+
permutation.packageType === 'module' ? 'module' : 'commonjs';
252+
const parentFormat = fileExtensionFormat(
253+
permutation.parentExtension,
254+
effectivePackageType
255+
);
256+
const depFormat = fileExtensionFormat(
257+
permutation.depExtension,
258+
effectivePackageType
259+
);
260+
// non-sensical attempt to require ESM
261+
if (depFormat === 'module' && parentFormat === 'commonjs') {
262+
continue;
263+
}
264+
const depPath = `./dep${permutation.depExtension}`;
265+
266+
const packageJSON = {
267+
main: depPath,
268+
type: permutation.packageType,
269+
};
270+
if (permutation.packageType === 'no-field') {
271+
delete packageJSON.type;
272+
}
273+
const resources = {
274+
[depPath]: {
275+
body: '',
276+
integrities: hash('sha256', ''),
277+
},
278+
};
279+
if (permutation.depIntegrity === 'invalid') {
280+
resources[depPath].body += '\n// INVALID INTEGRITY';
281+
shouldSucceed = false;
282+
} else if (permutation.depIntegrity === 'missing') {
283+
resources[depPath].integrities = null;
284+
shouldSucceed = false;
285+
} else if (permutation.depIntegrity === 'match') {
286+
} else {
287+
throw new Error('unreachable');
288+
}
289+
if (parentFormat !== 'commonjs') {
290+
permutation.preloads = permutation.preloads.filter((_) => _ !== 'parent');
291+
}
292+
const hasParent = permutation.preloads.includes('parent');
293+
if (hasParent) {
294+
resources[parentPath] = {
295+
body: parentBody[parentFormat],
296+
integrities: hash('sha256', parentBody[parentFormat]),
297+
};
298+
if (permutation.parentIntegrity === 'invalid') {
299+
resources[parentPath].body += '\n// INVALID INTEGRITY';
300+
shouldSucceed = false;
301+
} else if (permutation.parentIntegrity === 'missing') {
302+
resources[parentPath].integrities = null;
303+
shouldSucceed = false;
304+
} else if (permutation.parentIntegrity === 'match') {
305+
} else {
306+
throw new Error('unreachable');
307+
}
308+
}
309+
310+
if (permutation.packageType !== 'no-package-json') {
311+
let packageBody = JSON.stringify(packageJSON, null, 2);
312+
let packageIntegrities = hash('sha256', packageBody);
313+
if (
314+
permutation.parentExtension !== '.js' ||
315+
permutation.depExtension !== '.js'
316+
) {
317+
// NO PACKAGE LOOKUP
318+
continue;
319+
}
320+
if (permutation.packageIntegrity === 'invalid') {
321+
packageJSON['//'] = 'INVALID INTEGRITY';
322+
packageBody = JSON.stringify(packageJSON, null, 2);
323+
shouldSucceed = false;
324+
} else if (permutation.packageIntegrity === 'missing') {
325+
packageIntegrities = [];
326+
shouldSucceed = false;
327+
} else if (permutation.packageIntegrity === 'match') {
328+
} else {
329+
throw new Error('unreachable');
330+
}
331+
resources['./package.json'] = {
332+
body: packageBody,
333+
integrities: packageIntegrities,
334+
};
335+
}
336+
337+
if (permutation.onError === 'log') {
338+
shouldSucceed = true;
339+
}
340+
tests.add(
341+
JSON.stringify({
342+
onError: permutation.onError,
343+
shouldSucceed,
344+
entryPath: depPath,
345+
preloads: permutation.preloads
346+
.map((_) => {
347+
return {
348+
'': '',
349+
'parent': parentFormat === 'commonjs' ? parentPath : '',
350+
'dep': depFormat === 'commonjs' ? depPath : '',
351+
}[_];
352+
})
353+
.filter(Boolean),
354+
parentPath,
355+
depPath,
356+
resources,
357+
})
358+
);
359+
}
360+
debug(`spawning ${tests.size} policy integrity permutations`);
361+
362+
for (const config of tests) {
363+
const parsed = JSON.parse(config);
364+
queueSpawn(parsed);
365+
}

0 commit comments

Comments
 (0)