run.js 4.2 KB

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