const v8 = require('node:v8');
const fork = require('node:child_process').fork;
const path = require('node:path');
const runAuto = require('./auto.js');
const NanoTime = require('../NanoTime.js');
const createReport = require('../createReport.js');
/**
* @module pete/lib/runs/fork
*/
module.exports = createRunFork;
/**
* @private
*/
const NANO2MILLI = BigInt(1000000); // eslint-disable-line no-magic-numbers
/**
* This run forks target function to separate process and then simply passes any reports from it.
* Single, and only run, is finished after child process exits.
*
* @alias module:pete/lib/runs/fork
* @param {Function|string} fn to run
* @param {object} options
* @param {object} state to pass to the reporting function
* @return {module:pete/lib/runner~run}
*/
function createRunFork (fn, options, state) {
state.runType = module.filename;
if (!options.id) {
state.error = new Error('When using `fork`, `options.id` is required');
return null;
}
// Prevent multi-level forks
if (process.send && (!process.env.PETE_FORKED_FROM || process.env.PETE_FORKED_FROM === options.id)) {
return runAuto(fn, options, state);
}
return runFork.bind(null, fn, options, state);
}
/**
* @private
* @param {Function} fn to run
* @param {object} options
* @param {object} state to pass to the reporting function
* @param {module:pete/lib/getReporters~report} report
*/
function runFork (fn, options, state, report) {
/*
* Bun does not include column number for the first of error traces, when test is called from the beginning of a line.
* So we get just the line number :(.
* See {module:pete/lib/createId} for more info.
*/
var filepath = typeof fn === 'string' ? fn : options.id.replace(/:\d+(?::\d+)?$/, '');
/* eslint-disable array-element-newline */
var args = [
// '--name', filepath,
'--report', 'process',
'--exitOnError', options.exitOnError ? 'true' : 'false',
'--limitCPUTime', options.limitCPUTime === Infinity ? 'Infinity' : Number(options.limitCPUTime / NANO2MILLI),
'--limitRealTime', options.limitRealTime === Infinity ? 'Infinity' : Number(options.limitRealTime / NANO2MILLI),
'--limitSamples', options.limitSamples,
'--warmupSamples', options.warmupSamples
];
/* eslint-enable array-element-newline */
if (typeof fn === 'function') {
args.push('--only', options.id);
}
state.runType = 'fork';
state.mode = 'regular';
state.started = process.hrtime.bigint();
state.current = state.started;
if (!options.name) {
options.name = filepath;
}
// Ignore `options.args` - they will be set up by original test code anyway
var env = Object.assign({}, process.env, {PETE_FORKED_FROM: options.id});
var s = fork(path.resolve(process.cwd(), filepath), args, {env});
s.on('message', onMessage);
s.once('error', err => {
state.error = err;
report(err);
if (options.exitOnError) {
s.off('message', onMessage);
s.kill();
s = null;
}
});
s.once('close', () => { // CONSIDER: Use `code` passed from Node.js to report additional error?
state.finished = process.hrtime.bigint();
if (state.samplesCount < 1) {
console.warn(`"${filepath}" finished without any reports.`);
if (!state.error) {
// Assuming it was a standalone "raw" test file.
state.totalCPUTime = state.finished - state.started;
state.totalRealTime = state.finished - state.started;
state.samplesCount = 1;
state.hdr.record(Number(state.finished - state.current));
report(null, createReport(options, state));
}
}
// Finish
report();
});
/**
* @private
* @param {object} data
*/
function onMessage (data) {
if (!data || (!data.error && !data.report)) {
return;
}
state.samplesCount += 1;
if (data.report) {
data.report = v8.deserialize(Buffer.from(data.report, 'base64'));
if (data.types && data.types.NanoTime) {
data.types.NanoTime.forEach(key => {
data.report[key] = NanoTime.from(data.report[key]);
});
}
}
var error = null;
if (data.error) {
// Reconstruct error
error = new Error(data.error.split('\n')[0]);
error.stack = data.error;
if (!state.error) {
state.error = error;
}
if (options.exitOnError && (!data.report || data.report.exitOnError)) {
s.off('message', onMessage);
s.kill();
s = null;
}
}
if (data.report) {
report(error, data.report);
}
}
}