11#!/usr/bin/env node
22
33// USAGE:
4- // node certora/run.js [[CONTRACT_NAME:]SPEC_NAME] * [--all] [--options OPTIONS...] [--specs PATH ]
4+ // node certora/run.js [CONFIG] * [--all]
55// EXAMPLES:
66// node certora/run.js --all
7- // node certora/run.js AccessControl
8- // node certora/run.js AccessControlHarness:AccessControl
7+ // node certora/run.js ERC721
8+ // node certora/run.js certora/specs/ERC721.conf
99
10- import { spawn } from 'child_process' ;
11- import { PassThrough } from 'stream' ;
12- import { once } from 'events' ;
13- import path from 'path' ;
14- import yargs from 'yargs' ;
15- import { hideBin } from 'yargs/helpers' ;
16- import pLimit from 'p-limit' ;
17- import fs from 'fs/promises' ;
10+ const glob = require ( 'glob' ) ;
11+ const fs = require ( 'fs' ) ;
12+ const pLimit = require ( 'p-limit' ) . default ;
13+ const { hideBin } = require ( 'yargs/helpers' ) ;
14+ const yargs = require ( 'yargs/yargs' ) ;
15+ const { exec } = require ( 'child_process' ) ;
1816
19- const argv = yargs ( hideBin ( process . argv ) )
17+ const { argv } = yargs ( hideBin ( process . argv ) )
2018 . env ( '' )
2119 . options ( {
2220 all : {
23- alias : 'a' ,
2421 type : 'boolean' ,
2522 } ,
26- spec : {
27- alias : 's' ,
28- type : 'string' ,
29- default : path . resolve ( import . meta. dirname , 'specs.json' ) ,
30- } ,
3123 parallel : {
3224 alias : 'p' ,
3325 type : 'number' ,
@@ -38,131 +30,39 @@ const argv = yargs(hideBin(process.argv))
3830 type : 'count' ,
3931 default : 0 ,
4032 } ,
41- options : {
42- alias : 'o' ,
43- type : 'array' ,
44- default : [ ] ,
45- } ,
46- } )
47- . parse ( ) ;
48-
49- function match ( entry , request ) {
50- const [ reqSpec , reqContract ] = request . split ( ':' ) . reverse ( ) ;
51- return entry . spec == reqSpec && ( ! reqContract || entry . contract == reqContract ) ;
52- }
53-
54- const specs = JSON . parse ( fs . readFileSync ( argv . spec , 'utf8' ) ) . filter ( s => argv . all || argv . _ . some ( r => match ( s , r ) ) ) ;
33+ } ) ;
5534
35+ const pattern = 'certora/specs/*.conf' ;
5636const limit = pLimit ( argv . parallel ) ;
5737
5838if ( argv . _ . length == 0 && ! argv . all ) {
5939 console . error ( `Warning: No specs requested. Did you forget to toggle '--all'?` ) ;
60- }
61-
62- for ( const r of argv . _ ) {
63- if ( ! specs . some ( s => match ( s , r ) ) ) {
64- console . error ( `Error: Requested spec '${ r } ' not found in ${ argv . spec } ` ) ;
65- process . exitCode = 1 ;
66- }
67- }
68-
69- if ( process . exitCode ) {
70- process . exit ( process . exitCode ) ;
71- }
72-
73- for ( const { spec, contract, files, options = [ ] } of specs ) {
74- limit ( ( ) =>
75- runCertora (
76- spec ,
77- contract ,
78- files ,
79- [ ...options , ...argv . options ] . flatMap ( opt => opt . split ( ' ' ) ) ,
80- ) ,
81- ) ;
82- }
83-
84- // Run certora, aggregate the output and print it at the end
85- async function runCertora ( spec , contract , files , options = [ ] ) {
86- const args = [ ...files , '--verify' , `${ contract } :certora/specs/${ spec } .spec` , ...options ] ;
87- if ( argv . verbose ) {
88- console . log ( 'Running:' , args . join ( ' ' ) ) ;
89- }
90- const child = spawn ( 'certoraRun' , args ) ;
91-
92- const stream = new PassThrough ( ) ;
93- const output = collect ( stream ) ;
94-
95- child . stdout . pipe ( stream , { end : false } ) ;
96- child . stderr . pipe ( stream , { end : false } ) ;
97-
98- // as soon as we have a job id, print the output link
99- stream . on ( 'data' , function logStatusUrl ( data ) {
100- const { '-DjobId' : jobId , '-DuserId' : userId } = Object . fromEntries (
101- data
102- . toString ( 'utf8' )
103- . match ( / - D \S + = \S + / g)
104- ?. map ( s => s . split ( '=' ) ) || [ ] ,
105- ) ;
106-
107- if ( jobId && userId ) {
108- console . error ( `[${ spec } ] https://prover.certora.com/output/${ userId } /${ jobId } /` ) ;
109- stream . off ( 'data' , logStatusUrl ) ;
110- }
111- } ) ;
112-
113- // wait for process end
114- const [ code , signal ] = await once ( child , 'exit' ) ;
115-
116- // error
117- if ( code || signal ) {
118- console . error ( `[${ spec } ] Exited with code ${ code || signal } ` ) ;
119- process . exitCode = 1 ;
120- }
121-
122- // get all output
123- stream . end ( ) ;
124-
125- // write results in markdown format
126- writeEntry ( spec , contract , code || signal , ( await output ) . match ( / h t t p s : \/ \/ p r o v e r .c e r t o r a .c o m \/ o u t p u t \/ \S * / ) ?. [ 0 ] ) ;
127-
128- // write all details
129- console . error ( `+ certoraRun ${ args . join ( ' ' ) } \n` + ( await output ) ) ;
130- }
131-
132- // Collects stream data into a string
133- async function collect ( stream ) {
134- const buffers = [ ] ;
135- for await ( const data of stream ) {
136- const buf = Buffer . isBuffer ( data ) ? data : Buffer . from ( data ) ;
137- buffers . push ( buf ) ;
138- }
139- return Buffer . concat ( buffers ) . toString ( 'utf8' ) ;
140- }
141-
142- // Formatting
143- let hasHeader = false ;
144-
145- function formatRow ( ...array ) {
146- return [ '' , ...array , '' ] . join ( ' | ' ) ;
147- }
148-
149- function writeHeader ( ) {
150- console . log ( formatRow ( 'spec' , 'contract' , 'result' , 'status' , 'output' ) ) ;
151- console . log ( formatRow ( '-' , '-' , '-' , '-' , '-' ) ) ;
152- }
153-
154- function writeEntry ( spec , contract , success , url ) {
155- if ( ! hasHeader ) {
156- hasHeader = true ;
157- writeHeader ( ) ;
158- }
159- console . log (
160- formatRow (
161- spec ,
162- contract ,
163- success ? ':heavy_check_mark:' : ':x:' ,
164- url ? `[link](${ url ?. replace ( '/output/' , '/jobStatus/' ) } )` : 'error' ,
165- url ? `[link](${ url } )` : 'error' ,
40+ process . exitCode = 1 ;
41+ } else {
42+ Promise . all (
43+ ( argv . all ? glob . sync ( pattern ) : argv . _ . map ( name => ( fs . existsSync ( name ) ? name : pattern . replace ( '*' , name ) ) ) ) . map (
44+ ( conf , i , { length } ) =>
45+ limit (
46+ ( ) =>
47+ new Promise ( resolve => {
48+ if ( argv . verbose ) console . log ( `[${ i + 1 } /${ length } ] Running ${ conf } ` ) ;
49+ exec ( `certoraRun ${ conf } ` , ( error , stdout , stderr ) => {
50+ const match = stdout . match (
51+ 'https://prover.certora.com/output/[a-z0-9]+/[a-z0-9]+[?]anonymousKey=[a-z0-9]+' ,
52+ ) ;
53+ if ( error ) {
54+ console . error ( `[ERR] ${ conf } failed with:\n${ stderr || stdout } ` ) ;
55+ process . exitCode = 1 ;
56+ } else if ( match ) {
57+ console . log ( `${ conf } - ${ match [ 0 ] } ` ) ;
58+ } else {
59+ console . error ( `[ERR] Could not parse stdout for ${ conf } :\n${ stdout } ` ) ;
60+ process . exitCode = 1 ;
61+ }
62+ resolve ( ) ;
63+ } ) ;
64+ } ) ,
65+ ) ,
16666 ) ,
16767 ) ;
16868}
0 commit comments