node/test/pummel/test-policy-integrity-parent-module.js
Richard Lau 73d53fe9f5 test: only skip slow tests on Raspberry Pi devices
Detect the Raspberry Pi devices in the Node.js CI and only skip the
slow tests on those instead of all armv7l devices.

PR-URL: https://github.com/nodejs/node/pull/42645
Reviewed-By: Michael Dawson <midawson@redhat.com>
Reviewed-By: Tobias Nießen <tniessen@tnie.de>
Reviewed-By: Stewart X Addison <sxa@redhat.com>
Reviewed-By: Darshan Sen <raisinten@gmail.com>
2022-04-12 14:19:36 -04:00

353 lines
9.2 KiB
JavaScript

'use strict';
const common = require('../common');
if (!common.hasCrypto) {
common.skip('missing crypto');
}
if (common.isPi) {
common.skip('Too slow for Raspberry Pi devices');
}
common.requireNoPackageJSONAbove();
const { debuglog } = require('util');
const debug = debuglog('test');
const tmpdir = require('../common/tmpdir');
const assert = require('assert');
const { spawnSync, spawn } = require('child_process');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const { pathToFileURL } = require('url');
const cpus = require('os').cpus().length;
function hash(algo, body) {
const values = [];
{
const h = crypto.createHash(algo);
h.update(body);
values.push(`${algo}-${h.digest('base64')}`);
}
{
const h = crypto.createHash(algo);
h.update(body.replace('\n', '\r\n'));
values.push(`${algo}-${h.digest('base64')}`);
}
return values;
}
const policyPath = './policy.json';
const parentBody = {
commonjs: `
if (!process.env.DEP_FILE) {
console.error(
'missing required DEP_FILE env to determine dependency'
);
process.exit(33);
}
require(process.env.DEP_FILE)
`,
module: `
if (!process.env.DEP_FILE) {
console.error(
'missing required DEP_FILE env to determine dependency'
);
process.exit(33);
}
import(process.env.DEP_FILE)
`,
};
let nextTestId = 1;
function newTestId() {
return nextTestId++;
}
tmpdir.refresh();
common.requireNoPackageJSONAbove(tmpdir.path);
let spawned = 0;
const toSpawn = [];
function queueSpawn(opts) {
toSpawn.push(opts);
drainQueue();
}
function drainQueue() {
if (spawned > cpus) {
return;
}
if (toSpawn.length) {
const config = toSpawn.shift();
const {
shouldSucceed,
preloads,
entryPath,
willDeletePolicy,
onError,
resources,
parentPath,
depPath,
} = config;
const testId = newTestId();
const configDirPath = path.join(
tmpdir.path,
`test-policy-integrity-permutation-${testId}`
);
const tmpPolicyPath = path.join(
tmpdir.path,
`deletable-policy-${testId}.json`
);
const cliPolicy = willDeletePolicy ? tmpPolicyPath : policyPath;
fs.rmSync(configDirPath, { maxRetries: 3, recursive: true, force: true });
fs.mkdirSync(configDirPath, { recursive: true });
const manifest = {
onerror: onError,
resources: {},
};
const manifestPath = path.join(configDirPath, policyPath);
for (const [resourcePath, { body, integrities }] of Object.entries(
resources
)) {
const filePath = path.join(configDirPath, resourcePath);
if (integrities !== null) {
manifest.resources[pathToFileURL(filePath).href] = {
integrity: integrities.join(' '),
dependencies: true,
};
}
fs.writeFileSync(filePath, body, 'utf8');
}
const manifestBody = JSON.stringify(manifest);
fs.writeFileSync(manifestPath, manifestBody);
if (cliPolicy === tmpPolicyPath) {
fs.writeFileSync(tmpPolicyPath, manifestBody);
}
const spawnArgs = [
process.execPath,
[
'--unhandled-rejections=strict',
'--experimental-policy',
cliPolicy,
...preloads.flatMap((m) => ['-r', m]),
entryPath,
'--',
testId,
configDirPath,
],
{
env: {
...process.env,
DELETABLE_POLICY_FILE: tmpPolicyPath,
PARENT_FILE: parentPath,
DEP_FILE: depPath,
},
cwd: configDirPath,
stdio: 'pipe',
},
];
spawned++;
const stdout = [];
const stderr = [];
const child = spawn(...spawnArgs);
child.stdout.on('data', (d) => stdout.push(d));
child.stderr.on('data', (d) => stderr.push(d));
child.on('exit', (status, signal) => {
spawned--;
try {
if (shouldSucceed) {
assert.strictEqual(status, 0);
} else {
assert.notStrictEqual(status, 0);
}
} catch (e) {
console.log(
'permutation',
testId,
'failed'
);
console.dir(
{ config, manifest },
{ depth: null }
);
console.log('exit code:', status, 'signal:', signal);
console.log(`stdout: ${Buffer.concat(stdout)}`);
console.log(`stderr: ${Buffer.concat(stderr)}`);
throw e;
}
fs.rmSync(configDirPath, { maxRetries: 3, recursive: true, force: true });
drainQueue();
});
}
}
{
const { status } = spawnSync(
process.execPath,
['--experimental-policy', policyPath, '--experimental-policy', policyPath],
{
stdio: 'pipe',
}
);
assert.notStrictEqual(status, 0, 'Should not allow multiple policies');
}
{
const enoentFilepath = path.join(tmpdir.path, 'enoent');
try {
fs.unlinkSync(enoentFilepath);
} catch {
// Continue regardless of error.
}
const { status } = spawnSync(
process.execPath,
['--experimental-policy', enoentFilepath, '-e', ''],
{
stdio: 'pipe',
}
);
assert.notStrictEqual(status, 0, 'Should not allow missing policies');
}
/**
* @template {Record<string, Array<string | string[] | boolean>>} T
* @param {T} configurations
* @param {object} path
* @returns {Array<{[key: keyof T]: T[keyof configurations]}>}
*/
function permutations(configurations, path = {}) {
const keys = Object.keys(configurations);
if (keys.length === 0) {
return path;
}
const config = keys[0];
const { [config]: values, ...otherConfigs } = configurations;
return values.flatMap((value) => {
return permutations(otherConfigs, { ...path, [config]: value });
});
}
const tests = new Set();
function fileExtensionFormat(extension) {
if (extension === '.js') {
return 'module';
} else if (extension === '.mjs') {
return 'module';
} else if (extension === '.cjs') {
return 'commonjs';
}
throw new Error('unknown format ' + extension);
}
for (const permutation of permutations({
preloads: [[], ['parent'], ['dep']],
onError: ['log', 'exit'],
parentExtension: ['.js', '.mjs', '.cjs'],
parentIntegrity: ['match', 'invalid', 'missing'],
depExtension: ['.js', '.mjs', '.cjs'],
depIntegrity: ['match', 'invalid', 'missing'],
packageIntegrity: ['match', 'invalid', 'missing'],
})) {
let shouldSucceed = true;
const parentPath = `./parent${permutation.parentExtension}`;
const parentFormat = fileExtensionFormat(permutation.parentExtension);
const depFormat = fileExtensionFormat(permutation.depExtension);
// non-sensical attempt to require ESM
if (depFormat === 'module' && parentFormat === 'commonjs') {
continue;
}
const depPath = `./dep${permutation.depExtension}`;
const entryPath = parentPath;
const packageJSON = {
main: entryPath,
type: 'module',
};
const resources = {
[depPath]: {
body: '',
integrities: hash('sha256', ''),
},
};
if (permutation.depIntegrity === 'invalid') {
resources[depPath].body += '\n// INVALID INTEGRITY';
shouldSucceed = false;
} else if (permutation.depIntegrity === 'missing') {
resources[depPath].integrities = null;
shouldSucceed = false;
} else if (permutation.depIntegrity !== 'match') {
throw new Error('unreachable');
}
if (parentFormat !== 'commonjs') {
permutation.preloads = permutation.preloads.filter((_) => _ !== 'parent');
}
resources[parentPath] = {
body: parentBody[parentFormat],
integrities: hash('sha256', parentBody[parentFormat]),
};
if (permutation.parentIntegrity === 'invalid') {
resources[parentPath].body += '\n// INVALID INTEGRITY';
shouldSucceed = false;
} else if (permutation.parentIntegrity === 'missing') {
resources[parentPath].integrities = null;
shouldSucceed = false;
} else if (permutation.parentIntegrity !== 'match') {
throw new Error('unreachable');
}
let packageBody = JSON.stringify(packageJSON, null, 2);
let packageIntegrities = hash('sha256', packageBody);
if (
permutation.parentExtension !== '.js' ||
permutation.depExtension !== '.js'
) {
// NO PACKAGE LOOKUP
continue;
}
if (permutation.packageIntegrity === 'invalid') {
packageJSON['//'] = 'INVALID INTEGRITY';
packageBody = JSON.stringify(packageJSON, null, 2);
shouldSucceed = false;
} else if (permutation.packageIntegrity === 'missing') {
packageIntegrities = [];
shouldSucceed = false;
} else if (permutation.packageIntegrity !== 'match') {
throw new Error('unreachable');
}
resources['./package.json'] = {
body: packageBody,
integrities: packageIntegrities,
};
if (permutation.onError === 'log') {
shouldSucceed = true;
}
tests.add(
JSON.stringify({
onError: permutation.onError,
shouldSucceed,
entryPath,
willDeletePolicy: false,
preloads: permutation.preloads
.map((_) => {
return {
'': '',
'parent': parentFormat === 'commonjs' ? parentPath : '',
'dep': depFormat === 'commonjs' ? depPath : '',
}[_];
})
.filter(Boolean),
parentPath,
depPath,
resources,
})
);
}
debug(`spawning ${tests.size} policy integrity permutations`);
for (const config of tests) {
const parsed = JSON.parse(config);
queueSpawn(parsed);
}