const OVERWRITE = false ; import * as assert from 'assert' ; import * as fs from 'fs' ; import { Context , Suite } from 'mocha' ; import * as os from 'os' ; import * as path from 'path' ; import { commands , Uri } from 'vscode' ; import { parseError } from 'vscode-azureextensionui' ; import { getTempFilePath } from '../support/getTempFilePath' ; import { normalizeString } from '../support/normalizeString' ; import { writeToLog } from '../support/testLog' ; import { DISABLE_SLOW_TESTS } from '../testConstants' ; const tleGrammarSourcePath = path . join ( __dirname , '../../../grammars/arm-expression-string.tmLanguage.json' ) ; export interface IGrammar { preprocess ? : { builtin : string ; [ key ] : string ; } ; [ key ] : unknown ; } interface ITestcase { testString ? : string ; data : ITokenInfo [ ] ; } interface ITokenInfo { text : string ; scopes : string ; colors : { [ key ] : string ; } [ ] ; } const tabSize = 20 ; let unpreprocess ; function unpreprocessScopes ( scopes ) { if ( ! unpreprocess ) { let source = fs . readFileSync ( tleGrammarSourcePath ) . toString ( ) ; let grammar = < IGrammar > JSON . parse ( source ) ; let preprocess = grammar . preprocess || < { [ key ] : string ; } > { } ; unpreprocess = [ ] ; for ( let key of Object . getOwnPropertyNames ( preprocess ) ) { if ( key . startsWith ( "scope-" ) ) { let value = preprocess [ key ] ; unpreprocess . push ( [ new RegExp ( value . replace ( '.' , '\\.' ) , "g" ) , `{{${ key }}}` ] ) ; } } } unpreprocess . forEach ( entry => { let [ regex , replacement ] = entry ; scopes = scopes . replace ( regex , replacement ) ; } ) ; return scopes ; } let longestTestDuration = 0 ; async function assertUnchangedTokens ( testPath , resultPath ) { let start = Date . now ( ) ; try { let shouldHaveInvalidTokens = ! ! testPath . match ( /\.INVALID\./i ) ; let shouldBeArmTemplate = ! testPath . match ( /\.NOT-ARM\./i ) ; let shouldBeExpression = shouldBeArmTemplate && ! testPath . match ( /\.NOT-EXPR\./i ) ; let filePathForReadingTokens = testPath ; if ( shouldBeArmTemplate ) { filePathForReadingTokens = getTempFilePath ( path . basename ( testPath ) , '.azrm' ) ; fs . writeFileSync ( filePathForReadingTokens , fs . readFileSync ( testPath ) ) ; } let rawData = await commands . executeCommand ( '_workbench.captureSyntaxTokens' , Uri . file ( filePathForReadingTokens ) ) ; if ( ! rawData ) { throw new Error ( "_workbench.captureSyntaxTokens failed" ) ; } let data = rawData . map ( d => < ITokenInfo > { text : d . c . trim ( ) , scopes : d . t , colors : d . r } ) . filter ( d => d . text !== "" ) ; let testCases ; let shouldCompareFullScope = ! ! testPath . match ( /\.FULL-SCOPE\./i ) ; const testStartToken = /^\$TEST/ ; for ( let iData = 0 ; iData < data . length ; ++ iData ) { let nBegin = data . findIndex ( ( t , i ) => i >= iData && t . text . match ( testStartToken ) !== null ) ; if ( nBegin < 0 ) { break ; } let dictionaryNestingLevel = getDictionaryNestingLevel ( data [ nBegin ] . scopes ) ; while ( getDictionaryNestingLevel ( data [ nBegin ] . scopes ) === dictionaryNestingLevel ) { nBegin ++ ; } assert ( data [ nBegin ] . text === ':' ) ; nBegin ++ ; if ( data [ nBegin ] . text === ' ' ) { nBegin ++ ; } let nEnd = data . findIndex ( ( t , i ) => i >= nBegin && getDictionaryNestingLevel ( t . scopes ) === dictionaryNestingLevel ) ; if ( nEnd < 0 ) { let { fullScopeString , text } = getTestcaseResults ( [ { testString : '' , data : data . slice ( nBegin ) } ] ) ; assert ( false , `Couldn't find end of test string starting here:\\n${ text }\n${ fullScopeString }` ) ; } nEnd -= 1 ; if ( data [ nEnd ] . text === ',' || data [ nEnd ] . text === '}' ) { -- nEnd ; } assert ( nEnd >= nBegin ) ; if ( testCases === undefined ) { testCases = [ ] ; } let testData = data . slice ( nBegin , nEnd + 1 ) ; let testcase = { testString : `TEST STRING: ${ testData . map ( d => d . text ) . join ( "" ) }` , data : testData } ; testCases . push ( testcase ) ; iData = nEnd ; } testCases = testCases || [ < ITestcase > { data } ] ; let { results : testcaseResults , fullScopeString : resultsFullString , shortScopeString : resultsShortString } = getTestcaseResults ( testCases ) ; let resultsString = shouldCompareFullScope ? resultsFullString : resultsShortString ; let actualResultPath = `${ resultPath }.actual` ; let resultPathToWriteTo = OVERWRITE ? resultPath : actualResultPath ; let removeActualResultPath = false ; if ( fs . existsSync ( resultPath ) ) { let previousResult = normalizeString ( fs . readFileSync ( resultPath ) . toString ( ) . trim ( ) ) ; let isJustDiff = false ; try { for ( let testcaseResult of testcaseResults ) { if ( shouldHaveInvalidTokens ) { assert ( testcaseResult . includes ( 'invalid.illegal' ) , "This test's filename contains '.INVALID.', and so should have had at least one invalid token in each testcase result." ) ; } else { assert ( ! testcaseResult . includes ( 'invalid.illegal' ) , "This test's filename does not contain '.INVALID.', but at least one testcase in it contains an invalid token (but shouldn't)." ) ; } if ( shouldBeArmTemplate ) { assert ( testcaseResult . includes ( 'source.json.arm-template' ) , "This test's filename does not contain '.NOT-ARM.', and so every testcase in it should contain at least one source.json.arm-template token." ) ; } else { assert ( ! testcaseResult . includes ( 'source.json.arm-template' ) , "This test's filename contains '.NOT-ARM.', but at least one testcase in it contains an source.json.arm-template token (but shouldn't)." ) ; } let isExpression = testcaseResult . includes ( 'meta.expression.tle.arm-template' ) ; if ( shouldBeExpression ) { assert ( isExpression , "This test's filename does not contain '.NOT-EXPR.', and so every testcase in it should be an ARM expression." ) ; } else { assert ( ! isExpression , "This test's filename contains '.NOT-EXPR.', but at least one testcase in it contains an ARM expression (but shouldn't)." ) ; } } isJustDiff = true ; assert . equal ( resultsString , previousResult ) ; removeActualResultPath = true ; } catch ( e ) { let nonDiffError = isJustDiff ? "" : `${ parseError ( e ) . message }${ os . EOL }` ; writeToResultsFile ( resultPathToWriteTo , resultsString , shouldCompareFullScope , resultsFullString ) ; if ( OVERWRITE ) { removeActualResultPath = true ; throw new Error ( `${ nonDiffError }*** MODIFIED THE RESULTS FILE (${ resultPathToWriteTo }). VERIFY THE CHANGES BEFORE CHECKING IN!` ) ; } else { fs . writeFileSync ( resultPathToWriteTo , resultsString ) ; throw new Error ( `${ nonDiffError }*** ACTUAL RESULTS ARE IN (${ resultPathToWriteTo }).` ) ; } } } else { writeToResultsFile ( resultPathToWriteTo , resultsString , shouldCompareFullScope , resultsFullString ) ; removeActualResultPath = true ; throw new Error ( `*** NEW RESULTS FILE ${ resultPathToWriteTo }` ) ; } if ( removeActualResultPath && fs . existsSync ( actualResultPath ) ) { fs . unlinkSync ( actualResultPath ) ; } } finally { let stop = Date . now ( ) ; let duration = stop - start ; writeToLog ( `Test duration: ${ duration }` ) ; if ( duration > longestTestDuration ) { longestTestDuration = duration ; } } function writeToResultsFile ( resultPathToWriteTo , resultsString , shouldCompareFullScope , resultsFullString ) { fs . writeFileSync ( resultPathToWriteTo , resultsString ) ; if ( ! shouldCompareFullScope ) { fs . writeFileSync ( resultPathToWriteTo + ".full-scope-result.txt" , resultsFullString ) ; } } } function getDictionaryNestingLevel ( scopes ) { let matches = scopes . match ( /meta\.structure\.dictionary\.value/g ) ; return matches ? matches . length : 0 ; } function getTestcaseResults ( testCases ) { let results = testCases . map ( ( testcase ) : { short : string ; full : string ; } => { let prefix = testcase . testString ? `${ testcase . testString }${ os . EOL }` : "" ; function getLastScope ( scopes ) { let lastSpaceIndex = scopes . lastIndexOf ( " " ) ; return lastSpaceIndex > 0 ? scopes . slice ( lastSpaceIndex + 1 ) : scopes ; } function getTestCaseString ( full ) { return testcase . data . map ( td => { let theText = td . text . trim ( ) ; let padding = tabSize - theText . length ; let scopes = unpreprocessScopes ( full ? td . scopes : getLastScope ( td . scopes ) ) ; if ( padding > 0 ) { return `${ theText }${ " " . repeat ( padding ) }${ scopes }` ; } else { return `${ theText }${ os . EOL }${ " " . repeat ( tabSize ) }${ scopes }` ; } } ) . join ( os . EOL ) ; } let fullTestCaseString = getTestCaseString ( true ) ; let shortTestCaseString = getTestCaseString ( false ) ; return { short : prefix + shortTestCaseString , full : prefix + fullTestCaseString } ; } ) ; let fullScopeString = results . map ( r => r . full ) . join ( `${ os . EOL }${ os . EOL }` ) ; fullScopeString = normalizeString ( fullScopeString . trim ( ) ) ; let shortScopeString = results . map ( r => r . short ) . join ( `${ os . EOL }${ os . EOL }` ) ; shortScopeString = normalizeString ( shortScopeString . trim ( ) ) ; let text = normalizeString ( testCases . map ( tc => tc . data ) . map ( ( tis ) => tis . map ( ti => ti . text ) . join ( '' ) ) . join ( '' ) ) ; return { text , results : results . map ( r => r . full ) , fullScopeString , shortScopeString } ; } suite ( 'TLE colorization' , function ( this ) : void { this . timeout ( 50000 ) ; let testFolder = path . join ( __dirname , '..' , '..' , '..' , 'test' , 'colorization' , 'inputs' ) ; let resultsFolder = path . join ( __dirname , '..' , '..' , '..' , 'test' , 'colorization' , 'results' ) ; let testFiles ; let resultFiles ; if ( ! fs . existsSync ( testFolder ) ) { throw new Error ( `Can't find colorization tests folder ${ testFolder }` ) ; } if ( ! fs . existsSync ( resultsFolder ) ) { fs . mkdirSync ( resultsFolder ) ; } testFiles = fs . readdirSync ( testFolder ) ; assert ( testFiles . length , `Couldn't find any test files in ${ testFolder }` ) ; resultFiles = fs . readdirSync ( resultsFolder ) ; for ( let resultFile of resultFiles ) { if ( resultFile . endsWith ( '.actual' ) || resultFile . endsWith ( '.full-scope-result.txt' ) ) { fs . unlinkSync ( path . join ( resultsFolder , resultFile ) ) ; } } resultFiles = fs . readdirSync ( resultsFolder ) ; let testToResultFileMap = new Map < string , string > ( ) ; let orphanedResultFiles = new Set < string > ( resultFiles ) ; testFiles . forEach ( testFile => { let resultFile = `${ path . basename ( testFile ) }.txt` ; testToResultFileMap . set ( testFile , resultFile ) ; orphanedResultFiles . delete ( resultFile ) ; } ) ; orphanedResultFiles . forEach ( orphanedFile => { test ( `ORPHANED: ${ orphanedFile }` , ( ) => { throw new Error ( `Orphaned result file ${ orphanedFile }` ) ; } ) ; } ) ; testFiles . forEach ( testFile => { if ( testFile . startsWith ( 'TO' + 'DO' ) ) { test ( testFile ) ; } else { test ( testFile , async function ( this ) : Promise < void > { if ( DISABLE_SLOW_TESTS ) { this . skip ( ) ; } await assertUnchangedTokens ( path . join ( testFolder , testFile ) , path . join ( resultsFolder , testToResultFileMap . get ( testFile ) ! ) ) ; } ) ; } } ) ; test ( "Check longest test duration" , ( ) => { writeToLog ( `Longest colorization test duration: ${ longestTestDuration / 1000 }s` ) ; } ) ; } ) ;