run.js 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. #!/usr/bin/env node
  2. // USAGE:
  3. // node certora/run.js [[CONTRACT_NAME:]SPEC_NAME] [OPTIONS...]
  4. // EXAMPLES:
  5. // node certora/run.js AccessControl
  6. // node certora/run.js AccessControlHarness:AccessControl
  7. const MAX_PARALLEL = 4;
  8. const proc = require('child_process');
  9. const { PassThrough } = require('stream');
  10. const events = require('events');
  11. const micromatch = require('micromatch');
  12. const limit = require('p-limit')(MAX_PARALLEL);
  13. let [, , request = '', ...extraOptions] = process.argv;
  14. if (request.startsWith('-')) {
  15. extraOptions.unshift(request);
  16. request = '';
  17. }
  18. const [reqSpec, reqContract] = request.split(':').reverse();
  19. const specs = require(__dirname + '/specs.js')
  20. .filter(entry => !reqSpec || micromatch.isMatch(entry.spec, reqSpec))
  21. .filter(entry => !reqContract || micromatch.isMatch(entry.contract, reqContract));
  22. if (specs.length === 0) {
  23. console.error(`Error: Requested spec '${request}' not found in specs.json`);
  24. process.exit(1);
  25. }
  26. console.table(specs.map(spec => `${spec.contract}:${spec.spec} ${spec.options.join(' ')}`))
  27. for (const { spec, contract, files, options = [] } of Object.values(specs)) {
  28. limit(runCertora, spec, contract, files, [...options.flatMap(opt => opt.split(' ')), ...extraOptions]);
  29. }
  30. // Run certora, aggregate the output and print it at the end
  31. async function runCertora(spec, contract, files, options = []) {
  32. const args = [...files, '--verify', `${contract}:certora/specs/${spec}.spec`, ...options];
  33. const child = proc.spawn('certoraRun', args);
  34. const stream = new PassThrough();
  35. const output = collect(stream);
  36. child.stdout.pipe(stream, { end: false });
  37. child.stderr.pipe(stream, { end: false });
  38. // as soon as we have a jobStatus link, print it
  39. stream.on('data', function logStatusUrl(data) {
  40. const urls = data.toString('utf8').match(/https?:\S*/g);
  41. for (const url of urls ?? []) {
  42. if (url.includes('/jobStatus/')) {
  43. console.error(`[${spec}] ${url.replace('/jobStatus/', '/output/')}`);
  44. stream.off('data', logStatusUrl);
  45. break;
  46. }
  47. }
  48. });
  49. // wait for process end
  50. const [code, signal] = await events.once(child, 'exit');
  51. // error
  52. if (code || signal) {
  53. console.error(`[${spec}] Exited with code ${code || signal}`);
  54. process.exitCode = 1;
  55. }
  56. // get all output
  57. stream.end();
  58. // write results in markdown format
  59. writeEntry(spec, contract, code || signal, (await output).match(/https:\S*/)?.[0]);
  60. // write all details
  61. console.error(`+ certoraRun ${args.join(' ')}\n` + (await output));
  62. }
  63. // Collects stream data into a string
  64. async function collect(stream) {
  65. const buffers = [];
  66. for await (const data of stream) {
  67. const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
  68. buffers.push(buf);
  69. }
  70. return Buffer.concat(buffers).toString('utf8');
  71. }
  72. // Formatting
  73. let hasHeader = false;
  74. function formatRow(...array) {
  75. return ['', ...array, ''].join(' | ');
  76. }
  77. function writeHeader() {
  78. console.log(formatRow('spec', 'contract', 'result', 'status', 'output'));
  79. console.log(formatRow('-', '-', '-', '-', '-'));
  80. }
  81. function writeEntry(spec, contract, success, url) {
  82. if (!hasHeader) {
  83. hasHeader = true;
  84. writeHeader();
  85. }
  86. console.log(
  87. formatRow(
  88. spec,
  89. contract,
  90. success ? ':x:' : ':heavy_check_mark:',
  91. url ? `[link](${url})` : 'error',
  92. url ? `[link](${url?.replace('/jobStatus/', '/output/')})` : 'error',
  93. ),
  94. );
  95. }