const DEBUG_BREAK_AFTER_INSERTING_SNIPPET = false ; const LOG_DOC_TEXT_BEFORE_AND_AFTER_SNIPPET_INSERTION = false ; import * as assert from 'assert' ; import * as fse from 'fs-extra' ; import { Context } from 'mocha' ; import * as path from 'path' ; import * as stripJsonComments from 'strip-json-comments' ; import { commands , Selection , Uri , window , workspace } from "vscode" ; import { DeploymentTemplateDoc , getVSCodePositionFromPosition , ISnippet , SnippetManager } from '../../extension.bundle' ; import { assertEx } from '../support/assertEx' ; import { delay } from '../support/delay' ; import { diagnosticSources , getDiagnosticsForDocument , IGetDiagnosticsOptions } from '../support/diagnostics' ; import { formatDocumentAndWait } from '../support/formatDocumentAndWait' ; import { getTempFilePath } from "../support/getTempFilePath" ; import { removeComments } from '../support/removeComments' ; import { resolveInTestFolder } from '../support/resolveInTestFolder' ; import { simulateCompletion } from '../support/simulateCompletion' ; import { stringify } from '../support/stringify' ; import { writeToLog } from '../support/testLog' ; import { UseRealSnippets } from '../support/TestSnippets' ; import { RequiresLanguageServer } from '../support/testWithLanguageServer' ; import { testWithPrep } from '../support/testWithPrep' ; const resultsFolder = path . join ( __dirname , '..' , '..' , '..' , 'test' , 'snippets' , 'results' ) ; const expectedFolder = path . join ( __dirname , '..' , '..' , '..' , 'test' , 'snippets' , 'expected' ) ; let resourceTemplate = `{
\t"resources": [
\t\t//Insert here: resource
\t],
\t"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
\t"contentVersion": "1.0.0.0",
\t"variables": {
\t\t//Insert here: variable
\t},
\t"parameters": {
\t\t//Insert here: parameter
\t},
\t"outputs": {
\t\t//Insert here: output
\t},
\t"functions": [
\t\t{
\t\t\t"namespace": "udf",
\t\t\t"members": {
\t\t\t\t//Insert here: user function
\t\t\t}
\t\t}
\t]
}` ; let resourceTemplateWithLocation = `{
    \t"resources": [
    \t\t//Insert here: resource
    \t],
    \t"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
    \t"contentVersion": "1.0.0.0",
    \t"variables": {
    \t\t//Insert here: variable
    \t},
    \t"parameters": {
    \t\t"location": {
    \t\t\t"type": "string"
    \t\t}
    \t\t//Insert here: parameter
    \t},
    \t"outputs": {
    \t\t//Insert here: output
    \t},
    \t"functions": [
    \t\t{
    \t\t\t"namespace": "udf",
    \t\t\t"members": {
    \t\t\t\t//Insert here: user function
    \t\t\t}
    \t\t}
    \t]
    }` ; let emptyTemplate = `
//Insert here: empty
` ; const overrideSkipTests = { "Tag Section" : true , "Linked Deployment With Relative Path" : true , "Linked Deployment With URI" : true , } ; const overrideTemplateForSnippet = { "Azure Resource Manager (ARM) Template" : emptyTemplate , "Azure Resource Manager (ARM) Template Subscription" : emptyTemplate , "Azure Resource Manager (ARM) Template Management Group" : emptyTemplate , "Azure Resource Manager (ARM) Template Tenant" : emptyTemplate , "Azure Resource Manager (ARM) Parameters Template" : emptyTemplate , "User Function Namespace" : `{
\t"resources": [
\t],
\t"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
\t"contentVersion": "1.0.0.0",
\t"functions": [
\t\t//Insert here: namespace
\t]
}` , "Resource Group" : `{
\t"$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#",
\t"contentVersion": "1.0.0.0",
\t"resources": [
\t\t//Insert here: resource
\t]
}` , "User Function Parameter Definition" : `{
    "functions": [
        {
            "namespace": "udf",
            "members": {
                "func1": {
                    "parameters": [
                        //Insert here
                    ],
                    "output": {
                        "type": "string",
                        "value": "myvalue"
                    }
                }
            }
        }
    ]
}` , } ; const overrideInsertPosition = { "Azure Resource Manager (ARM) Template" : "//Insert here: empty" , "Azure Resource Manager (ARM) Parameters Template" : "//Insert here: empty" , "Azure Resource Manager (ARM) Template Subscription" : "//Insert here: empty" , "Azure Resource Manager (ARM) Template Management Group" : "//Insert here: empty" , "Azure Resource Manager (ARM) Template Tenant" : "//Insert here: empty" , Variable : "//Insert here: variable" , Parameter : "//Insert here: parameter" , Output : "//Insert here: output" , "User Function" : "//Insert here: user function" , "User Function Namespace" : "//Insert here: namespace" , "User Function Parameter Definition" : "//Insert here" } ; const overrideExpectedDiagnostics = { "Application Gateway" : [ `Value must be one of the following types: object` ] , "Application Gateway and Firewall" : [ `Value must be one of the following types: object` ] , "Azure Resource Manager (ARM) Parameters Template" : [ "Template validation failed: Required property 'resources' not found in JSON. Path '', line 7, position 2." , "Unknown schema: https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#" , ] , "Linked Deployment With Relative Path" : [ /Linked template file not found/i ] , "Linked Deployment With URI" : [ /Linked template file not found/i ] , Variable : [ "The variable 'variable1' is never used." ] , Parameter : [ "The parameter 'parameter1' is never used." ] , "User Function" : [ "The user-defined function 'udf.functionname' is never used." , "User-function parameter 'parametername' is never used." ] , "User Function Namespace" : [ "The user-defined function 'namespacename.functionname' is never used." , "User-function parameter 'parametername' is never used." ] , "User Function Parameter Definition" : [ 'Missing required property "$schema"' , "Template validation failed: Required property '$schema' not found in JSON. Path '', line 21, position 1." , "The user-defined function 'udf.func1' is never used." , "User-function parameter 'parameter1' is never used." ] , "Automation Job Schedule" : [ "Value must match the regular expression ^[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}$" ] , "Automation Schedule" : [ "Value must be a valid ISO 8601 datetime" , "Value must be one of the following types: object" ] , "Cosmos DB Mongo Database" : [ "Template validation failed: The template resource 'account-name/mongodb/database-name/collectionName' for type 'Microsoft.WindowsAzure.ResourceStack.Frontdoor.Common.Entities.TemplateGenericProperty`1[System.String]' at line '4' and column '66' has incorrect segment lengths. A nested resource type must have identical number of segments as its resource name. A root resource type must have segment length one greater than its resource name. Please see https://aka.ms/arm-template/#resources for usage details." ] , "DNS Record" : [ "Template validation failed: The template resource 'dnsRecord1' for type 'Microsoft.WindowsAzure.ResourceStack.Frontdoor.Common.Entities.TemplateGenericProperty`1[System.String]' at line '5' and column '42' has incorrect segment lengths. A nested resource type must have identical number of segments as its resource name. A root resource type must have segment length one greater than its resource name. Please see https://aka.ms/arm-template/#resources for usage details." ] , "KeyVault" : [ "Value must match the regular expression ^[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}$" , "Value must match the regular expression ^[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}$" ] , "KeyVault Secret" : [ "Value must match the regular expression ^[a-zA-Z0-9-]{1,127}$" ] , "Network Security Group Rule" : [ "Template validation failed: The template resource 'networkSecurityGroupRuleName' for type 'Microsoft.WindowsAzure.ResourceStack.Frontdoor.Common.Entities.TemplateGenericProperty`1[System.String]' at line '5' and column '67' has incorrect segment lengths. A nested resource type must have identical number of segments as its resource name. A root resource type must have segment length one greater than its resource name. Please see https://aka.ms/arm-template/#resources for usage details." ] , "Route Table Route" : [ "Template validation failed: The template resource 'route-name' for type 'Microsoft.WindowsAzure.ResourceStack.Frontdoor.Common.Entities.TemplateGenericProperty`1[System.String]' at line '5' and column '50' has incorrect segment lengths. A nested resource type must have identical number of segments as its resource name. A root resource type must have segment length one greater than its resource name. Please see https://aka.ms/arm-template/#resources for usage details." ] , "Windows VM Custom Script" : [ "Missing required property \"commandToExecute\"" , ] } ; const overrideIgnoreSchemaValidation = { "Linux VM Custom Script" : true , } ; suite ( "Snippets functional tests" , ( ) => { suite ( "all snippets" , ( ) => { createSnippetTests ( "armsnippets.jsonc" ) ; } ) ; function createSnippetTests ( snippetsFile ) { suite ( snippetsFile , ( ) => { const snippetsFilePath = resolveInTestFolder ( path . join ( '..' , 'assets' , snippetsFile ) ) ; const resourceSnippetsFolderPath = resolveInTestFolder ( path . join ( '..' , 'assets' , 'resourceSnippets' ) ) ; const snippetsFromMainFile = < { [ name ] : ISnippet ; } > JSON . parse ( stripJsonComments ( fse . readFileSync ( snippetsFilePath ) . toString ( ) ) ) ; const resourceSnippets = fse . readdirSync ( resourceSnippetsFolderPath ) . filter ( f => f !== 'README.jsonc' ) . map ( file => file . replace ( /(.*)\.snippet\.json$/ , '$1' ) ) ; const snippetExpectedResultsNames = fse . readdirSync ( resolveInTestFolder ( 'snippets/expected' ) ) . map ( file => file . replace ( /\.snippetresult\.json$/ , '' ) ) ; let snippetNames = Object . getOwnPropertyNames ( snippetsFromMainFile ) . concat ( resourceSnippets ) . concat ( snippetExpectedResultsNames ) ; snippetNames = [ ... new Set ( snippetNames ) ] ; const manager = SnippetManager . createDefault ( ) ; for ( let snippetName of snippetNames ) { if ( ! snippetName . startsWith ( '$' ) ) { for ( let i = 0 ; i < 1 ; ++ i ) { testWithPrep ( `snippet: ${ snippetName }` , [ RequiresLanguageServer . instance , UseRealSnippets . instance ] , async function ( this ) : Promise < void > { const snippets = ( await manager . getAllSnippets ( ) ) ; const snippet = snippets . find ( s => s . name === snippetName ) ; assert ( snippet !== undefined , `Couldn't find snippet ${ snippetName }` ) ; await testSnippet ( this , snippetsFilePath , snippetName , snippet ! ) ; } ) ; } } } } ) ; } async function testSnippet ( testCallbackContext , snippetsPath , snippetName , snippet ) { if ( overrideSkipTests [ snippetName ] ) { testCallbackContext . skip ( ) ; } validateSnippet ( ) ; let defaultTemplate = resourceTemplate ; if ( snippet . insertText . includes ( "parameters('location')" ) ) { defaultTemplate = resourceTemplateWithLocation ; } const template = overrideTemplateForSnippet [ snippetName ] !== undefined ? overrideTemplateForSnippet [ snippetName ] : defaultTemplate ; const expectedDiagnostics = ( overrideExpectedDiagnostics [ snippetName ] || [ ] ) . sort ( ) ; const snippetInsertComment = overrideInsertPosition [ snippetName ] || "//Insert here: resource" ; const snippetInsertIndex = template . indexOf ( snippetInsertComment ) ; const snippetInsertLength = snippetInsertComment . length ; assert ( snippetInsertIndex >= 0 , `Couldn't find location to insert snippet (looking for "${ snippetInsertComment }")` ) ; const fakeDt = new DeploymentTemplateDoc ( template , Uri . file ( "fake template" ) , 0 ) ; const snippetInsertPos = getVSCodePositionFromPosition ( fakeDt . getContextFromDocumentCharacterIndex ( snippetInsertIndex , undefined ) . documentPosition ) ; const snippetInsertEndPos = getVSCodePositionFromPosition ( fakeDt . getContextFromDocumentCharacterIndex ( snippetInsertIndex + snippetInsertLength , undefined ) . documentPosition ) ; const tempPath = getTempFilePath ( `snippet ${ snippetName }` , '.azrm' ) ; fse . writeFileSync ( tempPath , template ) ; let doc = await workspace . openTextDocument ( tempPath ) ; let editor = await window . showTextDocument ( doc ) ; const diagnosticOptions = { ignoreSources : ( overrideIgnoreSchemaValidation [ snippetName ] ) ? [ diagnosticSources . schema ] : [ ] } ; let diagnosticResults = await getDiagnosticsForDocument ( doc , 1 , diagnosticOptions ) ; editor . selection = new Selection ( snippetInsertEndPos , snippetInsertPos ) ; await delay ( 1 ) ; await editor . edit ( e => e . replace ( editor . selection , ' ' ) ) ; diagnosticResults = await getDiagnosticsForDocument ( doc , 2 , diagnosticOptions , diagnosticResults ) ; const docTextBeforeInsertion = doc . getText ( ) ; if ( LOG_DOC_TEXT_BEFORE_AND_AFTER_SNIPPET_INSERTION ) { writeToLog ( `Document before inserting snippet:\n${ docTextBeforeInsertion }` ) ; } await simulateCompletion ( editor , snippet . prefix , undefined ) ; diagnosticResults = await getDiagnosticsForDocument ( editor . document , 3 , diagnosticOptions , diagnosticResults ) ; let messages = diagnosticResults . diagnostics . map ( d => d . message ) . sort ( ) ; if ( DEBUG_BREAK_AFTER_INSERTING_SNIPPET ) { debugger ; } const docTextAfterInsertion = await formatDocumentAndWait ( doc ) ; if ( LOG_DOC_TEXT_BEFORE_AND_AFTER_SNIPPET_INSERTION ) { writeToLog ( `Document after inserting snippet:\n${ docTextAfterInsertion }` ) ; } fse . mkdirpSync ( resultsFolder ) ; const outputPath = path . join ( resultsFolder , `${ snippetName }.snippetresult.json` ) ; fse . writeFileSync ( outputPath , docTextAfterInsertion ) ; validateDocumentWithSnippet ( ) ; assertEx . arraysEqual ( messages , expectedDiagnostics , { } , "Diagnostics mismatch" ) ; const expectedPath = path . join ( expectedFolder , `${ snippetName }.snippetresult.json` ) ; const expected = fse . readFileSync ( expectedPath ) . toString ( ) ; const expectedNormalized = stringify ( JSON . parse ( removeComments ( expected ) ) ) ; const actualNormalized = stringify ( JSON . parse ( removeComments ( docTextAfterInsertion ) ) ) ; assertEx . strictEqual ( actualNormalized , expectedNormalized , { } , `Actual result from ${ outputPath } did not match expected result in ${ expectedPath }` ) ; fse . unlinkSync ( tempPath ) ; await commands . executeCommand ( 'workbench.action.closeAllEditors' ) ; function validateSnippet ( ) { const snippetText = JSON . stringify ( snippet , null , 4 ) ; errorIfTextMatches ( snippetText , /\$\$/ , `Instead of $$ in snippet, use \\$` ) ; errorIfTextMatches ( snippetText , /\${[^0-9]/ , "Snippet placeholder is missing the numeric id, makes it look like a variable to vscode" ) ; } function validateDocumentWithSnippet ( ) { assert ( docTextBeforeInsertion !== docTextAfterInsertion , "No insertion happened?  Document didn't change." ) ; } function errorIfTextMatches ( text , regex , errorMessage ) { const match = text . match ( regex ) ; if ( match ) { assert ( false , `${ errorMessage }.  At "${ text . slice ( match . index ! , match . index ! + 20 ) }..."` ) ; } } } } ) ;