2024-10-31 07:11:41 +00:00
// Usage: `node tolk-tester.js tests_dir` OR `node tolk-tester.js test_file.tolk`
// from current dir, providing some env (see getenv() calls).
// This is a JS version of tolk-tester.py to test Tolk compiled to WASM.
// Don't forget to keep it identical to Python version!
const fs = require ( 'fs' ) ;
const os = require ( 'os' ) ;
const path = require ( 'path' ) ;
const child _process = require ( 'child_process' ) ;
function print ( ... args ) {
console . log ( ... args )
}
/** @return {string} */
function getenv ( name , def = null ) {
if ( name in process . env )
return process . env [ name ]
if ( def === null ) {
print ( ` Environment variable ${ name } is not set ` )
process . exit ( 1 )
}
return def
}
const TOLKFIFTLIB _MODULE = getenv ( 'TOLKFIFTLIB_MODULE' )
const TOLKFIFTLIB _WASM = getenv ( 'TOLKFIFTLIB_WASM' )
const FIFT _EXECUTABLE = getenv ( 'FIFT_EXECUTABLE' )
const FIFT _LIBS _FOLDER = getenv ( 'FIFTPATH' ) // this env is needed for fift to work properly
2024-10-31 07:16:19 +00:00
const STDLIB _FOLDER = _ _dirname + '/../crypto/smartcont/tolk-stdlib'
2024-10-31 07:11:41 +00:00
const TMP _DIR = os . tmpdir ( )
class CmdLineOptions {
constructor ( /**string[]*/ argv ) {
if ( argv . length !== 3 ) {
print ( "Usage: node tolk-tester.js tests_dir OR node tolk-tester.js test_file.tolk" )
process . exit ( 1 )
}
if ( ! fs . existsSync ( argv [ 2 ] ) ) {
print ( ` Input ' ${ argv [ 2 ] } ' doesn't exist ` )
process . exit ( 1 )
}
if ( fs . lstatSync ( argv [ 2 ] ) . isDirectory ( ) ) {
this . tests _dir = argv [ 2 ]
this . test _file = null
} else {
this . tests _dir = path . dirname ( argv [ 2 ] )
this . test _file = argv [ 2 ]
}
}
/** @return {string[]} */
find _tests ( ) {
if ( this . test _file ) // an option to run (debug) a single test
return [ this . test _file ]
let tests = fs . readdirSync ( this . tests _dir ) . filter ( f => f . endsWith ( '.tolk' ) || f . endsWith ( '.ton' ) )
tests . sort ( )
return tests . map ( f => path . join ( this . tests _dir , f ) )
}
}
class ParseInputError extends Error {
}
class TolkCompilationFailedError extends Error {
constructor ( /**string*/ message , /**string*/ stderr ) {
super ( message ) ;
this . stderr = stderr
}
}
class TolkCompilationSucceededError extends Error {
}
class FiftExecutionFailedError extends Error {
constructor ( /**string*/ message , /**string*/ stderr ) {
super ( message ) ;
this . stderr = stderr
}
}
class CompareOutputError extends Error {
constructor ( /**string*/ message , /**string*/ output ) {
super ( message ) ;
this . output = output
}
}
class CompareFifCodegenError extends Error {
}
class CompareCodeHashError extends Error {
}
/ *
* In positive tests , there are several testcases "input X should produce output Y" .
* /
class TolkTestCaseInputOutput {
static reJustNumber = /^[-+]?\d+$/
static reMathExpr = /^[0x123456789()+\-*/<>]*$/
constructor ( /**string*/ method _id _str , /**string*/ input _str , /**string*/ output _str ) {
let processed _inputs = [ ]
for ( let in _arg of input _str . split ( ' ' ) ) {
if ( in _arg . length === 0 )
continue
else if ( in _arg . startsWith ( "x{" ) || TolkTestCaseInputOutput . reJustNumber . test ( in _arg ) )
processed _inputs . push ( in _arg )
else if ( TolkTestCaseInputOutput . reMathExpr . test ( in _arg ) )
// replace "3<<254" with "3n<<254n" (big number) before eval (in Python we don't need this)
processed _inputs . push ( eval ( in _arg . replace ( '//' , '/' ) . replace ( /(\d)($|\D)/gmi , '$1n$2' ) ) . toString ( ) )
else if ( in _arg === "null" )
processed _inputs . push ( "null" )
else
throw new ParseInputError ( ` ' ${ in _arg } ' can't be evaluated ` )
}
this . method _id = + method _id _str
this . input = processed _inputs . join ( ' ' )
this . expected _output = output _str
}
check ( /**string[]*/ stdout _lines , /**number*/ line _idx ) {
if ( stdout _lines [ line _idx ] !== this . expected _output )
throw new CompareOutputError ( ` error on case # ${ line _idx + 1 } ( ${ this . method _id } | ${ this . input } ): expected ' ${ this . expected _output } ', found ' ${ stdout _lines [ line _idx ] } ' ` , stdout _lines . join ( "\n" ) )
}
}
/ *
* @ stderr checks , when compilation fails , that stderr ( compilation error ) is expected .
* If it ' s multiline , all lines must be present in specified order .
* /
class TolkTestCaseStderr {
constructor ( /**string[]*/ stderr _pattern , /**boolean*/ avoid ) {
this . stderr _pattern = stderr _pattern
this . avoid = avoid
}
check ( /**string*/ stderr ) {
const line _match = this . find _pattern _in _stderr ( stderr . split ( /\n/ ) )
if ( line _match === - 1 && ! this . avoid )
throw new CompareOutputError ( "pattern not found in stderr:\n" +
this . stderr _pattern . map ( x => " " + x ) . join ( "\n" ) , stderr )
else if ( line _match !== - 1 && this . avoid )
throw new CompareOutputError ( ` pattern found (line ${ line _match + 1 } ), but not expected to be: \n ` +
this . stderr _pattern . map ( x => " " + x ) . join ( "\n" ) , stderr )
}
find _pattern _in _stderr ( /**string[]*/ stderr ) {
for ( let line _start = 0 ; line _start < stderr . length ; ++ line _start )
if ( this . try _match _pattern ( 0 , stderr , line _start ) )
return line _start
return - 1
}
try _match _pattern ( /**number*/ pattern _offset , /**string[]*/ stderr , /**number*/ offset ) {
if ( pattern _offset >= this . stderr _pattern . length )
return true
if ( offset >= stderr . length )
return false
const line _pattern = this . stderr _pattern [ pattern _offset ]
const line _output = stderr [ offset ]
return line _output . includes ( line _pattern ) && this . try _match _pattern ( pattern _offset + 1 , stderr , offset + 1 )
}
}
/ *
* @ fif _codegen checks that contents of compiled . fif matches the expected pattern .
* @ fif _codegen _avoid checks that is does not match the pattern .
* See comments in run _tests . py .
* /
class TolkTestCaseFifCodegen {
constructor ( /**string[]*/ fif _pattern , /**boolean*/ avoid ) {
/** @type {string[]} */
this . fif _pattern = fif _pattern . map ( s => s . trim ( ) )
this . avoid = avoid
}
check ( /**string[]*/ fif _output ) {
const line _match = this . find _pattern _in _fif _output ( fif _output )
if ( line _match === - 1 && ! this . avoid )
throw new CompareFifCodegenError ( "pattern not found:\n" +
this . fif _pattern . map ( x => " " + x ) . join ( "\n" ) )
else if ( line _match !== - 1 && this . avoid )
throw new CompareFifCodegenError ( ` pattern found (line ${ line _match + 1 } ), but not expected to be: \n ` +
this . fif _pattern . map ( x => " " + x ) . join ( "\n" ) )
}
find _pattern _in _fif _output ( /**string[]*/ fif _output ) {
for ( let line _start = 0 ; line _start < fif _output . length ; ++ line _start )
if ( this . try _match _pattern ( 0 , fif _output , line _start ) )
return line _start
return - 1
}
try _match _pattern ( /**number*/ pattern _offset , /**string[]*/ fif _output , /**number*/ offset ) {
if ( pattern _offset >= this . fif _pattern . length )
return true
if ( offset >= fif _output . length )
return false
const line _pattern = this . fif _pattern [ pattern _offset ]
const line _output = fif _output [ offset ]
if ( line _pattern !== "..." ) {
if ( ! TolkTestCaseFifCodegen . does _line _match ( line _pattern , line _output ) )
return false
return this . try _match _pattern ( pattern _offset + 1 , fif _output , offset + 1 )
}
while ( offset < fif _output . length ) {
if ( this . try _match _pattern ( pattern _offset + 1 , fif _output , offset ) )
return true
offset = offset + 1
}
return false
}
static split _line _to _cmd _and _comment ( /**string*/ trimmed _line ) {
const pos = trimmed _line . indexOf ( "//" )
if ( pos === - 1 )
return [ trimmed _line , null ]
else
return [ trimmed _line . substring ( 0 , pos ) . trimEnd ( ) , trimmed _line . substring ( pos + 2 ) . trimStart ( ) ]
}
static does _line _match ( /**string*/ line _pattern , /**string*/ line _output ) {
const [ cmd _pattern , comment _pattern ] = TolkTestCaseFifCodegen . split _line _to _cmd _and _comment ( line _pattern )
const [ cmd _output , comment _output ] = TolkTestCaseFifCodegen . split _line _to _cmd _and _comment ( line _output . trim ( ) )
return cmd _pattern === cmd _output && ( comment _pattern === null || comment _pattern === comment _output )
}
}
/ *
* @ code _hash checks that hash of compiled output . fif matches the provided value .
* It ' s used to "record" code boc hash and to check that it remains the same on compiler modifications .
* Being much less flexible than @ fif _codegen , it nevertheless gives a guarantee of bytecode stability .
* /
class TolkTestCaseExpectedHash {
constructor ( /**string*/ expected _hash ) {
this . code _hash = expected _hash
}
check ( /**string*/ fif _code _hash ) {
if ( this . code _hash !== fif _code _hash )
throw new CompareCodeHashError ( ` expected ${ this . code _hash } , actual ${ fif _code _hash } ` )
}
}
class TolkTestFile {
constructor ( /**string*/ tolk _filename , /**string*/ artifacts _folder ) {
this . line _idx = 0
this . tolk _filename = tolk _filename
this . artifacts _folder = artifacts _folder
this . compilation _should _fail = false
/** @type {TolkTestCaseStderr[]} */
this . stderr _includes = [ ]
/** @type {TolkTestCaseInputOutput[]} */
this . input _output = [ ]
/** @type {TolkTestCaseFifCodegen[]} */
this . fif _codegen = [ ]
/** @type {TolkTestCaseExpectedHash | null} */
this . expected _hash = null
/** @type {string | null} */
this . experimental _options = null
}
parse _input _from _tolk _file ( ) {
const lines = fs . readFileSync ( this . tolk _filename , 'utf-8' ) . split ( /\r?\n/ )
this . line _idx = 0
while ( this . line _idx < lines . length ) {
const line = lines [ this . line _idx ]
if ( line . startsWith ( '@testcase' ) ) {
let s = line . split ( "|" ) . map ( p => p . trim ( ) )
if ( s . length !== 4 )
throw new ParseInputError ( ` incorrect format of @testcase: ${ line } ` )
this . input _output . push ( new TolkTestCaseInputOutput ( s [ 1 ] , s [ 2 ] , s [ 3 ] ) )
} else if ( line . startsWith ( '@compilation_should_fail' ) ) {
this . compilation _should _fail = true
} else if ( line . startsWith ( '@stderr' ) ) {
this . stderr _includes . push ( new TolkTestCaseStderr ( this . parse _string _value ( lines ) , false ) )
} else if ( line . startsWith ( "@fif_codegen_avoid" ) ) {
this . fif _codegen . push ( new TolkTestCaseFifCodegen ( this . parse _string _value ( lines ) , true ) )
} else if ( line . startsWith ( "@fif_codegen" ) ) {
this . fif _codegen . push ( new TolkTestCaseFifCodegen ( this . parse _string _value ( lines ) , false ) )
} else if ( line . startsWith ( "@code_hash" ) ) {
this . expected _hash = new TolkTestCaseExpectedHash ( this . parse _string _value ( lines , false ) [ 0 ] )
} else if ( line . startsWith ( "@experimental_options" ) ) {
this . experimental _options = line . substring ( 22 )
}
this . line _idx ++
}
if ( this . input _output . length === 0 && ! this . compilation _should _fail )
throw new ParseInputError ( "no @testcase present" )
if ( this . input _output . length !== 0 && this . compilation _should _fail )
throw new ParseInputError ( "@testcase present, but compilation_should_fail" )
}
/** @return {string[]} */
parse _string _value ( /**string[]*/ lines , allow _multiline = true ) {
// a tag must be followed by a space (single-line), e.g. '@stderr some text'
// or be a multi-line value, surrounded by """
const line = lines [ this . line _idx ]
const pos _sp = line . indexOf ( ' ' )
const is _multi _line = lines [ this . line _idx + 1 ] === '"""'
const is _single _line = pos _sp !== - 1
if ( ! is _single _line && ! is _multi _line )
throw new ParseInputError ( ` ${ line } value is empty (not followed by a string or a multiline """) ` )
if ( is _single _line && is _multi _line )
throw new ParseInputError ( ` ${ line . substring ( 0 , pos _sp ) } value is both single-line and followed by """ ` )
if ( is _multi _line && ! allow _multiline )
throw new ParseInputError ( ` ${ line } value should be single-line ` ) ;
if ( is _single _line )
return [ line . substring ( pos _sp + 1 ) . trim ( ) ]
this . line _idx += 2
let s _multiline = [ ]
while ( this . line _idx < lines . length && lines [ this . line _idx ] !== '"""' ) {
s _multiline . push ( lines [ this . line _idx ] )
this . line _idx = this . line _idx + 1
}
return s _multiline
}
get _compiled _fif _filename ( ) {
return this . artifacts _folder + "/compiled.fif"
}
get _runner _fif _filename ( ) {
return this . artifacts _folder + "/runner.fif"
}
async run _and _check ( ) {
const wasmModule = await compileWasm ( TOLKFIFTLIB _MODULE , TOLKFIFTLIB _WASM )
let res = compileFile ( wasmModule , this . tolk _filename , this . experimental _options )
let exit _code = res . status === 'ok' ? 0 : 1
let stderr = res . message
let stdout = ''
if ( exit _code === 0 && this . compilation _should _fail )
throw new TolkCompilationSucceededError ( "compilation succeeded, but it should have failed" )
if ( exit _code !== 0 && this . compilation _should _fail ) {
for ( let should _include of this . stderr _includes )
should _include . check ( stderr )
return
}
if ( exit _code !== 0 && ! this . compilation _should _fail )
throw new TolkCompilationFailedError ( ` tolk exit_code = ${ exit _code } ` , stderr )
fs . writeFileSync ( this . get _compiled _fif _filename ( ) , ` "Asm.fif" include \n ${ res . fiftCode } ` )
{
let runner = ` " ${ this . get _compiled _fif _filename ( ) } " include <s constant code \n `
for ( let t of this . input _output )
runner += ` ${ t . input } ${ t . method _id } code 1 runvmx abort"exitcode is not 0" .s cr { drop } depth 1- times \n `
if ( this . expected _hash !== null )
runner += ` " ${ this . get _compiled _fif _filename ( ) } " include hash .s \n `
fs . writeFileSync ( this . get _runner _fif _filename ( ) , runner )
}
res = child _process . spawnSync ( FIFT _EXECUTABLE , [ this . get _runner _fif _filename ( ) ] )
exit _code = res . status
stderr = ( res . stderr || res . error ) . toString ( )
stdout = ( res . stdout || '' ) . toString ( )
if ( exit _code )
throw new FiftExecutionFailedError ( ` fift exit_code = ${ exit _code } ` , stderr )
let stdout _lines = stdout . split ( "\n" ) . map ( x => x . trim ( ) ) . filter ( s => s . length > 0 )
let fif _code _hash = null
if ( this . expected _hash !== null ) { // then the last stdout line is a hash
fif _code _hash = stdout _lines [ stdout _lines . length - 1 ]
stdout _lines = stdout _lines . slice ( 0 , stdout _lines . length - 1 )
}
if ( stdout _lines . length !== this . input _output . length )
throw new CompareOutputError ( ` unexpected number of fift output: ${ stdout _lines . length } lines, but ${ this . input _output . length } testcases ` , stdout )
for ( let i = 0 ; i < stdout _lines . length ; ++ i )
this . input _output [ i ] . check ( stdout _lines , i )
if ( this . fif _codegen . length ) {
const fif _output = fs . readFileSync ( this . get _compiled _fif _filename ( ) , 'utf-8' ) . split ( /\r?\n/ )
for ( let fif _codegen of this . fif _codegen )
fif _codegen . check ( fif _output )
}
if ( this . expected _hash !== null )
this . expected _hash . check ( fif _code _hash )
}
}
async function run _all _tests ( /**string[]*/ tests ) {
for ( let ti = 0 ; ti < tests . length ; ++ ti ) {
let tolk _filename = tests [ ti ]
print ( ` Running test ${ ti + 1 } / ${ tests . length } : ${ tolk _filename } ` )
let artifacts _folder = path . join ( TMP _DIR , tolk _filename )
let testcase = new TolkTestFile ( tolk _filename , artifacts _folder )
try {
if ( ! fs . existsSync ( artifacts _folder ) )
fs . mkdirSync ( artifacts _folder , { recursive : true } )
testcase . parse _input _from _tolk _file ( )
await testcase . run _and _check ( )
fs . rmSync ( artifacts _folder , { recursive : true } )
if ( testcase . compilation _should _fail )
print ( " OK, compilation failed as it should" )
else
print ( ` OK, ${ testcase . input _output . length } cases ` )
} catch ( e ) {
if ( e instanceof ParseInputError ) {
print ( ` Error parsing input (cur line # ${ testcase . line _idx + 1 } ): ` , e . message )
process . exit ( 2 )
} else if ( e instanceof TolkCompilationFailedError ) {
print ( " Error compiling tolk:" , e . message )
print ( " stderr:" )
print ( e . stderr . trimEnd ( ) )
process . exit ( 2 )
} else if ( e instanceof FiftExecutionFailedError ) {
print ( " Error executing fift:" , e . message )
print ( " stderr:" )
print ( e . stderr . trimEnd ( ) )
print ( " compiled.fif at:" , testcase . get _compiled _fif _filename ( ) )
process . exit ( 2 )
} else if ( e instanceof CompareOutputError ) {
print ( " Mismatch in output:" , e . message )
print ( " Full output:" )
print ( e . output . trimEnd ( ) )
print ( " Was compiled to:" , testcase . get _compiled _fif _filename ( ) )
process . exit ( 2 )
} else if ( e instanceof CompareFifCodegenError ) {
print ( " Mismatch in fif codegen:" , e . message )
print ( " Was compiled to:" , testcase . get _compiled _fif _filename ( ) )
print ( fs . readFileSync ( testcase . get _compiled _fif _filename ( ) , 'utf-8' ) )
process . exit ( 2 )
} else if ( e instanceof CompareCodeHashError ) {
print ( " Mismatch in code hash:" , e . message )
print ( " Was compiled to:" , testcase . get _compiled _fif _filename ( ) )
process . exit ( 2 )
}
throw e
}
}
}
const tests = new CmdLineOptions ( process . argv ) . find _tests ( )
print ( ` Found ${ tests . length } tests ` )
run _all _tests ( tests ) . then (
( ) => print ( ` Done, ${ tests . length } tests ` ) ,
console . error
)
// below are WASM helpers, which don't exist in Python version
process . setMaxListeners ( 0 ) ;
function copyToCString ( mod , str ) {
const len = mod . lengthBytesUTF8 ( str ) + 1 ;
const ptr = mod . _malloc ( len ) ;
mod . stringToUTF8 ( str , ptr , len ) ;
return ptr ;
}
function copyToCStringPtr ( mod , str , ptr ) {
const allocated = copyToCString ( mod , str ) ;
mod . setValue ( ptr , allocated , '*' ) ;
return allocated ;
}
2024-10-31 07:16:19 +00:00
/** @return {string} */
2024-10-31 07:11:41 +00:00
function copyFromCString ( mod , ptr ) {
return mod . UTF8ToString ( ptr ) ;
}
/** @return {{status: string, message: string, fiftCode: string, codeBoc: string, codeHashHex: string}} */
function compileFile ( mod , filename , experimentalOptions ) {
2024-10-31 07:16:19 +00:00
// see tolk-wasm.cpp: typedef void (*WasmFsReadCallback)(int, char const*, char**, char**)
2024-10-31 07:11:41 +00:00
const callbackPtr = mod . addFunction ( ( kind , dataPtr , destContents , destError ) => {
if ( kind === 0 ) { // realpath
try {
2024-10-31 07:16:19 +00:00
let relative = copyFromCString ( mod , dataPtr )
if ( relative . startsWith ( '@stdlib/' ) ) {
// import "@stdlib/filename" or import "@stdlib/filename.tolk"
relative = STDLIB _FOLDER + '/' + relative . substring ( 7 )
if ( ! relative . endsWith ( '.tolk' ) ) {
relative += '.tolk'
}
}
copyToCStringPtr ( mod , fs . realpathSync ( relative ) , destContents ) ;
2024-10-31 07:11:41 +00:00
} catch ( err ) {
copyToCStringPtr ( mod , 'cannot find file' , destError ) ;
}
} else if ( kind === 1 ) { // read file
try {
2024-10-31 07:16:19 +00:00
const absolute = copyFromCString ( mod , dataPtr ) // already normalized (as returned above)
copyToCStringPtr ( mod , fs . readFileSync ( absolute ) . toString ( 'utf-8' ) , destContents ) ;
2024-10-31 07:11:41 +00:00
} catch ( err ) {
copyToCStringPtr ( mod , err . message || err . toString ( ) , destError ) ;
}
} else {
copyToCStringPtr ( mod , 'Unknown callback kind=' + kind , destError ) ;
}
} , 'viiii' ) ;
const config = {
optimizationLevel : 2 ,
withStackComments : true ,
experimentalOptions : experimentalOptions || undefined ,
entrypointFileName : filename
} ;
const configPtr = copyToCString ( mod , JSON . stringify ( config ) ) ;
const responsePtr = mod . _tolk _compile ( configPtr , callbackPtr ) ;
return JSON . parse ( copyFromCString ( mod , responsePtr ) ) ;
}
async function compileWasm ( tolkFiftLibJsFileName , tolkFiftLibWasmFileName ) {
const wasmModule = require ( tolkFiftLibJsFileName )
const wasmBinary = new Uint8Array ( fs . readFileSync ( tolkFiftLibWasmFileName ) )
return await wasmModule ( { wasmBinary } )
}