You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
146 lines
3.8 KiB
146 lines
3.8 KiB
#!/usr/bin/env node
|
|
|
|
// USAGE:
|
|
// node certora/run.js [[CONTRACT_NAME:]SPEC_NAME]* [--all] [--options OPTIONS...] [--specs PATH]
|
|
// EXAMPLES:
|
|
// node certora/run.js --all
|
|
// node certora/run.js AccessControl
|
|
// node certora/run.js AccessControlHarness:AccessControl
|
|
|
|
const proc = require('child_process');
|
|
const { PassThrough } = require('stream');
|
|
const events = require('events');
|
|
|
|
const argv = require('yargs')
|
|
.env('')
|
|
.options({
|
|
all: {
|
|
alias: 'a',
|
|
type: 'boolean',
|
|
},
|
|
spec: {
|
|
alias: 's',
|
|
type: 'string',
|
|
default: __dirname + '/specs.json',
|
|
},
|
|
parallel: {
|
|
alias: 'p',
|
|
type: 'number',
|
|
default: 4,
|
|
},
|
|
options: {
|
|
alias: 'o',
|
|
type: 'array',
|
|
default: [],
|
|
},
|
|
}).argv;
|
|
|
|
function match(entry, request) {
|
|
const [reqSpec, reqContract] = request.split(':').reverse();
|
|
return entry.spec == reqSpec && (!reqContract || entry.contract == reqContract);
|
|
}
|
|
|
|
const specs = require(argv.spec).filter(s => argv.all || argv._.some(r => match(s, r)));
|
|
const limit = require('p-limit')(argv.parallel);
|
|
|
|
if (argv._.length == 0 && !argv.all) {
|
|
console.error(`Warning: No specs requested. Did you forgot to toggle '--all'?`);
|
|
}
|
|
|
|
for (const r of argv._) {
|
|
if (!specs.some(s => match(s, r))) {
|
|
console.error(`Error: Requested spec '${r}' not found in ${argv.spec}`);
|
|
process.exitCode = 1;
|
|
}
|
|
}
|
|
|
|
if (process.exitCode) {
|
|
process.exit(process.exitCode);
|
|
}
|
|
|
|
for (const { spec, contract, files, options = [] } of specs) {
|
|
limit(runCertora, spec, contract, files, [...options.flatMap(opt => opt.split(' ')), ...argv.options]);
|
|
}
|
|
|
|
// Run certora, aggregate the output and print it at the end
|
|
async function runCertora(spec, contract, files, options = []) {
|
|
const args = [...files, '--verify', `${contract}:certora/specs/${spec}.spec`, ...options];
|
|
const child = proc.spawn('certoraRun', args);
|
|
|
|
const stream = new PassThrough();
|
|
const output = collect(stream);
|
|
|
|
child.stdout.pipe(stream, { end: false });
|
|
child.stderr.pipe(stream, { end: false });
|
|
|
|
// as soon as we have a job id, print the output link
|
|
stream.on('data', function logStatusUrl(data) {
|
|
const { '-DjobId': jobId, '-DuserId': userId } = Object.fromEntries(
|
|
data
|
|
.toString('utf8')
|
|
.match(/-D\S+=\S+/g)
|
|
?.map(s => s.split('=')) || [],
|
|
);
|
|
|
|
if (jobId && userId) {
|
|
console.error(`[${spec}] https://prover.certora.com/output/${userId}/${jobId}/`);
|
|
stream.off('data', logStatusUrl);
|
|
}
|
|
});
|
|
|
|
// wait for process end
|
|
const [code, signal] = await events.once(child, 'exit');
|
|
|
|
// error
|
|
if (code || signal) {
|
|
console.error(`[${spec}] Exited with code ${code || signal}`);
|
|
process.exitCode = 1;
|
|
}
|
|
|
|
// get all output
|
|
stream.end();
|
|
|
|
// write results in markdown format
|
|
writeEntry(spec, contract, code || signal, (await output).match(/https:\/\/prover.certora.com\/output\/\S*/)?.[0]);
|
|
|
|
// write all details
|
|
console.error(`+ certoraRun ${args.join(' ')}\n` + (await output));
|
|
}
|
|
|
|
// Collects stream data into a string
|
|
async function collect(stream) {
|
|
const buffers = [];
|
|
for await (const data of stream) {
|
|
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
buffers.push(buf);
|
|
}
|
|
return Buffer.concat(buffers).toString('utf8');
|
|
}
|
|
|
|
// Formatting
|
|
let hasHeader = false;
|
|
|
|
function formatRow(...array) {
|
|
return ['', ...array, ''].join(' | ');
|
|
}
|
|
|
|
function writeHeader() {
|
|
console.log(formatRow('spec', 'contract', 'result', 'status', 'output'));
|
|
console.log(formatRow('-', '-', '-', '-', '-'));
|
|
}
|
|
|
|
function writeEntry(spec, contract, success, url) {
|
|
if (!hasHeader) {
|
|
hasHeader = true;
|
|
writeHeader();
|
|
}
|
|
console.log(
|
|
formatRow(
|
|
spec,
|
|
contract,
|
|
success ? ':x:' : ':heavy_check_mark:',
|
|
url ? `[link](${url?.replace('/output/', '/jobStatus/')})` : 'error',
|
|
url ? `[link](${url})` : 'error',
|
|
),
|
|
);
|
|
}
|
|
|