diff --git a/.github/actions/templates/avm-getModuleTestFiles/action.yml b/.github/actions/templates/avm-getModuleTestFiles/action.yml index bcfc36b307..2e0a37ebe2 100644 --- a/.github/actions/templates/avm-getModuleTestFiles/action.yml +++ b/.github/actions/templates/avm-getModuleTestFiles/action.yml @@ -21,17 +21,17 @@ runs: # Grouping task logs Write-Output '::group::Get parameter files' # Load used functions - . (Join-Path $env:GITHUB_WORKSPACE 'avm' 'utilities' 'pipelines' 'sharedScripts' 'Get-ModuleTestFileList.ps1') - $functionInput = @{ - ModulePath = Join-Path $env:GITHUB_WORKSPACE '${{ inputs.modulePath }}' - } + # Get the list of parameter file paths + $moduleFolderPath = Join-Path $env:GITHUB_WORKSPACE '${{ inputs.modulePath }}' + $testFilePaths = (Get-ChildItem -Path $moduleFolderPath -Recurse -Filter 'main.test.bicep').FullName | Sort-Object - Write-Verbose "Invoke task with" -Verbose - Write-Verbose ($functionInput | ConvertTo-Json | Out-String) -Verbose + $testFilePaths = $testFilePaths | ForEach-Object { + $_.Replace($moduleFolderPath, '').Trim('\').Trim('/') + } - # Get the list of parameter file paths - $testFilePaths = Get-ModuleTestFileList @functionInput -Verbose + Write-Verbose 'Found module test files' -Verbose + $testFilePaths | ForEach-Object { Write-Verbose "- [$_]" -Verbose } # Output values to be accessed by next jobs $compressedOutput = $testFilePaths | ConvertTo-Json -Compress diff --git a/avm/utilities/pipelines/sharedScripts/Get-ModuleTestFileList.ps1 b/avm/utilities/pipelines/sharedScripts/Get-ModuleTestFileList.ps1 deleted file mode 100644 index 252f90aafb..0000000000 --- a/avm/utilities/pipelines/sharedScripts/Get-ModuleTestFileList.ps1 +++ /dev/null @@ -1,61 +0,0 @@ -<# -.SYNOPSIS -Get the relative file paths of all test files in the given module. - -.DESCRIPTION -Get the relative file paths of all test files (*.json / main.test.bicep) in the given module. -The relative path is returned instead of the full one to make paths easier to read in the pipeline. - -.PARAMETER ModulePath -Mandatory. The module path to search in. - -.PARAMETER SearchFolder -Optional. The folder to search for files in - -.PARAMETER TestFilePattern -Optional. The pattern of test files to search for. For example '*.json' - -.EXAMPLE -Get-ModuleTestFileList -ModulePath 'C:\ResourceModules\modules\compute\virtual-machine' - -Returns the relative file paths of all test files of the virtual-machine module in the default test folder ('tests'). - -.EXAMPLE -Get-ModuleTestFileList -ModulePath 'C:\ResourceModules\modules\compute\virtual-machine' -SearchFolder 'parameters' - -Returns the relative file paths of all test files of the virtual-machine module in folder 'parameters'. -#> -function Get-ModuleTestFileList { - - [CmdletBinding()] - param ( - [Parameter(Mandatory)] - [string] $ModulePath, - - [Parameter(Mandatory = $false)] - [string] $SearchFolder = 'tests/e2e', - - [Parameter(Mandatory = $false)] - [string[]] $TestFilePattern = @('main.test.bicep') - ) - - $deploymentTests = @() - if (Test-Path (Join-Path $ModulePath $SearchFolder)) { - $deploymentTests += (Get-ChildItem -Path (Join-Path $ModulePath $SearchFolder) -Recurse -Include $TestFilePattern -File).FullName | Where-Object { - $_ -ne (Join-Path (Join-Path $ModulePath $SearchFolder) 'main.test.bicep') # Excluding PBR test file - } - } - - if (-not $deploymentTests) { - throw "No deployment test files found for module [$ModulePath]" - } - - $deploymentTests = $deploymentTests | ForEach-Object { - $_.Replace($ModulePath, '').Trim('\').Trim('/') - } - - Write-Verbose 'Found parameter files' - $deploymentTests | ForEach-Object { Write-Verbose "- $_" } - - return $deploymentTests -} diff --git a/avm/utilities/pipelines/sharedScripts/Set-ModuleReadMe.ps1 b/avm/utilities/pipelines/sharedScripts/Set-ModuleReadMe.ps1 index d253e6f428..41566dae9a 100644 --- a/avm/utilities/pipelines/sharedScripts/Set-ModuleReadMe.ps1 +++ b/avm/utilities/pipelines/sharedScripts/Set-ModuleReadMe.ps1 @@ -1173,7 +1173,7 @@ function Set-UsageExamplesSection { $moduleNameCamelCase = $First.Tolower() + (Get-Culture).TextInfo.ToTitleCase($Rest) -Replace '-' } - $testFilePaths = Get-ModuleTestFileList -ModulePath $moduleRoot | ForEach-Object { Join-Path $moduleRoot $_ } + $testFilePaths = (Get-ChildItem -Path $ModuleRoot -Recurse -Filter 'main.test.bicep').FullName | Sort-Object $RequiredParametersList = $TemplateFileContent.parameters.Keys | Where-Object { Get-IsParameterRequired -TemplateFileContent $TemplateFileContent -Parameter $TemplateFileContent.parameters[$_] @@ -1627,7 +1627,6 @@ function Set-ModuleReadMe { # Load external functions . (Join-Path $PSScriptRoot 'Get-NestedResourceList.ps1') - . (Join-Path $PSScriptRoot 'Get-ModuleTestFileList.ps1') . (Join-Path $PSScriptRoot 'helper' 'Merge-FileWithNewContent.ps1') . (Join-Path $PSScriptRoot 'helper' 'Get-IsParameterRequired.ps1') . (Join-Path $PSScriptRoot 'helper' 'Get-SpecsAlignedResourceName.ps1') diff --git a/avm/utilities/pipelines/staticValidation/compliance/helper/helper.psm1 b/avm/utilities/pipelines/staticValidation/compliance/helper/helper.psm1 index b32a6a1e9d..305fb572de 100644 --- a/avm/utilities/pipelines/staticValidation/compliance/helper/helper.psm1 +++ b/avm/utilities/pipelines/staticValidation/compliance/helper/helper.psm1 @@ -5,7 +5,6 @@ $repoRootPath = (Get-Item $PSScriptRoot).Parent.Parent.Parent.Parent.Parent.Pare . (Join-Path $repoRootPath 'avm' 'utilities' 'pipelines' 'sharedScripts' 'Get-NestedResourceList.ps1') . (Join-Path $repoRootPath 'avm' 'utilities' 'pipelines' 'sharedScripts' 'Get-ScopeOfTemplateFile.ps1') -. (Join-Path $repoRootPath 'avm' 'utilities' 'pipelines' 'sharedScripts' 'Get-ModuleTestFileList.ps1') . (Join-Path $repoRootPath 'avm' 'utilities' 'pipelines' 'sharedScripts' 'Get-PipelineFileName.ps1') . (Join-Path $repoRootPath 'avm' 'utilities' 'pipelines' 'sharedScripts' 'helper' 'Get-IsParameterRequired.ps1') . (Join-Path $repoRootPath 'avm' 'utilities' 'pipelines' 'sharedScripts' 'helper' 'ConvertTo-OrderedHashtable.ps1') diff --git a/avm/utilities/pipelines/staticValidation/compliance/module.tests.ps1 b/avm/utilities/pipelines/staticValidation/compliance/module.tests.ps1 index a7bb93b160..097acbce2a 100644 --- a/avm/utilities/pipelines/staticValidation/compliance/module.tests.ps1 +++ b/avm/utilities/pipelines/staticValidation/compliance/module.tests.ps1 @@ -20,12 +20,9 @@ $script:RgDeploymentSchema = 'https://schema.management.azure.com/schemas/2019-0 $script:SubscriptionDeploymentSchema = 'https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#' $script:MgDeploymentSchema = 'https://schema.management.azure.com/schemas/2019-08-01/managementGroupDeploymentTemplate.json#' $script:TenantDeploymentSchema = 'https://schema.management.azure.com/schemas/2019-08-01/tenantDeploymentTemplate.json#' -$script:moduleFolderPaths = $moduleFolderPaths $script:telemetryResCsvLink = 'https://aka.ms/avm/index/bicep/res/csv' $script:telemetryPtnCsvLink = 'https://aka.ms/avm/index/bicep/ptn/csv' - -# For runtime purposes, we cache the compiled template in a hashtable that uses a formatted relative module path as a key -$script:convertedTemplates = @{} +$script:moduleFolderPaths = $moduleFolderPaths # Shared exception messages $script:bicepTemplateCompilationFailedException = "Unable to compile the main.bicep template's content. This can happen if there is an error in the template. Please check if you can run the command ``bicep build {0} --stdout | ConvertFrom-Json -AsHashtable``." # -f $templateFilePath @@ -34,6 +31,24 @@ $script:templateNotFoundException = 'No template file found in folder [{0}]' # - # Import any helper function used in this test script Import-Module (Join-Path $PSScriptRoot 'helper' 'helper.psm1') -Force +# Building all required files for tests to optimize performance (using thread-safe multithreading) to consume later +# Collecting paths +$pathsToBuild = [System.Collections.ArrayList]@() +$pathsToBuild += $moduleFolderPaths | ForEach-Object { Join-Path $_ 'main.bicep' } +foreach ($moduleFolderPath in $moduleFolderPaths) { + if ($testFilePaths = ((Get-ChildItem -Path $moduleFolderPath -Recurse -Filter 'main.test.bicep').FullName | Sort-Object)) { + $pathsToBuild += $testFilePaths + } +} + +# building paths +$builtTestFileMap = [System.Collections.Concurrent.ConcurrentDictionary[string, object]]::new() +$pathsToBuild | ForEach-Object -Parallel { + $dict = $using:builtTestFileMap + $builtTemplate = bicep build $_ --stdout | ConvertFrom-Json -AsHashtable + $null = $dict.TryAdd($_, $builtTemplate) +} + Describe 'File/folder tests' -Tag 'Modules' { Context 'General module folder tests' { @@ -238,33 +253,12 @@ Describe 'Module tests' -Tag 'Module' { foreach ($moduleFolderPath in $moduleFolderPaths) { - # For runtime purposes, we cache the compiled template in a hashtable that uses a formatted relative module path as a key - $moduleFolderPathKey = $moduleFolderPath.Replace('\', '/').Split('/avm/')[1].Trim('/').Replace('/', '-') - if (-not ($convertedTemplates.Keys -contains $moduleFolderPathKey)) { - if (Test-Path (Join-Path $moduleFolderPath 'main.bicep')) { - $templateFilePath = Join-Path $moduleFolderPath 'main.bicep' - $templateContent = bicep build $templateFilePath --stdout | ConvertFrom-Json -AsHashtable - - if (-not $templateContent) { - throw ($bicepTemplateCompilationFailedException -f $templateFilePath) - } - } else { - throw ($templateNotFoundException -f $moduleFolderPath) - } - $convertedTemplates[$moduleFolderPathKey] = @{ - templateFilePath = $templateFilePath - templateContent = $templateContent - } - } else { - $templateContent = $convertedTemplates[$moduleFolderPathKey].templateContent - $templateFilePath = $convertedTemplates[$moduleFolderPathKey].templateFilePath - } - $resourceTypeIdentifier = ($moduleFolderPath -split '[\/|\\]{1}avm[\/|\\]{1}(res|ptn)[\/|\\]{1}')[2] -replace '\\', '/' # avm/res// + $templateFilePath = Join-Path $moduleFolderPath 'main.bicep' $readmeFileTestCases += @{ moduleFolderName = $resourceTypeIdentifier - templateContent = $templateContent + templateContent = $builtTestFileMap[$templateFilePath] templateFilePath = $templateFilePath readMeFilePath = Join-Path -Path $moduleFolderPath 'README.md' } @@ -367,27 +361,8 @@ Describe 'Module tests' -Tag 'Module' { $deploymentFolderTestCases = [System.Collections.ArrayList] @() foreach ($moduleFolderPath in $moduleFolderPaths) { - # For runtime purposes, we cache the compiled template in a hashtable that uses a formatted relative module path as a key - $moduleFolderPathKey = $moduleFolderPath.Replace('\', '/').Split('/avm/')[1].Trim('/').Replace('/', '-') - if (-not ($convertedTemplates.Keys -contains $moduleFolderPathKey)) { - if (Test-Path (Join-Path $moduleFolderPath 'main.bicep')) { - $templateFilePath = Join-Path $moduleFolderPath 'main.bicep' - $templateContent = bicep build $templateFilePath --stdout | ConvertFrom-Json -AsHashtable - - if (-not $templateContent) { - throw ($bicepTemplateCompilationFailedException -f $templateFilePath) - } - } else { - throw ($templateNotFoundException -f $moduleFolderPath) - } - $convertedTemplates[$moduleFolderPathKey] = @{ - templateFilePath = $templateFilePath - templateContent = $templateContent - } - } else { - $templateContent = $convertedTemplates[$moduleFolderPathKey].templateContent - $templateFilePath = $convertedTemplates[$moduleFolderPathKey].templateFilePath - } + $templateFilePath = Join-Path $moduleFolderPath 'main.bicep' + $templateContent = $builtTestFileMap[$templateFilePath] # Parameter file test cases $testFileTestCases = @() @@ -397,11 +372,11 @@ Describe 'Module tests' -Tag 'Module' { if (Test-Path (Join-Path $moduleFolderPath 'tests')) { - # Can be removed after full migration to bicep test files - $moduleTestFilePaths = Get-ModuleTestFileList -ModulePath $moduleFolderPath | ForEach-Object { Join-Path $moduleFolderPath $_ } + # TODO: Can be removed after full migration to bicep test files + $moduleTestFilePaths = (Get-ChildItem -Path $moduleFolderPath -Recurse -Filter 'main.test.bicep').FullName | Sort-Object foreach ($moduleTestFilePath in $moduleTestFilePaths) { - $deploymentFileContent = bicep build $moduleTestFilePath --stdout | ConvertFrom-Json -AsHashtable + $deploymentFileContent = $builtTestFileMap[$moduleTestFilePath] $deploymentTestFile_AllParameterNames = $deploymentFileContent.resources[-1].properties.parameters.Keys | Sort-Object # The last resource should be the test $testFileTestCases += @{ @@ -940,26 +915,8 @@ Describe 'Module tests' -Tag 'Module' { foreach ($moduleFolderPath in $moduleFolderPaths) { $resourceTypeIdentifier = ($moduleFolderPath -split '[\/|\\]{1}avm[\/|\\]{1}(res|ptn)[\/|\\]{1}')[2] -replace '\\', '/' # avm/res// - - # For runtime purposes, we cache the compiled template in a hashtable that uses a formatted relative module path as a key - $moduleFolderPathKey = $moduleFolderPath.Replace('\', '/').Split('/avm/')[1].Trim('/').Replace('/', '-') - if (-not ($convertedTemplates.Keys -contains $moduleFolderPathKey)) { - if (Test-Path (Join-Path $moduleFolderPath 'main.bicep')) { - $templateFilePath = Join-Path $moduleFolderPath 'main.bicep' - $templateContent = bicep build $templateFilePath --stdout | ConvertFrom-Json -AsHashtable - - if (-not $templateContent) { - throw ($bicepTemplateCompilationFailedException -f $templateFilePath) - } - } else { - throw ($templateNotFoundException -f $moduleFolderPath) - } - $convertedTemplates[$moduleFolderPathKey] = @{ - templateContent = $templateContent - } - } else { - $templateContent = $convertedTemplates[$moduleFolderPathKey].templateContent - } + $templateFilePath = Join-Path $moduleFolderPath 'main.bicep' + $templateContent = $builtTestFileMap[$templateFilePath] $metadataFileTestCases += @{ moduleFolderName = $resourceTypeIdentifier @@ -1008,28 +965,8 @@ Describe 'Module tests' -Tag 'Module' { foreach ($moduleFolderPath in $moduleFolderPaths) { $resourceTypeIdentifier = ($moduleFolderPath -split '[\/|\\]{1}avm[\/|\\]{1}(res|ptn)[\/|\\]{1}')[2] -replace '\\', '/' # avm/res// - - # For runtime purposes, we cache the compiled template in a hashtable that uses a formatted relative module path as a key - $moduleFolderPathKey = $moduleFolderPath.Replace('\', '/').Split('/avm/')[1].Trim('/').Replace('/', '-') - if (-not ($convertedTemplates.Keys -contains $moduleFolderPathKey)) { - if (Test-Path (Join-Path $moduleFolderPath 'main.bicep')) { - $templateFilePath = Join-Path $moduleFolderPath 'main.bicep' - $templateContent = bicep build $templateFilePath --stdout | ConvertFrom-Json -AsHashtable - - if (-not $templateContent) { - throw ($bicepTemplateCompilationFailedException -f $templateFilePath) - } - } else { - throw ($templateNotFoundException -f $moduleFolderPath) - } - $convertedTemplates[$moduleFolderPathKey] = @{ - templateContent = $templateContent - templateFilePath = $templateFilePath - } - } else { - $templateContent = $convertedTemplates[$moduleFolderPathKey].templateContent - $templateFilePath = $convertedTemplates[$moduleFolderPathKey].templateFilePath - } + $templateFilePath = Join-Path $moduleFolderPath 'main.bicep' + $templateContent = $builtTestFileMap[$templateFilePath] $udtSpecificTestCases += @{ moduleFolderName = $resourceTypeIdentifier @@ -1198,7 +1135,7 @@ Describe 'Test file tests' -Tag 'TestTemplate' { foreach ($moduleFolderPath in $moduleFolderPaths) { if (Test-Path (Join-Path $moduleFolderPath 'tests')) { - $testFilePaths = Get-ModuleTestFileList -ModulePath $moduleFolderPath | ForEach-Object { Join-Path $moduleFolderPath $_ } + $testFilePaths = (Get-ChildItem -Path $moduleFolderPath -Recurse -Filter 'main.test.bicep').FullName | Sort-Object foreach ($testFilePath in $testFilePaths) { $testFileContent = Get-Content $testFilePath $resourceTypeIdentifier = ($moduleFolderPath -split '[\/|\\]{1}avm[\/|\\]{1}(res|ptn)[\/|\\]{1}')[2] -replace '\\', '/' # avm/res// @@ -1337,28 +1274,8 @@ Describe 'API version tests' -Tag 'ApiCheck' { foreach ($moduleFolderPath in $moduleFolderPaths) { $moduleFolderName = $moduleFolderPath.Replace('\', '/').Split('/avm/')[1] - - # For runtime purposes, we cache the compiled template in a hashtable that uses a formatted relative module path as a key - $moduleFolderPathKey = $moduleFolderPath.Replace('\', '/').Split('/avm/')[1].Trim('/').Replace('/', '-') - if (-not ($convertedTemplates.Keys -contains $moduleFolderPathKey)) { - if (Test-Path (Join-Path $moduleFolderPath 'main.bicep')) { - $templateFilePath = Join-Path $moduleFolderPath 'main.bicep' - $templateContent = bicep build $templateFilePath --stdout | ConvertFrom-Json -AsHashtable - - if (-not $templateContent) { - throw ($bicepTemplateCompilationFailedException -f $templateFilePath) - } - } else { - throw ($templateNotFoundException -f $moduleFolderPath) - } - $convertedTemplates[$moduleFolderPathKey] = @{ - templateFilePath = $templateFilePath - templateContent = $templateContent - } - } else { - $templateContent = $convertedTemplates[$moduleFolderPathKey].templateContent - $templateFilePath = $convertedTemplates[$moduleFolderPathKey].templateFilePath - } + $templateFilePath = Join-Path $moduleFolderPath 'main.bicep' + $templateContent = $builtTestFileMap[$templateFilePath] $nestedResources = Get-NestedResourceList -TemplateFileContent $templateContent | Where-Object { $_.type -notin @('Microsoft.Resources/deployments') -and $_ @@ -1370,7 +1287,7 @@ Describe 'API version tests' -Tag 'ApiCheck' { { $PSItem -like '*diagnosticsettings*' } { $testCases += @{ moduleName = $moduleFolderName - resourceType = 'diagnosticsettings' + resourceType = 'diagnosticSettings' ProviderNamespace = 'Microsoft.Insights' TargetApi = $resource.ApiVersion AvailableApiVersions = $ApiVersions @@ -1392,7 +1309,7 @@ Describe 'API version tests' -Tag 'ApiCheck' { { $PSItem -like '*roleAssignments' } { $testCases += @{ moduleName = $moduleFolderName - resourceType = 'roleassignments' + resourceType = 'roleAssignments' ProviderNamespace = 'Microsoft.Authorization' TargetApi = $resource.ApiVersion AvailableApiVersions = $ApiVersions @@ -1434,16 +1351,16 @@ Describe 'API version tests' -Tag 'ApiCheck' { [string] $ResourceType, [string] $TargetApi, [string] $ProviderNamespace, - [PSCustomObject] $AvailableApiVersions, + [hashtable] $AvailableApiVersions, [bool] $AllowPreviewVersionsInAPITests ) - if (-not (($AvailableApiVersions | Get-Member -Type NoteProperty).Name -contains $ProviderNamespace)) { + if ($AvailableApiVersions.Keys -notcontains $ProviderNamespace) { Write-Warning "[API Test] The Provider Namespace [$ProviderNamespace] is missing in your Azure API versions file. Please consider updating it and if it is still missing to open an issue in the 'AzureAPICrawler' PowerShell module's GitHub repository." Set-ItResult -Skipped -Because "The Azure API version file is missing the Provider Namespace [$ProviderNamespace]." return } - if (-not (($AvailableApiVersions.$ProviderNamespace | Get-Member -Type NoteProperty).Name -contains $ResourceType)) { + if ($AvailableApiVersions.$ProviderNamespace.Keys -notcontains $ResourceType) { Write-Warning "[API Test] The Provider Namespace [$ProviderNamespace] is missing the Resource Type [$ResourceType] in your API versions file. Please consider updating it and if it is still missing to open an issue in the 'AzureAPICrawler' PowerShell module's GitHub repository." Set-ItResult -Skipped -Because "The Azure API version file is missing the Resource Type [$ResourceType] for Provider Namespace [$ProviderNamespace]." return @@ -1468,7 +1385,7 @@ Describe 'API version tests' -Tag 'ApiCheck' { $approvedApiVersions = $approvedApiVersions | Sort-Object -Unique -Descending - if ( $approvedApiVersions -notcontains $TargetApi) { + if ($approvedApiVersions -notcontains $TargetApi) { # Using a warning now instead of an error, as we don't want to block PRs for this. Write-Warning ("The used API version [$TargetApi] is not one of the most recent 5 versions. Please consider upgrading to one of the following: {0}" -f $approvedApiVersions -join ', ') diff --git a/avm/utilities/tools/Test-ModuleLocally.ps1 b/avm/utilities/tools/Test-ModuleLocally.ps1 index 7d23be4b67..d2a5f612f9 100644 --- a/avm/utilities/tools/Test-ModuleLocally.ps1 +++ b/avm/utilities/tools/Test-ModuleLocally.ps1 @@ -151,7 +151,7 @@ function Test-ModuleLocally { [string] $ModuleTestFilePath = (Join-Path (Split-Path $TemplateFilePath -Parent) '.test'), [Parameter(Mandatory = $false)] - [string] $PesterTestFilePath = 'utilities/pipelines/staticValidation/module.tests.ps1', + [string] $PesterTestFilePath = 'avm/utilities/pipelines/staticValidation/compliance/module.tests.ps1', [Parameter(Mandatory = $false)] [Psobject] $ValidateOrDeployParameters = @{}, @@ -173,7 +173,7 @@ function Test-ModuleLocally { ) begin { - $repoRootPath = (Get-Item $PSScriptRoot).Parent.Parent + $repoRootPath = (Get-Item $PSScriptRoot).Parent.Parent.Parent.FullName $ModuleName = Split-Path (Split-Path $TemplateFilePath -Parent) -Leaf $utilitiesFolderPath = Split-Path $PSScriptRoot -Parent Write-Verbose "Running local tests for [$ModuleName]"