run.js 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. #!/usr/bin/env node
  2. // USAGE:
  3. // node certora/run.js [[CONTRACT_NAME:]SPEC_NAME]* [--all] [--options OPTIONS...] [--specs PATH]
  4. // EXAMPLES:
  5. // node certora/run.js --all
  6. // node certora/run.js AccessControl
  7. // node certora/run.js AccessControlHarness:AccessControl
  8. const proc = require('child_process');
  9. const { PassThrough } = require('stream');
  10. const events = require('events');
  11. const argv = require('yargs')
  12. .env('')
  13. .options({
  14. all: {
  15. alias: 'a',
  16. type: 'boolean',
  17. },
  18. spec: {
  19. alias: 's',
  20. type: 'string',
  21. default: __dirname + '/specs.json',
  22. },
  23. parallel: {
  24. alias: 'p',
  25. type: 'number',
  26. default: 4,
  27. },
  28. verbose: {
  29. alias: 'v',
  30. type: 'count',
  31. default: 0,
  32. },
  33. options: {
  34. alias: 'o',
  35. type: 'array',
  36. default: [],
  37. },
  38. }).argv;
  39. function match(entry, request) {
  40. const [reqSpec, reqContract] = request.split(':').reverse();
  41. return entry.spec == reqSpec && (!reqContract || entry.contract == reqContract);
  42. }
  43. const specs = require(argv.spec).filter(s => argv.all || argv._.some(r => match(s, r)));
  44. const limit = require('p-limit')(argv.parallel);
  45. if (argv._.length == 0 && !argv.all) {
  46. console.error(`Warning: No specs requested. Did you forgot to toggle '--all'?`);
  47. }
  48. for (const r of argv._) {
  49. if (!specs.some(s => match(s, r))) {
  50. console.error(`Error: Requested spec '${r}' not found in ${argv.spec}`);
  51. process.exitCode = 1;
  52. }
  53. }
  54. if (process.exitCode) {
  55. process.exit(process.exitCode);
  56. }
  57. for (const { spec, contract, files, options = [] } of specs) {
  58. limit(runCertora, spec, contract, files, [...options.flatMap(opt => opt.split(' ')), ...argv.options]);
  59. }
  60. // Run certora, aggregate the output and print it at the end
  61. async function runCertora(spec, contract, files, options = []) {
  62. const args = [...files, '--verify', `${contract}:certora/specs/${spec}.spec`, ...options];
  63. if (argv.verbose) {
  64. console.log('Running:', args.join(' '));
  65. }
  66. const child = proc.spawn('certoraRun', args);
  67. const stream = new PassThrough();
  68. const output = collect(stream);
  69. child.stdout.pipe(stream, { end: false });
  70. child.stderr.pipe(stream, { end: false });
  71. // as soon as we have a job id, print the output link
  72. stream.on('data', function logStatusUrl(data) {
  73. const { '-DjobId': jobId, '-DuserId': userId } = Object.fromEntries(
  74. data
  75. .toString('utf8')
  76. .match(/-D\S+=\S+/g)
  77. ?.map(s => s.split('=')) || [],
  78. );
  79. if (jobId && userId) {
  80. console.error(`[${spec}] https://prover.certora.com/output/${userId}/${jobId}/`);
  81. stream.off('data', logStatusUrl);
  82. }
  83. });
  84. // wait for process end
  85. const [code, signal] = await events.once(child, 'exit');
  86. // error
  87. if (code || signal) {
  88. console.error(`[${spec}] Exited with code ${code || signal}`);
  89. process.exitCode = 1;
  90. }
  91. // get all output
  92. stream.end();
  93. // write results in markdown format
  94. writeEntry(spec, contract, code || signal, (await output).match(/https:\/\/prover.certora.com\/output\/\S*/)?.[0]);
  95. // write all details
  96. console.error(`+ certoraRun ${args.join(' ')}\n` + (await output));
  97. }
  98. // Collects stream data into a string
  99. async function collect(stream) {
  100. const buffers = [];
  101. for await (const data of stream) {
  102. const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
  103. buffers.push(buf);
  104. }
  105. return Buffer.concat(buffers).toString('utf8');
  106. }
  107. // Formatting
  108. let hasHeader = false;
  109. function formatRow(...array) {
  110. return ['', ...array, ''].join(' | ');
  111. }
  112. function writeHeader() {
  113. console.log(formatRow('spec', 'contract', 'result', 'status', 'output'));
  114. console.log(formatRow('-', '-', '-', '-', '-'));
  115. }
  116. function writeEntry(spec, contract, success, url) {
  117. if (!hasHeader) {
  118. hasHeader = true;
  119. writeHeader();
  120. }
  121. console.log(
  122. formatRow(
  123. spec,
  124. contract,
  125. success ? ':x:' : ':heavy_check_mark:',
  126. url ? `[link](${url?.replace('/output/', '/jobStatus/')})` : 'error',
  127. url ? `[link](${url})` : 'error',
  128. ),
  129. );
  130. }