import * as fs from 'fs' ; import * as path from 'path' ; import { exec } from 'child_process' ; import "mocha" ; import * as chai from "chai" ; import { TestHost } from "../common/testHost" ; import * as util from "../common/testUtils" ; import { resolve } from 'url' ; import { promisify } from 'util' ; function initGlobals ( ) { let g = global as any ; g . pxt = pxt ; g . ts = ts ; g . pxtc = pxtc ; g . btoa = ( str ) => Buffer . from ( str , "binary" ) . toString ( "base64" ) ; g . atob = ( str ) => Buffer . from ( str , "base64" ) . toString ( "binary" ) ; } initGlobals ( ) ; pxt . setAppTarget ( util . testAppTarget ) ; const casesDir = path . join ( process . cwd ( ) , "tests" , "runtime-trace-tests" , "cases" ) ; describe ( "convert between ts<->py " , ( ) => { let tsAndPyFiles ; pxsim . initCurrentRuntime = pxsim . initBareRuntime ; cleanup ( ) ; let tsFiles = util . getFilesByExt ( casesDir , ".ts" ) ; let pyFiles = util . getFilesByExt ( casesDir , ".py" ) ; const isDisabled = ( file ) : boolean => path . basename ( file ) . indexOf ( "TODO" ) >= 0 ; tsAndPyFiles = tsFiles . concat ( pyFiles ) . filter ( f => ! isDisabled ( f ) ) ; const isWhitelisted = ( file ) : boolean => path . basename ( file ) . indexOf ( "ONLY" ) >= 0 ; let whitelisted = tsAndPyFiles . filter ( isWhitelisted ) ; if ( whitelisted . length ) tsAndPyFiles = whitelisted ; tsAndPyFiles . forEach ( file => { it ( "should preserve the runtime semantics of " + path . basename ( file ) , async function ( ) { this . timeout ( 10000 ) ; await testTsOrPy ( file ) ; return ; } ) ; } ) ; } ) ; function writeFileStringSync ( filePath , content ) { fs . writeFileSync ( filePath , content ) ; } async function testTsOrPy ( tsOrPyFile ) { let ext = path . extname ( tsOrPyFile ) ; let isPy = ext === ".py" ; let isTs = ext === ".ts" ; if ( ! isPy && ! isTs ) return Promise . reject ( new Error ( "Invald non-.py, non-.ts file: " + tsOrPyFile ) ) ; let baselineFile ; let baseline ; let recordBaseline = ( bl ) => { baseline = bl ; baselineFile = tsOrPyFile + ".baseline" ; writeFileStringSync ( baselineFile , baseline ) ; } ; if ( isPy ) { let pyFile = tsOrPyFile ; recordBaseline ( await PY ( pyFile ) ) ; let tsFile = await testPy2Ts ( pyFile ) ; await testSts ( tsFile ) ; let pyFile2 = await testTs2Py ( tsFile ) ; } else { let tsFile = tsOrPyFile ; recordBaseline ( await TS ( tsFile ) ) ; await testSts ( tsFile ) ; let pyFile = await testTs2Py ( tsFile ) ; } return ; type Trace = string ; type File = string ; function STS ( tsFile ) { return compileAndRunStsAsync ( tsFile ) ; } function TS ( tsFile ) { return compileAndRunTs ( tsFile ) ; } function PY ( pyFile ) { return runPyAsync ( pyFile ) ; } function TS2PY ( tsFile ) { return convertTs2Py ( tsFile ) ; } function PY2TS ( pyFile ) { return convertPy2Ts ( pyFile ) ; } async function testPy2Ts ( pyFile ) { return testConversion ( pyFile , true ) ; } async function testTs2Py ( tsFile ) { return testConversion ( tsFile , false ) ; } async function testConversion ( inFile , isPy ) { let convert = isPy ? PY2TS : TS2PY ; let runConverted = isPy ? TS : PY ; let fnName = isPy ? "py2ts" : "ts2py" ; let errFile = inFile + `.${ fnName }_error` ; return convert ( inFile ) . catch ( e => { writeFileStringSync ( errFile , JSON . stringify ( e ) ) ; return `${ fnName } failed to convert '${ inFile }'. Error saved at:\n${ errFile }\nError is:\n${ e }\n` ; } ) . then ( async ( outFile ) => { let outTrace = await runConverted ( outFile ) ; if ( ! util . compareBaselines ( outTrace , baseline ) ) { writeFileStringSync ( errFile , outTrace ) ; return Promise . reject ( new Error ( `${ fnName } incorrectly converted:\n` + `${ inFile }\n${ fs . readFileSync ( inFile , "utf8" ) }\nto:\n${ outFile }\n${ fs . readFileSync ( outFile , "utf8" ) }\n` + `Trace mismatch with baseline. Baseline:\n${ baseline }\nIncorrect trace:\n${ outTrace }\n` + `Diff conversion with:\ncode --diff ${ inFile } ${ outFile }\n` + `Diff traces with:\ncode --diff ${ baselineFile } ${ errFile }\n` ) ) ; } return outFile ; } ) ; } async function testSts ( tsFile ) { let errFile = tsFile + ".sts_error" ; return STS ( tsFile ) . catch ( error => { let errStr = `${ error }` ; writeFileStringSync ( errFile , errStr ) ; return Promise . reject ( new Error ( `Static Typescript failed to run on:\n${ tsFile }\nError saved at:\n${ errFile }\nError is:\n${ errStr }\n` ) ) ; } ) . then ( outTrace => { if ( ! util . compareBaselines ( outTrace , baseline ) ) { writeFileStringSync ( errFile , outTrace ) ; return Promise . reject ( new Error ( `Static Typescript produced a different trace than node.js when run on:\n${ tsFile }\n` + `Baseline:\n${ baseline }\nIncorrect trace:\n${ outTrace }\n` + `Diff traces with:\ncode --diff ${ baselineFile } ${ errFile }\n` ) ) ; } return Promise . resolve ( ) ; } ) ; } } function removeBySubstring ( dir , sub ) { return fs . readdirSync ( dir ) . filter ( f => f . indexOf ( sub ) >= 0 ) . map ( f => path . join ( dir , f ) ) . forEach ( f => fs . unlinkSync ( f ) ) ; } function cleanup ( ) { removeBySubstring ( casesDir , ".js" ) ; removeBySubstring ( casesDir , ".py.ts" ) ; removeBySubstring ( casesDir , ".ts.py" ) ; removeBySubstring ( casesDir , ".py2ts_error" ) ; removeBySubstring ( casesDir , ".ts2py_error" ) ; removeBySubstring ( casesDir , ".sts_error" ) ; removeBySubstring ( casesDir , ".baseline" ) ; } function runProcAsync ( cmd , stdin ? ) { return new Promise ( ( resolve , reject ) => { let e = exec ( cmd , ( err , stdout , stderr ) => { let trace = "" ; if ( stdout ) trace += stdout ; if ( stderr ) trace += stderr ; resolve ( trace ) ; } ) ; if ( stdin ) { e . stdin . write ( stdin ) ; e . stdin . end ( ) ; } } ) ; } function runPyAsync ( pyFile ) { let pyBody = fs . readFileSync ( pyFile , "utf8" ) ; pyBody = pyBody . replace ( /: \((.*?)\) -> ([\w]+)/gi , function ( f , m1 , m2 ) { return ": Callable[[" + m1 + "], " + m2 + "]" ; } ) ; const prelude = `
from typing import *
number = Any
class Enum:
    pass
# end prelude
    ` ; let pyStr = `${ prelude }\n${ pyBody }` ; let escapedStr = `<<"EOF"\n${ pyStr }\nEOF` ; let cmd = `python3 ${ escapedStr }` ; return runProcAsync ( "python3" , pyStr ) ; } function runNodeJsAsync ( nodeArgs ) { return runProcAsync ( `node ${ nodeArgs }` ) ; } async function convertTs2Py ( tsFile ) { const tsCode = fs . readFileSync ( tsFile , "utf8" ) . replace ( /\r\n/g , "\n" ) ; let pyCode = await util . ts2pyAsync ( tsCode , null , false , tsFile ) ; const pyFile = path . join ( util . replaceFileExtension ( tsFile , ".ts.py" ) ) ; writeFileStringSync ( pyFile , pyCode ) ; return pyFile ; } async function convertPy2Ts ( pyFile ) { const pyCode = fs . readFileSync ( pyFile , "utf8" ) . replace ( /\r\n/g , "\n" ) ; let tsCode = await util . py2tsAsync ( pyCode , null , false , true , pyFile ) ; const tsFile = path . join ( util . replaceFileExtension ( pyFile , ".py.ts" ) ) ; writeFileStringSync ( tsFile , tsCode . ts ) ; return tsFile ; } function emitJsFiles ( prog , file ? ) { let jsFiles = [ ] ; prog . emit ( file , ( f , data ) => { writeFileStringSync ( f , data ) ; jsFiles . push ( f ) ; } ) ; return jsFiles ; } function compileTsToJs ( files ) { let cOpts = { noEmitOnError : true , noImplicitAny : true , target : ts . ScriptTarget . ES5 , module : ts . ModuleKind . ES2015 , noLib : true , skipLibCheck : true , types : [ ] , } ; let allFiles = [ ... files , path . resolve ( "pxtcompiler/ext-typescript/lib/lib.d.ts" ) ] ; return ts . pxtc . plainTscCompileFiles ( allFiles , cOpts ) ; } async function compileAndRunTs ( filename ) { let prog = compileTsToJs ( [ filename ] ) ; let diagnostics = ts . pxtc . getProgramDiagnostics ( prog ) ; let diagMsgs = diagnostics . map ( ts . pxtc . getDiagnosticString ) ; if ( diagMsgs . length ) return diagMsgs . join ( "\n" ) ; else { let fileSrc = prog . getSourceFile ( path . basename ( filename ) ) ; let jsFiles = emitJsFiles ( prog , fileSrc ) ; let nodejsCmdLine = jsFiles . join ( ", " ) ; let trace = await runNodeJsAsync ( nodejsCmdLine ) ; return trace ; } } function compileAndRunStsAsync ( filename ) { const prelude = `
// end prelude
    ` ; const postlude = `
// start postlude
pause(300);
    ` ; let body = fs . readFileSync ( filename , "utf8" ) ; let tsMain = [ prelude , body , postlude ] . join ( "\n" ) ; return util . stsAsync ( tsMain ) . then ( ( compiled ) => { return runStsAsync ( compiled ) ; } ) ; } function runStsAsync ( res ) { return new Promise < string > ( ( resolve , reject ) => { let f = res . outfiles [ pxtc . BINARY_JS ] ; if ( f ) { let timeout = setTimeout ( ( ) => { reject ( new Error ( "Simulating code timed out" ) ) ; } , 5000 ) ; let r = new pxsim . Runtime ( { type : "run" , code : f } ) ; r . errorHandler = ( e ) => { clearTimeout ( timeout ) ; reject ( e ) ; } ; let trace = "" ; pxsim . Runtime . messagePosted = ( msg ) => { if ( msg . type === "bulkserial" ) { let smsg = msg as pxsim . SimulatorBulkSerialMessage ; for ( let m of smsg . data ) trace += `${ m . data }\n` ; } else if ( msg . type === "status" ) { } else { trace += `\nUNKNOWN: ${ msg . type }\n` ; } } ; r . run ( ( ) => { clearTimeout ( timeout ) ; pxsim . dumpLivePointers ( ) ; resolve ( trace ) ; } ) ; } else { reject ( new Error ( "No compiled js" ) ) ; } } ) ; }