diff --git a/private/functions/Get-ObjectNameParts.ps1 b/private/functions/Get-ObjectNameParts.ps1 index 1ff05cfac763..625610f2bc63 100644 --- a/private/functions/Get-ObjectNameParts.ps1 +++ b/private/functions/Get-ObjectNameParts.ps1 @@ -110,13 +110,17 @@ function Get-ObjectNameParts { } if ($fixSchema) { - $dbName = $dbName.Replace($fixSchema, '') + if ($dbName) { + $dbName = $dbName.Replace($fixSchema, '') + } if ($schema -eq $fixSchema) { $schema = $null - } else { - $schema = $dbName.Replace($fixSchema, '') + } elseif ($schema) { + $schema = $schema.Replace($fixSchema, '') + } + if ($name) { + $name = $name.Replace($fixSchema, '') } - $name = $name.Replace($fixSchema, '') } $fqtns = [PSCustomObject] @{ diff --git a/public/Export-DbaBinaryFile.ps1 b/public/Export-DbaBinaryFile.ps1 index f99931e9d2b7..1694d00d0ae5 100644 --- a/public/Export-DbaBinaryFile.ps1 +++ b/public/Export-DbaBinaryFile.ps1 @@ -1,7 +1,7 @@ function Export-DbaBinaryFile { <# .SYNOPSIS - Extracts binary data from SQL Server tables and writes it to physical files + Extracts binary data from SQL Server tables and writes it to physical files. .DESCRIPTION Retrieves binary data stored in SQL Server tables and writes it as files to the filesystem. This is useful for extracting documents, images, or other files that have been stored in database columns using binary, varbinary, or image datatypes. diff --git a/tests/Get-ObjectNameParts.Tests.ps1 b/tests/Get-ObjectNameParts.Tests.ps1 index 726b04b6c217..58827f4d1071 100644 --- a/tests/Get-ObjectNameParts.Tests.ps1 +++ b/tests/Get-ObjectNameParts.Tests.ps1 @@ -1,64 +1,74 @@ -$CommandName = $MyInvocation.MyCommand.Name.Replace(".Tests.ps1", "") -Write-Host -Object "Running $PSCommandPath" -ForegroundColor Cyan -$global:TestConfig = Get-TestConfig -. "$PSScriptRoot\..\private\functions\Get-DirectoryRestoreFile.ps1" +#Requires -Module @{ ModuleName="Pester"; ModuleVersion="5.0" } +param( + $ModuleName = "dbatools", + $CommandName = "Get-ObjectNameParts", + $PSDefaultParameterValues = $TestConfig.Defaults +) -Describe "$CommandName Unit Tests" -Tag 'UnitTests' { - Context "Validate parameters" { - [object[]]$params = (Get-Command $CommandName).Parameters.Keys | Where-Object { $_ -notin ('whatif', 'confirm') } - [object[]]$knownParameters = 'ObjectName' - It "Should only contain our specific parameters" { - (@(Compare-Object -ReferenceObject ($knownParameters | Where-Object { $_ }) -DifferenceObject $params).Count ) | Should Be 0 +. "$PSScriptRoot\..\private\functions\Get-ObjectNameParts.ps1" + +Describe $CommandName -Tag UnitTests { + Context 'Parameter validation' { + BeforeAll { + $command = Get-Command $CommandName + $hasParameters = $command.Parameters.Keys | Where-Object { $PSItem -notin ('whatif', 'confirm') } + $expectedParameters = @( + 'ObjectName' + ) + } + + It 'Should have the expected parameters' { + Compare-Object -ReferenceObject $expectedParameters -DifferenceObject $hasParameters | Should -BeNullOrEmpty } } } -Describe "$CommandName Integration Tests" -Tag 'IntegrationTests' { - Context "Test one part names" { - It "Should return correct parts" { +Describe $CommandName -Tag IntegrationTests { + Context 'Test one part names' { + It 'Should return correct parts' { $objectName = 'table1', '[table2]', '[tab..le3]', '[table]]x4]', '[table5]]]' - $table = 'table1', 'table2', 'tab..le3', 'table]]x4', 'table5]]' - for ($i = 0; $i -lt $input.Count; $i++) { + $table = 'table1', 'table2', 'tab..le3', 'table]x4', 'table5]' + for ($i = 0; $i -lt $objectName.Count; $i++) { $result = Get-ObjectNameParts -ObjectName $objectName[$i] - $result.Parsed | Should Be $true - $result.Database | Should Be $null - $result.Schema | Should Be $null - $result.Name | Should Be $table[$i] + $result.Parsed | Should -Be $true + $result.Database | Should -BeNull + $result.Schema | Should -BeNull + $result.Name | Should -Be $table[$i] } } } - Context "Test two part names" { - It "Should return correct parts" { - $objectName = 'schema1.table1', '[sche..ma2].[table2]', 'schema3.[tab..le3]', '[schema4].[table]]x4]', 'schema5.[table5]]]' - $table = 'table1', 'table2', 'tab..le3', 'table]]x4', 'table5]]' + Context 'Test two part names' { + It 'Should return correct parts' { + $objectName = 'schema1.table1', '[sche..ma2].[table2]', '[sche ma3].[tab..le3]', '[schema4].[table]]x4]', 'schema5.[table5]]]' + $table = 'table1', 'table2', 'tab..le3', 'table]x4', 'table5]' $schema = 'schema1', 'sche..ma2', 'sche ma3', 'schema4', 'schema5' - for ($i = 0; $i -lt $input.Count; $i++) { + for ($i = 0; $i -lt $objectName.Count; $i++) { $result = Get-ObjectNameParts -ObjectName $objectName[$i] - $result.Parsed | Should Be $true - $result.Database | Should Be $null - $result.Schema | Should Be $schema[$i] - $result.Name | Should Be $table[$i] + $result.Parsed | Should -Be $true + $result.Database | Should -BeNull + $result.Schema | Should -Be $schema[$i] + $result.Name | Should -Be $table[$i] } } } - Context "Test three part names" { - It "Should return correct parts" { + Context 'Test three part names' { + It 'Should return correct parts' { $objectName = 'database1.schema1.table1', 'database2..table2', 'database3..[tab..le3]', 'db4.[sche..ma4].table4' $table = 'table1', 'table2', 'tab..le3', 'table4' $schema = 'schema1', $null, $null, 'sche..ma4' $database = 'database1', 'database2', 'database3', 'db4' - for ($i = 0; $i -lt $input.Count; $i++) { + for ($i = 0; $i -lt $objectName.Count; $i++) { $result = Get-ObjectNameParts -ObjectName $objectName[$i] - $result.Parsed | Should Be $true - $result.Database | Should Be $database[$i] - $result.Schema | Should Be $schema[$i] - $result.Name | Should Be $table[$i] + $result.Parsed | Should -Be $true + $result.Database | Should -Be $database[$i] + $result.Schema | Should -Be $schema[$i] + $result.Name | Should -Be $table[$i] } } } - Context "Test wrong names" { - It "Should not return parts for 'part1.part2.part3.part4'" { - (Get-ObjectNameParts -ObjectName 'part1.part2.part3.part4').Parsed | Should Be $false + Context 'Test wrong names' { + It 'Should not return parts for ''part1.part2.part3.part4''' { + (Get-ObjectNameParts -ObjectName 'part1.part2.part3.part4').Parsed | Should -Be $false } } } \ No newline at end of file diff --git a/tests/InModule.Commands.Tests.ps1 b/tests/InModule.Commands.Tests.ps1 index 9ddf908c5241..76d19b4d0940 100644 --- a/tests/InModule.Commands.Tests.ps1 +++ b/tests/InModule.Commands.Tests.ps1 @@ -1,15 +1,23 @@ -$CommandName = $MyInvocation.MyCommand.Name.Replace(".Tests.ps1", "") -Write-Host -Object "Running $PSCommandPath" -ForegroundColor Cyan -$global:TestConfig = Get-TestConfig +#Requires -Module @{ ModuleName="Pester"; ModuleVersion="5.0" } +param( + $ModuleName = "dbatools", + $CommandName = "InModule.Commands", + $PSDefaultParameterValues = $TestConfig.Defaults +) + +Describe $CommandName -Tag IntegrationTests { + # The original test used Get-TestConfig, but that is now handled by the test runner + # and the results are available in $TestConfig. + # The original test also dynamically set $CommandName, which is now static. -Describe "$commandname Integration Tests" -Tag "IntegrationTests" { Context "duplicate commands are not added" { It "only indexes one instance per command" { # this no longer works in PS 5.1, no idea why, maybe it doesn't like the new test for core or desktop # $commandlist = Import-PowerShellDataFile -Path '$PSScriptRoot\..\dbatools.psd1' - $commandlist = Invoke-Expression (Get-Content '$PSScriptRoot\..\dbatools.psd1' -Raw) + $psd1Path = Join-Path $PSScriptRoot '..\dbatools.psd1' + $commandlist = Invoke-Expression (Get-Content $psd1Path -Raw) $dupes = $commandlist.FunctionsToExport | Group-Object | Where-Object Count -gt 1 - $dupes.Name | Should -be $null + $dupes | Should -BeNullOrEmpty } } } \ No newline at end of file diff --git a/tests/InModule.Help.Tests.ps1 b/tests/InModule.Help.Tests.ps1 index 2b924c55f1bc..a2d7ccda257b 100644 --- a/tests/InModule.Help.Tests.ps1 +++ b/tests/InModule.Help.Tests.ps1 @@ -1,3 +1,10 @@ +#Requires -Module @{ ModuleName="Pester"; ModuleVersion="5.0" } +param( + $ModuleName = "dbatools", + $CommandName = "InModule.Help", + $PSDefaultParameterValues = $TestConfig.Defaults +) + Write-Host -Object "Running $PSCommandPath" -ForegroundColor Cyan <# .NOTES @@ -15,12 +22,12 @@ Write-Host -Object "Running $PSCommandPath" -ForegroundColor Cyan # quit failing appveyor if ($env:appveyor) { $names = @( - 'Microsoft.SqlServer.Management.XEvent', - 'Microsoft.SqlServer.Management.XEventDbScoped', - 'Microsoft.SqlServer.Management.XEventDbScopedEnum', - 'Microsoft.SqlServer.Management.XEventEnum', - 'Microsoft.SqlServer.Replication', - 'Microsoft.SqlServer.Rmo' + "Microsoft.SqlServer.Management.XEvent", + "Microsoft.SqlServer.Management.XEventDbScoped", + "Microsoft.SqlServer.Management.XEventDbScopedEnum", + "Microsoft.SqlServer.Management.XEventEnum", + "Microsoft.SqlServer.Replication", + "Microsoft.SqlServer.Rmo" ) foreach ($name in $names) { @@ -34,170 +41,109 @@ if ($env:appveyor) { if ($SkipHelpTest) { return } . "$PSScriptRoot\InModule.Help.Exceptions.ps1" -$includedNames = (Get-ChildItem "$PSScriptRoot\..\public" | Where-Object Name -like "*.ps1" ).BaseName -$commands = Get-Command -Module (Get-Module dbatools) -CommandType Cmdlet, Function, Workflow | Where-Object Name -in $includedNames - ## When testing help, remember that help is cached at the beginning of each session. ## To test, restart session. +Describe "dbatools Module Help" -Tag "Help" { + BeforeAll { + $includedNames = (Get-ChildItem "$PSScriptRoot\..\public" | Where-Object Name -like "*.ps1").BaseName + $global:commandsWithHelp = Get-Command -Module (Get-Module dbatools) -CommandType Cmdlet, Function, Workflow | Where-Object Name -in $includedNames + } -foreach ($command in $commands) { - $commandName = $command.Name + foreach ($command in $global:commandsWithHelp) { + $commandName = $command.Name - # Skip all functions that are on the exclusions list - if ($global:FunctionHelpTestExceptions -contains $commandName) { continue } + # Skip all functions that are on the exclusions list + if ($global:FunctionHelpTestExceptions -contains $commandName) { continue } - # The module-qualified command fails on Microsoft.PowerShell.Archive cmdlets - $Help = Get-Help $commandName -ErrorAction SilentlyContinue - $testhelperrors = 0 - $testhelpall = 0 - Describe "Test help for $commandName" { + Describe "Help for $commandName" { + BeforeAll { + $Help = Get-Help $commandName -ErrorAction SilentlyContinue + } - $testhelpall += 1 - if ($Help.Synopsis -like '*`[``]*') { - # If help is not found, synopsis in auto-generated help is the syntax diagram It "should not be auto-generated" { - $Help.Synopsis | Should Not BeLike '*`[``]*' + # If help is not found, synopsis in auto-generated help is the syntax diagram + $Help.Synopsis | Should -Not -BeLike "*`[``]*" } - $testhelperrors += 1 - } - $testhelpall += 1 - if ([String]::IsNullOrEmpty($Help.Description.Text)) { - # Should be a description for every function - It "gets description for $commandName" { - $Help.Description | Should Not BeNullOrEmpty + It "should have a description" { + # Should be a description for every function + $Help.Description.Text | Should -Not -BeNullOrEmpty } - $testhelperrors += 1 - } - $testhelpall += 1 - if ([String]::IsNullOrEmpty(($Help.Examples.Example | Select-Object -First 1).Code)) { - # Should be at least one example - It "gets example code from $commandName" { - ($Help.Examples.Example | Select-Object -First 1).Code | Should Not BeNullOrEmpty + It "should have at least one example with code" { + ($Help.Examples.Example | Select-Object -First 1).Code | Should -Not -BeNullOrEmpty } - $testhelperrors += 1 - } - $testhelpall += 1 - if ([String]::IsNullOrEmpty(($Help.Examples.Example.Remarks | Select-Object -First 1).Text)) { - # Should be at least one example description - It "gets example help from $commandName" { - ($Help.Examples.Example.Remarks | Select-Object -First 1).Text | Should Not BeNullOrEmpty + It "should have at least one example with remarks" { + # Should be at least one example description + ($Help.Examples.Example.Remarks | Select-Object -First 1).Text | Should -Not -BeNullOrEmpty } - $testhelperrors += 1 - } - # :-( - $testhelpall += 1 - if ([string]::IsNullOrEmpty($help.relatedLinks.NavigationLink)) { - # Should have a navigation link - It "There should be a navigation link for $commandName" { + # :-) + It "should have a related navigation link" { + # Should have a navigation link $help.relatedLinks.NavigationLink | Should -Not -BeNullOrEmpty -Because "We need a .LINK for Get-Help -Online to work" } - $testhelperrors += 1 - } - # :-( - $testhelpall += 1 - if (-not ([string]::Equals($help.relatedLinks.NavigationLink.uri, "https://dbatools.io/$commandName"))) { - # the link should point to the correct page - It "The link for $commandName should be https://dbatools.io/$commandName" { - $help.relatedLinks[0].NavigationLink.uri | Should -MatchExactly "https://dbatools.io/$commandName" -Because "The web-page should be the one for the command!" + # :-) + It "should have the correct online link" { + # the link should point to the correct page + $help.relatedLinks.NavigationLink[0].uri | Should -Be "https://dbatools.io/$commandName" -Because "The web-page should be the one for the command!" } - $testhelperrors += 1 - } - if ($testhelperrors -eq 0) { - It "Ran silently $testhelpall tests" { - $testhelperrors | Should be 0 - } - } - - $testparamsall = 0 - $testparamserrors = 0 - Context "Test parameter help for $commandName" { - - $Common = 'Debug', 'ErrorAction', 'ErrorVariable', 'InformationAction', 'InformationVariable', 'OutBuffer', 'OutVariable', - 'PipelineVariable', 'Verbose', 'WarningAction', 'WarningVariable' - - $parameters = $command.ParameterSets.Parameters | Sort-Object -Property Name -Unique | Where-Object Name -notin $common - $parameterNames = $parameters.Name - $HelpParameterNames = $Help.Parameters.Parameter.Name | Sort-Object -Unique - foreach ($parameter in $parameters) { - $parameterName = $parameter.Name - $parameterHelp = $Help.parameters.parameter | Where-Object Name -EQ $parameterName - - $testparamsall += 1 - if ([String]::IsNullOrEmpty($parameterHelp.Description.Text)) { - # Should be a description for every parameter - It "gets help for parameter: $parameterName : in $commandName" { - $parameterHelp.Description.Text | Should Not BeNullOrEmpty - } - $testparamserrors += 1 + Context "Parameter help" { + BeforeAll { + $Common = "Debug", "ErrorAction", "ErrorVariable", "InformationAction", "InformationVariable", "OutBuffer", "OutVariable", "PipelineVariable", "Verbose", "WarningAction", "WarningVariable" + $commandParameters = $command.ParameterSets.Parameters | Sort-Object -Property Name -Unique | Where-Object Name -notin $common + $HelpParameterNames = $Help.Parameters.Parameter.Name | Sort-Object -Unique } - $testparamsall += 1 - $codeMandatory = $parameter.IsMandatory.toString() - if ($parameterHelp.Required -ne $codeMandatory) { - # Required value in Help should match IsMandatory property of parameter - It "help for $parameterName parameter in $commandName has correct Mandatory value" { - $parameterHelp.Required | Should Be $codeMandatory - } - $testparamserrors += 1 + It "should not have extra parameters in help" { + $extraParams = $HelpParameterNames | Where-Object { $PSItem -notin $commandParameters.Name } + $extraParams | Should -BeNullOrEmpty } - if ($HelpTestSkipParameterType[$commandName] -contains $parameterName) { continue } - - $codeType = $parameter.ParameterType.Name + foreach ($parameter in $commandParameters) { + Context "for parameter '$($parameter.Name)'" { + BeforeAll { + $parameterName = $parameter.Name + $parameterHelp = $Help.parameters.parameter | Where-Object Name -EQ $parameterName + } - $testparamsall += 1 - if ($parameter.ParameterType.IsEnum) { - # Enumerations often have issues with the typename not being reliably available - $names = $parameter.ParameterType::GetNames($parameter.ParameterType) - if ($parameterHelp.parameterValueGroup.parameterValue -ne $names) { - # Parameter type in Help should match code - It "help for $commandName has correct parameter type for $parameterName" { - $parameterHelp.parameterValueGroup.parameterValue | Should be $names + It "should have a description" { + # Should be a description for every parameter + $parameterHelp.Description.Text | Should -Not -BeNullOrEmpty } - $testparamserrors += 1 - } - } elseif ($parameter.ParameterType.FullName -in $HelpTestEnumeratedArrays) { - # Enumerations often have issues with the typename not being reliably available - $names = [Enum]::GetNames($parameter.ParameterType.DeclaredMembers[0].ReturnType) - if ($parameterHelp.parameterValueGroup.parameterValue -ne $names) { - # Parameter type in Help should match code - It "help for $commandName has correct parameter type for $parameterName" { - $parameterHelp.parameterValueGroup.parameterValue | Should be $names + + It "should have the correct 'Required' value" { + # Required value in Help should match IsMandatory property of parameter + $codeMandatory = $parameter.IsMandatory.ToString() + $parameterHelp.Required | Should -Be $codeMandatory } - $testparamserrors += 1 - } - } else { - # To avoid calling Trim method on a null object. - $helpType = if ($parameterHelp.parameterValue) { $parameterHelp.parameterValue.Trim() } - if ($helpType -ne $codeType ) { - # Parameter type in Help should match code - It "help for $commandName has correct parameter type for $parameterName" { - $helpType | Should be $codeType + + if ($HelpTestSkipParameterType[$commandName] -notcontains $parameter.Name) { + It "should have the correct parameter type" { + # Parameter type in Help should match code + $codeType = $parameter.ParameterType.Name + if ($parameter.ParameterType.IsEnum) { + # Enumerations often have issues with the typename not being reliably available + $names = $parameter.ParameterType::GetNames($parameter.ParameterType) + Compare-Object -ReferenceObject $names -DifferenceObject $parameterHelp.parameterValueGroup.parameterValue | Should -BeNullOrEmpty + } + elseif ($parameter.ParameterType.FullName -in $HelpTestEnumeratedArrays) { + # Enumerations often have issues with the typename not being reliably available + $names = [Enum]::GetNames($parameter.ParameterType.DeclaredMembers[0].ReturnType) + Compare-Object -ReferenceObject $names -DifferenceObject $parameterHelp.parameterValueGroup.parameterValue | Should -BeNullOrEmpty + } + else { + # To avoid calling Trim method on a null object. + $helpType = if ($parameterHelp.parameterValue) { $parameterHelp.parameterValue.Trim() } + $helpType | Should -Be $codeType + } + } } - $testparamserrors += 1 } } } - foreach ($helpParm in $HelpParameterNames) { - $testparamsall += 1 - if ($helpParm -notin $parameterNames) { - # Shouldn't find extra parameters in help. - It "finds help parameter in code: $helpParm" { - $helpParm -in $parameterNames | Should Be $true - } - $testparamserrors += 1 - } - } - if ($testparamserrors -eq 0) { - It "Ran silently $testparamsall tests" { - $testparamserrors | Should be 0 - } - } } } -} \ No newline at end of file +} diff --git a/tests/appveyor.common.ps1 b/tests/appveyor.common.ps1 index 8b5ca9aa64a6..45b234ad977b 100644 --- a/tests/appveyor.common.ps1 +++ b/tests/appveyor.common.ps1 @@ -149,7 +149,7 @@ function Get-TestsForBuildScenario { foreach ($t in $AllTests) { $testsThatDependOn += Get-AllTestsIndications -Path $t -ModuleBase $ModuleBase } - $AllTests = (($testsThatDependOn + $AllTests) | Select-Object -Unique) + $AllTests = ($testsThatDependOn + $AllTests) | Group-Object -Property FullName | ForEach-Object { $_.Group | Select-Object -First 1 } # Re-filter disabled tests that may have been picked up by dependency tracking $AllTests = $AllTests | Where-Object { ($_.Name -replace '^([^.]+)(.+)?.Tests.ps1', '$1') -notin $TestsRunGroups['disabled'] } @@ -189,7 +189,7 @@ function Get-TestsForBuildScenario { $testsThatDependOn += Get-AllTestsIndications -Path $t -ModuleBase $ModuleBase } - $AllTests = (($testsThatDependOn + $AllTests) | Select-Object -Unique) + $AllTests = ($testsThatDependOn + $AllTests) | Group-Object -Property FullName | ForEach-Object { $_.Group | Select-Object -First 1 } # re-filter disabled tests that may have been picked up by dependency tracking $AllTests = $AllTests | Where-Object { ($_.Name -replace '^([^.]+)(.+)?.Tests.ps1', '$1') -notin $TestsRunGroups['disabled'] } $AllTests = $AllTests | Where-Object { ($_.Name -replace '^([^.]+)(.+)?.Tests.ps1', '$1') -notin $TestsRunGroups['appveyor_disabled'] } diff --git a/tests/appveyor.pester.ps1 b/tests/appveyor.pester.ps1 index 9a75a04e0eb1..a0723a324f7a 100644 --- a/tests/appveyor.pester.ps1 +++ b/tests/appveyor.pester.ps1 @@ -174,18 +174,10 @@ function Get-CodecovReport($Results, $ModuleBase) { $newreport } -function Get-PesterTestVersion($testFilePath) { - $testFileContent = Get-Content -Path $testFilePath -Raw - if ($testFileContent -match '#Requires\s+-Module\s+@\{\s+ModuleName="Pester";\s+ModuleVersion="5\.') { - return '5' - } - return '4' -} function Get-ComprehensiveErrorMessage { param( $TestResult, - $PesterVersion, [switch]$DebugMode ) @@ -194,148 +186,83 @@ function Get-ComprehensiveErrorMessage { $debugInfo = @() try { - if ($PesterVersion -eq '4') { - # Pester 4 error extraction with multiple fallbacks - if ($TestResult.FailureMessage) { - $errorMessages += $TestResult.FailureMessage - } - - if ($TestResult.ErrorRecord) { - if ($TestResult.ErrorRecord.Exception) { - $errorMessages += $TestResult.ErrorRecord.Exception.Message - if ($TestResult.ErrorRecord.Exception.InnerException) { - $errorMessages += "Inner: $($TestResult.ErrorRecord.Exception.InnerException.Message)" + # Pester 5 error extraction with multiple fallbacks + if ($TestResult.ErrorRecord -and $TestResult.ErrorRecord.Count -gt 0) { + foreach ($errorRec in $TestResult.ErrorRecord) { + if ($errorRec.Exception) { + $errorMessages += $errorRec.Exception.Message + if ($errorRec.Exception.InnerException) { + $errorMessages += "Inner: $($errorRec.Exception.InnerException.Message)" } # Debug mode: extract more exception details if ($DebugMode) { - if ($TestResult.ErrorRecord.Exception.GetType) { - $debugInfo += "ExceptionType: $($TestResult.ErrorRecord.Exception.GetType().FullName)" + if ($errorRec.Exception.GetType) { + $debugInfo += "ExceptionType: $($errorRec.Exception.GetType().FullName)" } - if ($TestResult.ErrorRecord.Exception.HResult) { - $debugInfo += "HResult: $($TestResult.ErrorRecord.Exception.HResult)" + if ($errorRec.Exception.HResult) { + $debugInfo += "HResult: $($errorRec.Exception.HResult)" } - if ($TestResult.ErrorRecord.Exception.Source) { - $debugInfo += "Source: $($TestResult.ErrorRecord.Exception.Source)" + if ($errorRec.Exception.Source) { + $debugInfo += "Source: $($errorRec.Exception.Source)" } } } - if ($TestResult.ErrorRecord.ScriptStackTrace) { - $stackTraces += $TestResult.ErrorRecord.ScriptStackTrace + if ($errorRec.ScriptStackTrace) { + $stackTraces += $errorRec.ScriptStackTrace } - if ($TestResult.ErrorRecord.StackTrace) { - $stackTraces += $TestResult.ErrorRecord.StackTrace + if ($errorRec.StackTrace) { + $stackTraces += $errorRec.StackTrace + } + if ($errorRec.FullyQualifiedErrorId) { + $errorMessages += "ErrorId: $($errorRec.FullyQualifiedErrorId)" } # Debug mode: extract more ErrorRecord details if ($DebugMode) { - if ($TestResult.ErrorRecord.CategoryInfo) { - $debugInfo += "Category: $($TestResult.ErrorRecord.CategoryInfo.Category)" - $debugInfo += "Activity: $($TestResult.ErrorRecord.CategoryInfo.Activity)" - $debugInfo += "Reason: $($TestResult.ErrorRecord.CategoryInfo.Reason)" - $debugInfo += "TargetName: $($TestResult.ErrorRecord.CategoryInfo.TargetName)" - } - if ($TestResult.ErrorRecord.FullyQualifiedErrorId) { - $debugInfo += "ErrorId: $($TestResult.ErrorRecord.FullyQualifiedErrorId)" + if ($errorRec.CategoryInfo) { + $debugInfo += "Category: $($errorRec.CategoryInfo.Category)" + $debugInfo += "Activity: $($errorRec.CategoryInfo.Activity)" + $debugInfo += "Reason: $($errorRec.CategoryInfo.Reason)" + $debugInfo += "TargetName: $($errorRec.CategoryInfo.TargetName)" } - if ($TestResult.ErrorRecord.InvocationInfo) { - $debugInfo += "ScriptName: $($TestResult.ErrorRecord.InvocationInfo.ScriptName)" - $debugInfo += "Line: $($TestResult.ErrorRecord.InvocationInfo.ScriptLineNumber)" - $debugInfo += "Command: $($TestResult.ErrorRecord.InvocationInfo.MyCommand)" + if ($errorRec.InvocationInfo) { + $debugInfo += "ScriptName: $($errorRec.InvocationInfo.ScriptName)" + $debugInfo += "Line: $($errorRec.InvocationInfo.ScriptLineNumber)" + $debugInfo += "Command: $($errorRec.InvocationInfo.MyCommand)" } } } + } - if ($TestResult.StackTrace) { - $stackTraces += $TestResult.StackTrace - } - - # Try to extract from Result property if it's an object - if ($TestResult.Result -and $TestResult.Result -ne 'Failed') { - $errorMessages += "Result: $($TestResult.Result)" - } - - } else { - # Pester 5 error extraction with multiple fallbacks - if ($TestResult.ErrorRecord -and $TestResult.ErrorRecord.Count -gt 0) { - foreach ($errorRec in $TestResult.ErrorRecord) { - if ($errorRec.Exception) { - $errorMessages += $errorRec.Exception.Message - if ($errorRec.Exception.InnerException) { - $errorMessages += "Inner: $($errorRec.Exception.InnerException.Message)" - } - - # Debug mode: extract more exception details - if ($DebugMode) { - if ($errorRec.Exception.GetType) { - $debugInfo += "ExceptionType: $($errorRec.Exception.GetType().FullName)" - } - if ($errorRec.Exception.HResult) { - $debugInfo += "HResult: $($errorRec.Exception.HResult)" - } - if ($errorRec.Exception.Source) { - $debugInfo += "Source: $($errorRec.Exception.Source)" - } - } - } - if ($errorRec.ScriptStackTrace) { - $stackTraces += $errorRec.ScriptStackTrace - } - if ($errorRec.StackTrace) { - $stackTraces += $errorRec.StackTrace - } - if ($errorRec.FullyQualifiedErrorId) { - $errorMessages += "ErrorId: $($errorRec.FullyQualifiedErrorId)" - } - - # Debug mode: extract more ErrorRecord details - if ($DebugMode) { - if ($errorRec.CategoryInfo) { - $debugInfo += "Category: $($errorRec.CategoryInfo.Category)" - $debugInfo += "Activity: $($errorRec.CategoryInfo.Activity)" - $debugInfo += "Reason: $($errorRec.CategoryInfo.Reason)" - $debugInfo += "TargetName: $($errorRec.CategoryInfo.TargetName)" - } - if ($errorRec.InvocationInfo) { - $debugInfo += "ScriptName: $($errorRec.InvocationInfo.ScriptName)" - $debugInfo += "Line: $($errorRec.InvocationInfo.ScriptLineNumber)" - $debugInfo += "Command: $($errorRec.InvocationInfo.MyCommand)" - } - } - } - } - - if ($TestResult.FailureMessage) { - $errorMessages += $TestResult.FailureMessage - } - - if ($TestResult.StackTrace) { - $stackTraces += $TestResult.StackTrace - } + if ($TestResult.FailureMessage) { + $errorMessages += $TestResult.FailureMessage + } - # Try StandardOutput and StandardError if available - if ($TestResult.StandardOutput) { - $errorMessages += "StdOut: $($TestResult.StandardOutput)" - } - if ($TestResult.StandardError) { - $errorMessages += "StdErr: $($TestResult.StandardError)" - } + if ($TestResult.StackTrace) { + $stackTraces += $TestResult.StackTrace + } - # Add after the existing StandardError check in Pester 5 section: + # Try StandardOutput and StandardError if available + if ($TestResult.StandardOutput) { + $errorMessages += "StdOut: $($TestResult.StandardOutput)" + } + if ($TestResult.StandardError) { + $errorMessages += "StdErr: $($TestResult.StandardError)" + } - # Check Block.ErrorRecord for container-level errors (common in Pester 5) - if ($TestResult.Block -and $TestResult.Block.ErrorRecord) { - foreach ($blockError in $TestResult.Block.ErrorRecord) { - if ($blockError.Exception) { - $errorMessages += "Block Error: $($blockError.Exception.Message)" - } + # Check Block.ErrorRecord for container-level errors (common in Pester 5) + if ($TestResult.Block -and $TestResult.Block.ErrorRecord) { + foreach ($blockError in $TestResult.Block.ErrorRecord) { + if ($blockError.Exception) { + $errorMessages += "Block Error: $($blockError.Exception.Message)" } } + } - # Check for Should assertion details in Data property - if ($TestResult.Data -and $TestResult.Data.Count -gt 0) { - $errorMessages += "Test Data: $($TestResult.Data | ConvertTo-Json -Compress)" - } + # Check for Should assertion details in Data property + if ($TestResult.Data -and $TestResult.Data.Count -gt 0) { + $errorMessages += "Test Data: $($TestResult.Data | ConvertTo-Json -Compress)" } # Fallback: try to extract from any property that might contain error info @@ -400,81 +327,54 @@ function Export-TestFailureSummary { $TestFile, $PesterRun, $Counter, - $ModuleBase, - $PesterVersion + $ModuleBase ) $failedTests = @() - if ($PesterVersion -eq '4') { - $failedTests = $PesterRun.TestResult | Where-Object { $PSItem.Passed -eq $false } | ForEach-Object { - # Extract line number from stack trace for Pester 4 - $lineNumber = $null - if ($PSItem.StackTrace -match 'line (\d+)') { - $lineNumber = [int]$Matches[1] - } + # Pester 5 format + $failedTests = $PesterRun.Tests | Where-Object { $PSItem.Passed -eq $false } | ForEach-Object { + # Extract line number from stack trace + $lineNumber = $null + $stackTrace = "" - # Get comprehensive error message with fallbacks - $errorInfo = Get-ComprehensiveErrorMessage -TestResult $PSItem -PesterVersion '4' -DebugMode:$DebugErrorExtraction - - @{ - Name = $PSItem.Name - Describe = $PSItem.Describe - Context = $PSItem.Context - ErrorMessage = $errorInfo.ErrorMessage - StackTrace = if ($errorInfo.StackTrace) { $errorInfo.StackTrace } else { $PSItem.StackTrace } - LineNumber = $lineNumber - Parameters = $PSItem.Parameters - ParameterizedSuiteName = $PSItem.ParameterizedSuiteName - TestFile = $TestFile.Name - RawTestResult = $PSItem | ConvertTo-Json -Depth 3 -Compress + if ($PSItem.ErrorRecord -and $PSItem.ErrorRecord.Count -gt 0 -and $PSItem.ErrorRecord[0].ScriptStackTrace) { + $stackTrace = $PSItem.ErrorRecord[0].ScriptStackTrace + if ($stackTrace -match 'line (\d+)') { + $lineNumber = [int]$Matches[1] } } - } else { - # Pester 5 format - $failedTests = $PesterRun.Tests | Where-Object { $PSItem.Passed -eq $false } | ForEach-Object { - # Extract line number from stack trace for Pester 5 - $lineNumber = $null - $stackTrace = "" - - if ($PSItem.ErrorRecord -and $PSItem.ErrorRecord.Count -gt 0 -and $PSItem.ErrorRecord[0].ScriptStackTrace) { - $stackTrace = $PSItem.ErrorRecord[0].ScriptStackTrace - if ($stackTrace -match 'line (\d+)') { - $lineNumber = [int]$Matches[1] - } - } - # Get comprehensive error message with fallbacks - $errorInfo = Get-ComprehensiveErrorMessage -TestResult $PSItem -PesterVersion '5' -DebugMode:$DebugErrorExtraction - - @{ - Name = $PSItem.Name - Describe = if ($PSItem.Path.Count -gt 0) { $PSItem.Path[0] } else { "" } - Context = if ($PSItem.Path.Count -gt 1) { $PSItem.Path[1] } else { "" } - ErrorMessage = $errorInfo.ErrorMessage - StackTrace = if ($errorInfo.StackTrace) { $errorInfo.StackTrace } else { $stackTrace } - LineNumber = $lineNumber - Parameters = $PSItem.Data - TestFile = $TestFile.Name - RawTestResult = $PSItem | ConvertTo-Json -Depth 3 -Compress - } + # Get comprehensive error message with fallbacks + $errorInfo = Get-ComprehensiveErrorMessage -TestResult $PSItem -DebugMode:$DebugErrorExtraction + + @{ + Name = $PSItem.Name + Describe = if ($PSItem.Path.Count -gt 0) { $PSItem.Path[0] } else { "" } + Context = if ($PSItem.Path.Count -gt 1) { $PSItem.Path[1] } else { "" } + ErrorMessage = $errorInfo.ErrorMessage + StackTrace = if ($errorInfo.StackTrace) { $errorInfo.StackTrace } else { $stackTrace } + LineNumber = $lineNumber + Parameters = $PSItem.Data + TestFile = $TestFile.Name + RawTestResult = $PSItem | ConvertTo-Json -Depth 3 -Compress } } if ($failedTests.Count -gt 0) { $summary = @{ TestFile = $TestFile.Name - PesterVersion = $PesterVersion - TotalTests = if ($PesterVersion -eq '4') { $PesterRun.TotalCount } else { $PesterRun.TotalCount } - PassedTests = if ($PesterVersion -eq '4') { $PesterRun.PassedCount } else { $PesterRun.PassedCount } - FailedTests = if ($PesterVersion -eq '4') { $PesterRun.FailedCount } else { $PesterRun.FailedCount } - Duration = if ($PesterVersion -eq '4') { $PesterRun.Time.TotalMilliseconds } else { $PesterRun.Duration.TotalMilliseconds } + PesterVersion = "5" + TotalTests = $PesterRun.TotalCount + PassedTests = $PesterRun.PassedCount + FailedTests = $PesterRun.FailedCount + Duration = $PesterRun.Duration.TotalMilliseconds Failures = $failedTests } - $summaryFile = "$ModuleBase\TestFailureSummary_Pester${PesterVersion}_${Counter}.json" + $summaryFile = "$ModuleBase\TestFailureSummary_Pester5_${Counter}.json" $summary | ConvertTo-Json -Depth 10 | Out-File $summaryFile -Encoding UTF8 - Push-AppveyorArtifact $summaryFile -FileName "TestFailureSummary_Pester${PesterVersion}_${Counter}.json" + Push-AppveyorArtifact $summaryFile -FileName "TestFailureSummary_Pester5_${Counter}.json" } } @@ -495,8 +395,8 @@ if (-not $Finalize) { # Remove any previously loaded pester module Remove-Module -Name pester -ErrorAction SilentlyContinue - # Import pester 4 - Import-Module pester -RequiredVersion 4.4.2 + # Import pester 5 + Import-Module pester -RequiredVersion 5.6.1 Write-Host -Object "appveyor.pester: Running with Pester Version $((Get-Command Invoke-Pester -ErrorAction SilentlyContinue).Version)" -ForegroundColor DarkGreen # invoking a single invoke-pester consumes too much memory, let's go file by file @@ -509,101 +409,11 @@ if (-not $Finalize) { TestRuns = @() } - #start the round for pester 4 tests - $Counter = 0 - foreach ($f in $AllTestsWithinScenario) { - $Counter += 1 - $PesterSplat = @{ - 'Script' = $f.FullName - 'Show' = 'None' - 'PassThru' = $true - } - - #get if this test should run on pester 4 or pester 5 - $pesterVersionToUse = Get-PesterTestVersion -testFilePath $f.FullName - if ($pesterVersionToUse -eq '5') { - # we're in the "region" of pester 4, so skip - continue - } - - #opt-in - if ($IncludeCoverage) { - $CoverFiles = Get-CoverageIndications -Path $f -ModuleBase $ModuleBase - $PesterSplat['CodeCoverage'] = $CoverFiles - $PesterSplat['CodeCoverageOutputFile'] = "$ModuleBase\PesterCoverage$Counter.xml" - } - - # Pester 4.0 outputs already what file is being ran. If we remove write-host from every test, we can time - # executions for each test script (i.e. Executing Get-DbaFoo .... Done (40 seconds)) - $trialNo = 1 - while ($trialNo -le 3) { - if ($trialNo -eq 1) { - $appvTestName = $f.Name - } else { - $appvTestName = "$($f.Name), attempt #$trialNo" - } - Add-AppveyorTest -Name $appvTestName -Framework NUnit -FileName $f.FullName -Outcome Running - $PesterRun = Invoke-Pester @PesterSplat - $PesterRun | Export-Clixml -Path "$ModuleBase\PesterResults$PSVersion$Counter.xml" - - # Export failure summary for easier retrieval - Export-TestFailureSummary -TestFile $f -PesterRun $PesterRun -Counter $Counter -ModuleBase $ModuleBase -PesterVersion '4' - - if ($PesterRun.FailedCount -gt 0) { - $trialno += 1 - - # Create detailed error message for AppVeyor with comprehensive extraction - $failedTestsList = $PesterRun.TestResult | Where-Object { $PSItem.Passed -eq $false } | ForEach-Object { - $errorInfo = Get-ComprehensiveErrorMessage -TestResult $PSItem -PesterVersion '4' -DebugMode:$DebugErrorExtraction - "$($PSItem.Describe) > $($PSItem.Context) > $($PSItem.Name): $($errorInfo.ErrorMessage)" - } - $errorMessageDetail = $failedTestsList -join " | " - - Update-AppveyorTest -Name $appvTestName -Framework NUnit -FileName $f.FullName -Outcome "Failed" -Duration $PesterRun.Time.TotalMilliseconds -ErrorMessage $errorMessageDetail - - # Add to summary - $allTestsSummary.TestRuns += @{ - TestFile = $f.Name - Attempt = $trialNo - Outcome = "Failed" - FailedCount = $PesterRun.FailedCount - Duration = $PesterRun.Time.TotalMilliseconds - PesterVersion = '4' - } - } else { - Update-AppveyorTest -Name $appvTestName -Framework NUnit -FileName $f.FullName -Outcome "Passed" -Duration $PesterRun.Time.TotalMilliseconds - - # Add to summary - $allTestsSummary.TestRuns += @{ - TestFile = $f.Name - Attempt = $trialNo - Outcome = "Passed" - Duration = $PesterRun.Time.TotalMilliseconds - PesterVersion = '4' - } - break - } - } - } - - #start the round for pester 5 tests - # Remove any previously loaded pester module - Remove-Module -Name pester -ErrorAction SilentlyContinue - # Import pester 5 - Import-Module pester -RequiredVersion 5.6.1 - Write-Host -Object "appveyor.pester: Running with Pester Version $((Get-Command Invoke-Pester -ErrorAction SilentlyContinue).Version)" -ForegroundColor DarkGreen $TestConfig = Get-TestConfig $Counter = 0 foreach ($f in $AllTestsWithinScenario) { $Counter += 1 - #get if this test should run on pester 4 or pester 5 - $pesterVersionToUse = Get-PesterTestVersion -testFilePath $f.FullName - if ($pesterVersionToUse -eq '4') { - # we're in the "region" of pester 5, so skip - continue - } - $pester5Config = New-PesterConfiguration $pester5Config.Run.Path = $f.FullName $pester5config.Run.PassThru = $true @@ -632,7 +442,7 @@ if (-not $Finalize) { $PesterRun | Export-Clixml -Path "$ModuleBase\Pester5Results$PSVersion$Counter.xml" # Export failure summary for easier retrieval - Export-TestFailureSummary -TestFile $f -PesterRun $PesterRun -Counter $Counter -ModuleBase $ModuleBase -PesterVersion '5' + Export-TestFailureSummary -TestFile $f -PesterRun $PesterRun -Counter $Counter -ModuleBase $ModuleBase if ($PesterRun.FailedCount -gt 0) { $trialno += 1 @@ -640,7 +450,7 @@ if (-not $Finalize) { # Create detailed error message for AppVeyor with comprehensive extraction $failedTestsList = $PesterRun.Tests | Where-Object { $PSItem.Passed -eq $false } | ForEach-Object { $path = $PSItem.Path -join " > " - $errorInfo = Get-ComprehensiveErrorMessage -TestResult $PSItem -PesterVersion '5' -DebugMode:$DebugErrorExtraction + $errorInfo = Get-ComprehensiveErrorMessage -TestResult $PSItem -DebugMode:$DebugErrorExtraction "$path > $($PSItem.Name): $($errorInfo.ErrorMessage)" } $errorMessageDetail = $failedTestsList -join " | " @@ -717,27 +527,22 @@ if (-not $Finalize) { } #> - #What failed? How many tests did we run ? - $results = @(Get-ChildItem -Path "$ModuleBase\PesterResults*.xml" | Import-Clixml) - #Publish the support package regardless of the outcome if (Test-Path $ModuleBase\dbatools_messages_and_errors.xml.zip) { Get-ChildItem $ModuleBase\dbatools_messages_and_errors.xml.zip | ForEach-Object { Push-AppveyorArtifact $PSItem.FullName -FileName $PSItem.Name } } - #$totalcount = $results | Select-Object -ExpandProperty TotalCount | Measure-Object -Sum | Select-Object -ExpandProperty Sum - $failedcount = 0 - $results5 = @(Get-ChildItem -Path "$ModuleBase\Pester5Results*.xml" | Import-Clixml) - $failedcount += $results5 | Select-Object -ExpandProperty FailedCount | Measure-Object -Sum | Select-Object -ExpandProperty Sum - # pester 5 output - $faileditems = $results5 | Select-Object -ExpandProperty Tests | Where-Object { $PSItem.Passed -notlike $True } + #What failed? How many tests did we run ? + $results = @(Get-ChildItem -Path "$ModuleBase\Pester5Results*.xml" | Import-Clixml) + $failedcount = $results | Select-Object -ExpandProperty FailedCount | Measure-Object -Sum | Select-Object -ExpandProperty Sum + $faileditems = $results | Select-Object -ExpandProperty Tests | Where-Object { $PSItem.Passed -notlike $True } if ($faileditems) { - Write-Warning "Failed tests summary (pester 5):" + Write-Warning "Failed tests summary:" $detailedFailures = $faileditems | ForEach-Object { $name = $PSItem.Name # Use comprehensive error extraction for finalization too - $errorInfo = Get-ComprehensiveErrorMessage -TestResult $PSItem -PesterVersion '5' -DebugMode:$DebugErrorExtraction + $errorInfo = Get-ComprehensiveErrorMessage -TestResult $PSItem -DebugMode:$DebugErrorExtraction [PSCustomObject]@{ Path = $PSItem.Path -Join '/' diff --git a/tests/appveyor.prep.ps1 b/tests/appveyor.prep.ps1 index bf70be0250ed..4b12dd4d1ae3 100644 --- a/tests/appveyor.prep.ps1 +++ b/tests/appveyor.prep.ps1 @@ -49,11 +49,7 @@ if ($installedModule.Version.ToString() -notmatch [regex]::Escape($expectedVersi Write-Host -Object "appveyor.prep: Version validation successful" -ForegroundColor Green } -##Get Pester (to run tests) - choco isn't working onall scenarios, weird -Write-Host -Object "appveyor.prep: Install Pester4" -ForegroundColor DarkGreen -if (-not(Test-Path 'C:\Program Files\WindowsPowerShell\Modules\Pester\4.4.2')) { - Install-Module -Name Pester -Force -SkipPublisherCheck -MaximumVersion 4.4.2 | Out-Null -} +##Get Pester (to run tests) Write-Host -Object "appveyor.prep: Install Pester5" -ForegroundColor DarkGreen if (-not(Test-Path 'C:\Program Files\WindowsPowerShell\Modules\Pester\5.6.1')) { Install-Module -Name Pester -Force -SkipPublisherCheck -RequiredVersion 5.6.1 | Out-Null diff --git a/tests/dbatools.Tests.ps1 b/tests/dbatools.Tests.ps1 index 984e93dee23a..c7b6268e7e76 100644 --- a/tests/dbatools.Tests.ps1 +++ b/tests/dbatools.Tests.ps1 @@ -1,3 +1,4 @@ +#Requires -Module @{ ModuleName="Pester"; ModuleVersion="5.0" } Write-Host -Object "Running $PSCommandPath" -ForegroundColor Cyan $Path = Split-Path -Parent $MyInvocation.MyCommand.Path $ModulePath = (Get-Item $Path).Parent.FullName @@ -6,19 +7,20 @@ $ModuleName = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -Replace ".Tests.p Describe "$ModuleName Aliases" -Tag Aliases, Build { ## Get the Aliases that should -Be set from the psm1 file + BeforeAll { + $psm1 = Get-Content "$ModulePath\$ModuleName.psm1" + $Matches = [regex]::Matches($psm1, "AliasName`"\s=\s`"(\w*-\w*)`"") + $global:Aliases = $Matches.ForEach{ $_.Groups[1].Value } + } - $psm1 = Get-Content $ModulePath\$ModuleName.psm1 -Verbose - $Matches = [regex]::Matches($psm1, "AliasName`"\s=\s`"(\w*-\w*)`"") - $Aliases = $Matches.ForEach{ $_.Groups[1].Value } - - foreach ($Alias in $Aliases) { + foreach ($Alias in $global:Aliases) { Context "Testing $Alias Alias" { - $Definition = (Get-Alias $Alias).Definition It "$Alias Alias should exist" { - Get-Alias $Alias | Should Not BeNullOrEmpty + Get-Alias $Alias | Should -Not -BeNullOrEmpty } - It "$Alias Aliased Command $Definition Should Exist" { - Get-Command $Definition -ErrorAction SilentlyContinue | Should Not BeNullOrEmpty + It "$Alias Aliased Command Should Exist" { + $Definition = (Get-Alias $Alias).Definition + Get-Command $Definition -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty } } } @@ -27,6 +29,7 @@ Describe "$ModuleName Aliases" -Tag Aliases, Build { function Split-ArrayInParts($array, [int]$parts) { #splits an array in "equal" parts $size = $array.Length / $parts + if ($size -lt 1) { $size = 1 } $counter = [PSCustomObject] @{ Value = 0 } $groups = $array | Group-Object -Property { [math]::Floor($counter.Value++ / $size) } $rtn = @() @@ -43,10 +46,12 @@ Describe "$ModuleName style" -Tag 'Compliance' { - OTBS style, courtesy of PSSA's Invoke-Formatter, is what dbatools uses - UTF8 without BOM is what is going to be used in PS Core, so we adopt this standard for dbatools #> - $AllFiles = Get-ChildItem -Path $ModulePath -File -Recurse -Filter '*.ps*1' | Where-Object Name -ne 'dbatools.ps1' - $AllFunctionFiles = Get-ChildItem -Path "$ModulePath\public", "$ModulePath\private\functions"-Filter '*.ps*1' - Context "formatting" { + BeforeAll { + $global:AllFiles = Get-ChildItem -Path $ModulePath -File -Recurse -Filter '*.ps*1' | Where-Object Name -ne 'dbatools.ps1' + $AllFunctionFiles = Get-ChildItem -Path "$ModulePath\public", "$ModulePath\private\functions" -Filter '*.ps*1' + $maxConcurrentJobs = $env:NUMBER_OF_PROCESSORS + if (-not $maxConcurrentJobs) { $maxConcurrentJobs = 1 } $whatever = Split-ArrayInParts -array $AllFunctionFiles -parts $maxConcurrentJobs $jobs = @() foreach ($piece in $whatever) { @@ -61,9 +66,11 @@ Describe "$ModuleName style" -Tag 'Compliance' { } -ArgumentList $piece } $null = $jobs | Wait-Job #-Timeout 120 - $results = $jobs | Receive-Job + $global:formattingResults = $jobs | Receive-Job + } - foreach ($f in $results) { + Context "formatting" { + foreach ($f in $global:formattingResults) { It "$f is not compliant with the OTBS formatting style. Please run Invoke-DbatoolsFormatter against the failing file and commit the changes." { 1 | Should -Be 0 } @@ -71,9 +78,9 @@ Describe "$ModuleName style" -Tag 'Compliance' { } Context "BOM" { - foreach ($f in $AllFiles) { + foreach ($f in $global:AllFiles) { [byte[]]$byteContent = Get-Content -Path $f.FullName -Encoding Byte -ReadCount 4 -TotalCount 4 - if ( $byteContent[0] -eq 0xef -and $byteContent[1] -eq 0xbb -and $byteContent[2] -eq 0xbf ) { + if ( $byteContent.Length -gt 2 -and $byteContent[0] -eq 0xef -and $byteContent[1] -eq 0xbb -and $byteContent[2] -eq 0xbf ) { It "$f has no BOM in it" { "utf8bom" | Should -Be "utf8" } @@ -83,7 +90,7 @@ Describe "$ModuleName style" -Tag 'Compliance' { Context "indentation" { - foreach ($f in $AllFiles) { + foreach ($f in $global:AllFiles) { $LeadingTabs = Select-String -Path $f -Pattern '^[\t]+' if ($LeadingTabs.Count -gt 0) { It "$f is not indented with tabs (line(s) $($LeadingTabs.LineNumber -join ','))" { @@ -105,13 +112,15 @@ Describe "$ModuleName style" -Tag 'Compliance' { <# Ensures avoiding already discovered pitfalls #> - $AllPublicFunctions = Get-ChildItem -Path "$ModulePath\public" -Filter '*.ps*1' + BeforeAll { + $global:AllPublicFunctions = Get-ChildItem -Path "$ModulePath\public" -Filter '*.ps*1' + } Context "NoCompatibleTLS" { # .NET defaults clash with recent TLS hardening (e.g. no TLS 1.2 by default) - foreach ($f in $AllPublicFunctions) { + foreach ($f in $global:AllPublicFunctions) { $NotAllowed = Select-String -Path $f -Pattern 'Invoke-WebRequest | New-Object System.Net.WebClient|\.DownloadFile' - if ($NotAllowed.Count -gt 0 -and $f -notmatch 'DbaKbUpdate') { + if ($NotAllowed.Count -gt 0 -and $f.Name -notmatch 'DbaKbUpdate') { It "$f should instead use Invoke-TlsWebRequest, see #4250" { $NotAllowed.Count | Should -Be 0 } @@ -120,7 +129,7 @@ Describe "$ModuleName style" -Tag 'Compliance' { } Context "Shell.Application" { # Not every PS instance has Shell.Application - foreach ($f in $AllPublicFunctions) { + foreach ($f in $global:AllPublicFunctions) { $NotAllowed = Select-String -Path $f -Pattern 'shell.application' if ($NotAllowed.Count -gt 0) { It "$f should not use Shell.Application (usually fallbacks for Expand-Archive, which dbatools ships), see #4800" { @@ -134,12 +143,14 @@ Describe "$ModuleName style" -Tag 'Compliance' { Describe "$ModuleName ScriptAnalyzerErrors" -Tag 'Compliance' { - $ScriptAnalyzerErrors = @() - $ScriptAnalyzerErrors += Invoke-ScriptAnalyzer -Path "$ModulePath\public" -Severity Error - $ScriptAnalyzerErrors += Invoke-ScriptAnalyzer -Path "$ModulePath\private\functions" -Severity Error + BeforeAll { + $global:ScriptAnalyzerErrors = @() + $global:ScriptAnalyzerErrors += Invoke-ScriptAnalyzer -Path "$ModulePath\public" -Severity Error + $global:ScriptAnalyzerErrors += Invoke-ScriptAnalyzer -Path "$ModulePath\private\functions" -Severity Error + } Context "Errors" { - if ($ScriptAnalyzerErrors.Count -gt 0) { - foreach ($err in $ScriptAnalyzerErrors) { + if ($global:ScriptAnalyzerErrors.Count -gt 0) { + foreach ($err in $global:ScriptAnalyzerErrors) { It "$($err.scriptName) has Error(s) : $($err.RuleName)" { $err.Message | Should -Be $null } @@ -149,11 +160,13 @@ Describe "$ModuleName ScriptAnalyzerErrors" -Tag 'Compliance' { } Describe "$ModuleName Tests missing" -Tag 'Tests' { - $functions = Get-ChildItem "$ModulePath\public\" -Recurse -Include *.ps1 + BeforeAll { + $global:functions = Get-ChildItem "$ModulePath\public\" -Recurse -Include *.ps1 + } Context "Every function should have tests" { - foreach ($f in $functions) { + foreach ($f in $global:functions) { It "$($f.basename) has a tests.ps1 file" { - Test-Path "$ModulePath\tests\$($f.basename).tests.ps1" | Should Be $true + Test-Path "$ModulePath\tests\$($f.basename).tests.ps1" | Should -Be $true } If (Test-Path "$ModulePath\tests\$($f.basename).tests.ps1") { It "$($f.basename) has validate parameters unit test" { @@ -167,47 +180,49 @@ Describe "$ModuleName Tests missing" -Tag 'Tests' { } Describe "$ModuleName Function Name" -Tag 'Compliance' { - $FunctionNameMatchesErrors = @() - $FunctionNameDbaErrors = @() - foreach ($item in (Get-ChildItem -Path "$ModulePath\public" -Filter '*.ps*1')) { - $Tokens = $null - $Errors = $null - $ast = [System.Management.Automation.Language.Parser]::ParseFile($item.FullName, [ref]$Tokens, [ref]$Errors) - $FunctionName = $Ast.EndBlock.Statements.Name - $BaseName = $item.BaseName - if ($FunctionName -cne $BaseName) { - $FunctionNameMatchesErrors += [PSCustomObject]@{ - FunctionName = $FunctionName - BaseName = $BaseName - Message = "$FunctionName is not equal to $BaseName" - } - } - If ($FunctionName -NotMatch "-Dba") { - $FunctionNameDbaErrors += [PSCustomObject]@{ - FunctionName = $FunctionName - Message = "$FunctionName does not contain -Dba" + BeforeAll { + $global:FunctionNameMatchesErrors = @() + $global:FunctionNameDbaErrors = @() + foreach ($item in (Get-ChildItem -Path "$ModulePath\public" -Filter '*.ps*1')) { + $Tokens = $null + $Errors = $null + $ast = [System.Management.Automation.Language.Parser]::ParseFile($item.FullName, [ref]$Tokens, [ref]$Errors) + $FunctionName = $Ast.EndBlock.Statements.Name + $BaseName = $item.BaseName + if ($FunctionName -cne $BaseName) { + $global:FunctionNameMatchesErrors += [PSCustomObject]@{ + FunctionName = $FunctionName + BaseName = $BaseName + Message = "$FunctionName is not equal to $BaseName" + } } + If ($FunctionName -NotMatch "-Dba") { + $global:FunctionNameDbaErrors += [PSCustomObject]@{ + FunctionName = $FunctionName + Message = "$FunctionName does not contain -Dba" + } + } } - } - foreach ($item in (Get-ChildItem -Path "$ModulePath\private\functions" -Filter '*.ps*1' | Where-Object BaseName -ne 'Where-DbaObject')) { - $Tokens = $null - $Errors = $null - $Ast = [System.Management.Automation.Language.Parser]::ParseFile($item.FullName, [ref]$Tokens, [ref]$Errors) - $FunctionName = $Ast.EndBlock.Statements.Name - $BaseName = $item.BaseName - if ($FunctionName -cne $BaseName) { - Write-Host "aaa $functionname bbb $basename" - $FunctionNameMatchesErrors += [PSCustomObject]@{ - FunctionName = $FunctionName - BaseName = $BaseName - Message = "$FunctionName is not equal to $BaseName" + foreach ($item in (Get-ChildItem -Path "$ModulePath\private\functions" -Filter '*.ps*1' | Where-Object BaseName -ne 'Where-DbaObject')) { + $Tokens = $null + $Errors = $null + $Ast = [System.Management.Automation.Language.Parser]::ParseFile($item.FullName, [ref]$Tokens, [ref]$Errors) + $FunctionName = $Ast.EndBlock.Statements.Name + $BaseName = $item.BaseName + if ($FunctionName -cne $BaseName) { + Write-Host "aaa $functionname bbb $basename" + $global:FunctionNameMatchesErrors += [PSCustomObject]@{ + FunctionName = $FunctionName + BaseName = $BaseName + Message = "$FunctionName is not equal to $BaseName" + } } } } Context "Function Name Matching Filename Errors" { - if ($FunctionNameMatchesErrors.Count -gt 0) { - foreach ($err in $FunctionNameMatchesErrors) { + if ($global:FunctionNameMatchesErrors.Count -gt 0) { + foreach ($err in $global:FunctionNameMatchesErrors) { It "$($err.FunctionName) is not equal to $($err.BaseName)" { $err.Message | Should -Be $null } @@ -215,8 +230,8 @@ Describe "$ModuleName Function Name" -Tag 'Compliance' { } } Context "Function Name has -Dba in it" { - if ($FunctionNameDbaErrors.Count -gt 0) { - foreach ($err in $FunctionNameDbaErrors) { + if ($global:FunctionNameDbaErrors.Count -gt 0) { + foreach ($err in $global:FunctionNameDbaErrors) { It "$($err.FunctionName) does not contain -Dba" { $err.Message | Should -Be $null } @@ -229,23 +244,23 @@ Describe "$ModuleName Function Name" -Tag 'Compliance' { <# Describe "Manifest" { - $Manifest = $null + $global:Manifest = $null It "has a valid manifest" { { - $script:Manifest = Test-ModuleManifest -Path $ManifestPath -ErrorAction Stop -WarningAction SilentlyContinue + $global:Manifest = Test-ModuleManifest -Path $ManifestPath -ErrorAction Stop -WarningAction SilentlyContinue - } | Should Not Throw + } | Should -Not -Throw } ## Should -Be fixed now - Until the issue with requiring full paths for required assemblies is resolved need to keep this commented out RMS 01112016 -$script:Manifest = Test-ModuleManifest -Path $ManifestPath -ErrorAction SilentlyContinue +$global:Manifest = Test-ModuleManifest -Path $ManifestPath -ErrorAction SilentlyContinue It "has a valid name" { - $script:Manifest.Name | Should -Be $ModuleName + $global:Manifest.Name | Should -Be $ModuleName } @@ -253,7 +268,7 @@ $script:Manifest = Test-ModuleManifest -Path $ManifestPath -ErrorAction Silently It "has a valid root module" { - $script:Manifest.RootModule | Should -Be "$ModuleName.psm1" + $global:Manifest.RootModule | Should -Be "$ModuleName.psm1" } @@ -261,33 +276,33 @@ $script:Manifest = Test-ModuleManifest -Path $ManifestPath -ErrorAction Silently It "has a valid Description" { - $script:Manifest.Description | Should -Be 'Provides extra functionality for SQL Server Database admins and enables SQL Server instance migrations.' + $global:Manifest.Description | Should -Be 'Provides extra functionality for SQL Server Database admins and enables SQL Server instance migrations.' } It "has a valid Author" { - $script:Manifest.Author | Should -Be 'Chrissy LeMaire' + $global:Manifest.Author | Should -Be 'Chrissy LeMaire' } It "has a valid Company Name" { - $script:Manifest.CompanyName | Should -Be 'dbatools.io' + $global:Manifest.CompanyName | Should -Be 'dbatools.io' } It "has a valid guid" { - $script:Manifest.Guid | Should -Be '9d139310-ce45-41ce-8e8b-d76335aa1789' + $global:Manifest.Guid | Should -Be '9d139310-ce45-41ce-8e8b-d76335aa1789' } It "has valid PowerShell version" { - $script:Manifest.PowerShellVersion | Should -Be '3.0' + $global:Manifest.PowerShellVersion | Should -Be '3.0' } It "has valid required assemblies" { - {$script:Manifest.RequiredAssemblies -eq @()} | Should -Be $true + $global:Manifest.RequiredAssemblies | Should -BeEmpty } It "has a valid copyright" { - $script:Manifest.CopyRight | Should BeLike '* Chrissy LeMaire' + $global:Manifest.CopyRight | Should -BeLike '* Chrissy LeMaire' } @@ -301,7 +316,7 @@ $script:Manifest = Test-ModuleManifest -Path $ManifestPath -ErrorAction Silently $FunctionNames = $FunctionFiles - $ExFunctions = $script:Manifest.ExportedFunctions.Values.Name + $ExFunctions = $global:Manifest.ExportedFunctions.Values.Name $ExFunctions foreach ($FunctionName in $FunctionNames)