Skip to content

Commit b0bf0ff

Browse files
author
Friedrich Weinmann
committed
Added tests
1 parent b40bfa1 commit b0bf0ff

File tree

10 files changed

+537
-2
lines changed

10 files changed

+537
-2
lines changed

PSModuleDevelopment/PSModuleDevelopment.psproj

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Synchronized="True" SyncFilter="*.ps1;*.psm1;*.psd1;*.psxml;*.psf;*.pss;*.xml; *.txt;*.dll">
1+
<Project Synchronized="True" SyncFilter="*.ps1;*.psm1;*.psd1;*.psxml;*.psf;*.pss;*.xml; *.txt;*.dll;*.ps1xml;*.md">
22
<Version>2.0</Version>
33
<FileID>37dd5fce-e7b5-4d57-ac37-832055ce49d6</FileID>
44
<ProjectType>1</ProjectType>
@@ -19,6 +19,9 @@
1919
<Folder>functions\assembly</Folder>
2020
<Folder>bin</Folder>
2121
<Folder>xml</Folder>
22+
<Folder>tests</Folder>
23+
<Folder>tests\functions</Folder>
24+
<Folder>tests\general</Folder>
2225
</Folders>
2326
<Files>
2427
<File Build="2">PSModuleDevelopment.psd1</File>
@@ -54,9 +57,18 @@
5457
<File Build="2" Shared="True" ReferenceFunction="Invoke-Find-PSMDFileContent_ps1">functions\utility\Find-PSMDFileContent.ps1</File>
5558
<File Build="2" Shared="True" ReferenceFunction="Invoke-utility_ps1">internal\configurations\utility.ps1</File>
5659
<File Build="2" Shared="True" ReferenceFunction="Invoke-Get-PSMDConstructor_ps1">functions\assembly\Get-PSMDConstructor.ps1</File>
57-
<File Build="2">xml\PSModuleDevelopment.Format.ps1xml</File>
5860
<File Build="2" Shared="True" ReferenceFunction="Invoke-Get-PSMDAssembly_ps1">functions\assembly\Get-PSMDAssembly.ps1</File>
5961
<File Build="2" Shared="True" ReferenceFunction="Invoke-Find-PSMDType_ps1">functions\assembly\Find-PSMDType.ps1</File>
62+
<File Build="2" Shared="True" ReferenceFunction="Invoke-pester_ps1">tests\pester.ps1</File>
63+
<File Build="2" Shared="True" ReferenceFunction="Invoke-FileIntegrity_Exceptions_ps1">tests\general\FileIntegrity.Exceptions.ps1</File>
64+
<File Build="2" Shared="True" ReferenceFunction="Invoke-FileIntegrity_Tests_ps1">tests\general\FileIntegrity.Tests.ps1</File>
65+
<File Build="2" Shared="True" ReferenceFunction="Invoke-Help_Exceptions_ps1">tests\general\Help.Exceptions.ps1</File>
66+
<File Build="2" Shared="True" ReferenceFunction="Invoke-Help_Tests_ps1">tests\general\Help.Tests.ps1</File>
67+
<File Build="2" Shared="True" ReferenceFunction="Invoke-manifest_Tests_ps1">tests\general\manifest.Tests.ps1</File>
68+
<File Build="2" Shared="True" ReferenceFunction="Invoke-PSScriptAnalyzer_Tests_ps1">tests\general\PSScriptAnalyzer.Tests.ps1</File>
69+
<File Build="2">tests\readme.md</File>
70+
<File Build="2">xml\PSModuleDevelopment.Format.ps1xml</File>
71+
<File Build="2">tests\functions\readme.md</File>
6072
</Files>
6173
<StartupScript>F:\Synchronized Data\Scripte\Powershell Studio\Projects\PSModuleDevelopment\Test-Module.ps1</StartupScript>
6274
</Project>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Description
2+
This is where the function tests go.
3+
Make sure to put them in folders reflecting the actual module structure.
4+
It is not necessary to differentiate between internal and public functions here.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# List of forbidden commands
2+
$global:BannedCommands = @(
3+
'Write-Host',
4+
'Write-Verbose',
5+
'Write-Warning',
6+
'Write-Error',
7+
'Write-Output',
8+
'Write-Information',
9+
'Write-Debug'
10+
)
11+
12+
# Contains list of exceptions for banned cmdlets
13+
$global:MayContainCommand = @{
14+
"Write-Host" = @()
15+
"Write-Verbose" = @()
16+
"Write-Warning" = @()
17+
"Write-Error" = @()
18+
"Write-Output" = @()
19+
"Write-Information" = @()
20+
"Write-Debug" = @()
21+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
$moduleRoot = (Resolve-Path "$PSScriptRoot\..\..").Path
2+
3+
. "$PSScriptRoot\FileIntegrity.Exceptions.ps1"
4+
5+
function Get-FileEncoding
6+
{
7+
<#
8+
.SYNOPSIS
9+
Tests a file for encoding.
10+
11+
.DESCRIPTION
12+
Tests a file for encoding.
13+
14+
.PARAMETER Path
15+
The file to test
16+
#>
17+
[CmdletBinding()]
18+
Param (
19+
[Parameter(Mandatory = $True, ValueFromPipelineByPropertyName = $True)]
20+
[Alias('FullName')]
21+
[string]
22+
$Path
23+
)
24+
25+
[byte[]]$byte = get-content -Encoding byte -ReadCount 4 -TotalCount 4 -Path $Path
26+
27+
if ($byte[0] -eq 0xef -and $byte[1] -eq 0xbb -and $byte[2] -eq 0xbf) { 'UTF8' }
28+
elseif ($byte[0] -eq 0xfe -and $byte[1] -eq 0xff) { 'Unicode' }
29+
elseif ($byte[0] -eq 0 -and $byte[1] -eq 0 -and $byte[2] -eq 0xfe -and $byte[3] -eq 0xff) { 'UTF32' }
30+
elseif ($byte[0] -eq 0x2b -and $byte[1] -eq 0x2f -and $byte[2] -eq 0x76) { 'UTF7' }
31+
else { 'Unknown, possible ASCII' }
32+
}
33+
34+
Describe "Verifying integrity of module files" {
35+
Context "Validating PS1 Script files" {
36+
$allFiles = Get-ChildItem -Path $moduleRoot -Recurse -Filter "*.ps1" | Where-Object FullName -NotLike "$moduleRoot\tests\*"
37+
38+
foreach ($file in $allFiles)
39+
{
40+
$name = $file.FullName.Replace("$moduleRoot\", '')
41+
42+
It "[$name] Should have UTF8 encoding" {
43+
Get-FileEncoding -Path $file.FullName | Should Be 'UTF8'
44+
}
45+
46+
It "[$name] Should have no trailing space" {
47+
($file | Select-String "\s$" | Where-Object { $_.Line.Trim().Length -gt 0} | Measure-Object).Count | Should Be 0
48+
}
49+
50+
$tokens = $null
51+
$parseErrors = $null
52+
$ast = [System.Management.Automation.Language.Parser]::ParseFile($file.FullName, [ref]$tokens, [ref]$parseErrors)
53+
54+
It "[$name] Should have no syntax errors" {
55+
$parseErrors | Should Be $Null
56+
}
57+
58+
foreach ($command in $global:BannedCommands)
59+
{
60+
if ($global:MayContainCommand["$command"] -notcontains $file.Name)
61+
{
62+
It "[$name] Should not use $command" {
63+
$tokens | Where-Object Text -EQ $command | Should Be $null
64+
}
65+
}
66+
}
67+
68+
It "[$name] Should not contain aliases" {
69+
$tokens | Where-Object TokenFlags -eq CommandName | Where-Object { Test-Path "alias:\$($_.Text)" } | Measure-Object | Select-Object -ExpandProperty Count | Should Be 0
70+
}
71+
}
72+
}
73+
74+
Context "Validating help.txt help files" {
75+
$allFiles = Get-ChildItem -Path $moduleRoot -Recurse -Filter "*.help.txt" | Where-Object FullName -NotLike "$moduleRoot\tests\*"
76+
77+
foreach ($file in $allFiles)
78+
{
79+
$name = $file.FullName.Replace("$moduleRoot\", '')
80+
81+
It "[$name] Should have UTF8 encoding" {
82+
Get-FileEncoding -Path $file.FullName | Should Be 'UTF8'
83+
}
84+
85+
It "[$name] Should have no trailing space" {
86+
($file | Select-String "\s$" | Where-Object { $_.Line.Trim().Length -gt 0 } | Measure-Object).Count | Should Be 0
87+
}
88+
}
89+
}
90+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# List of functions that should be ignored
2+
$global:FunctionHelpTestExceptions = @(
3+
4+
)
5+
6+
<#
7+
List of arrayed enumerations. These need to be treated differently. Add full name.
8+
Example:
9+
10+
"Sqlcollaborative.Dbatools.Connection.ManagementConnectionType[]"
11+
#>
12+
$global:HelpTestEnumeratedArrays = @(
13+
)
14+
15+
<#
16+
Some types on parameters just fail their validation no matter what.
17+
For those it becomes possible to skip them, by adding them to this hashtable.
18+
Add by following this convention: <command name> = @(<list of parameter names>)
19+
Example:
20+
21+
"Get-DbaCmObject" = @("DoNotUse")
22+
#>
23+
$global:HelpTestSkipParameterType = @{
24+
25+
}
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
<#
2+
.NOTES
3+
The original test this is based upon was written by June Blender.
4+
After several rounds of modifications it stands now as it is, but the honor remains hers.
5+
6+
Thank you June, for all you have done!
7+
8+
.DESCRIPTION
9+
This test evaluates the help for all commands in a module.
10+
11+
.PARAMETER SkipTest
12+
Disables this test.
13+
14+
.PARAMETER CommandPath
15+
List of paths under which the script files are stored.
16+
This test assumes that all functions have their own file that is named after themselves.
17+
These paths are used to search for commands that should exist and be tested.
18+
Will search recursively and accepts wildcards, make sure only functions are found
19+
20+
.PARAMETER ModuleName
21+
Name of the module to be tested.
22+
The module must already be imported
23+
24+
.PARAMETER ExceptionsFile
25+
File in which exceptions and adjustments are configured.
26+
In it there should be two arrays and a hashtable defined:
27+
$global:FunctionHelpTestExceptions
28+
$global:HelpTestEnumeratedArrays
29+
$global:HelpTestSkipParameterType
30+
These can be used to tweak the tests slightly in cases of need.
31+
See the example file for explanations on each of these usage and effect.
32+
#>
33+
[CmdletBinding()]
34+
Param (
35+
[switch]
36+
$SkipTest,
37+
38+
[string[]]
39+
$CommandPath = @("$PSScriptRoot\..\..\functions", "$PSScriptRoot\..\..\internal\functions"),
40+
41+
[string]
42+
$ModuleName = "PSModuleDevelopment",
43+
44+
[string]
45+
$ExceptionsFile = "$PSScriptRoot\Help.Exceptions.ps1"
46+
)
47+
if ($SkipTest) { return }
48+
. $ExceptionsFile
49+
50+
$includedNames = (Get-ChildItem $CommandPath -Recurse -File | Where-Object Name -like "*.ps1").BaseName
51+
$commands = Get-Command -Module (Get-Module $ModuleName) -CommandType Cmdlet, Function, Workflow | Where-Object Name -in $includedNames
52+
53+
## When testing help, remember that help is cached at the beginning of each session.
54+
## To test, restart session.
55+
56+
57+
foreach ($command in $commands) {
58+
$commandName = $command.Name
59+
60+
# Skip all functions that are on the exclusions list
61+
if ($global:FunctionHelpTestExceptions -contains $commandName) { continue }
62+
63+
# The module-qualified command fails on Microsoft.PowerShell.Archive cmdlets
64+
$Help = Get-Help $commandName -ErrorAction SilentlyContinue
65+
$testhelperrors = 0
66+
$testhelpall = 0
67+
Describe "Test help for $commandName" {
68+
69+
$testhelpall += 1
70+
if ($Help.Synopsis -like '*`[`<CommonParameters`>`]*') {
71+
# If help is not found, synopsis in auto-generated help is the syntax diagram
72+
It "should not be auto-generated" {
73+
$Help.Synopsis | Should Not BeLike '*`[`<CommonParameters`>`]*'
74+
}
75+
$testhelperrors += 1
76+
}
77+
78+
$testhelpall += 1
79+
if ([String]::IsNullOrEmpty($Help.Description.Text)) {
80+
# Should be a description for every function
81+
It "gets description for $commandName" {
82+
$Help.Description | Should Not BeNullOrEmpty
83+
}
84+
$testhelperrors += 1
85+
}
86+
87+
$testhelpall += 1
88+
if ([String]::IsNullOrEmpty(($Help.Examples.Example | Select-Object -First 1).Code)) {
89+
# Should be at least one example
90+
It "gets example code from $commandName" {
91+
($Help.Examples.Example | Select-Object -First 1).Code | Should Not BeNullOrEmpty
92+
}
93+
$testhelperrors += 1
94+
}
95+
96+
$testhelpall += 1
97+
if ([String]::IsNullOrEmpty(($Help.Examples.Example.Remarks | Select-Object -First 1).Text)) {
98+
# Should be at least one example description
99+
It "gets example help from $commandName" {
100+
($Help.Examples.Example.Remarks | Select-Object -First 1).Text | Should Not BeNullOrEmpty
101+
}
102+
$testhelperrors += 1
103+
}
104+
105+
if ($testhelperrors -eq 0) {
106+
It "Ran silently $testhelpall tests" {
107+
$testhelperrors | Should be 0
108+
}
109+
}
110+
111+
$testparamsall = 0
112+
$testparamserrors = 0
113+
Context "Test parameter help for $commandName" {
114+
115+
$Common = 'Debug', 'ErrorAction', 'ErrorVariable', 'InformationAction', 'InformationVariable', 'OutBuffer', 'OutVariable',
116+
'PipelineVariable', 'Verbose', 'WarningAction', 'WarningVariable'
117+
118+
$parameters = $command.ParameterSets.Parameters | Sort-Object -Property Name -Unique | Where-Object Name -notin $common
119+
$parameterNames = $parameters.Name
120+
$HelpParameterNames = $Help.Parameters.Parameter.Name | Sort-Object -Unique
121+
foreach ($parameter in $parameters) {
122+
$parameterName = $parameter.Name
123+
$parameterHelp = $Help.parameters.parameter | Where-Object Name -EQ $parameterName
124+
125+
$testparamsall += 1
126+
if ([String]::IsNullOrEmpty($parameterHelp.Description.Text)) {
127+
# Should be a description for every parameter
128+
It "gets help for parameter: $parameterName : in $commandName" {
129+
$parameterHelp.Description.Text | Should Not BeNullOrEmpty
130+
}
131+
$testparamserrors += 1
132+
}
133+
134+
$testparamsall += 1
135+
$codeMandatory = $parameter.IsMandatory.toString()
136+
if ($parameterHelp.Required -ne $codeMandatory) {
137+
# Required value in Help should match IsMandatory property of parameter
138+
It "help for $parameterName parameter in $commandName has correct Mandatory value" {
139+
$parameterHelp.Required | Should Be $codeMandatory
140+
}
141+
$testparamserrors += 1
142+
}
143+
144+
if ($HelpTestSkipParameterType[$commandName] -contains $parameterName) { continue }
145+
146+
$codeType = $parameter.ParameterType.Name
147+
148+
$testparamsall += 1
149+
if ($parameter.ParameterType.IsEnum) {
150+
# Enumerations often have issues with the typename not being reliably available
151+
$names = $parameter.ParameterType::GetNames($parameter.ParameterType)
152+
if ($parameterHelp.parameterValueGroup.parameterValue -ne $names) {
153+
# Parameter type in Help should match code
154+
It "help for $commandName has correct parameter type for $parameterName" {
155+
$parameterHelp.parameterValueGroup.parameterValue | Should be $names
156+
}
157+
$testparamserrors += 1
158+
}
159+
}
160+
elseif ($parameter.ParameterType.FullName -in $HelpTestEnumeratedArrays) {
161+
# Enumerations often have issues with the typename not being reliably available
162+
$names = [Enum]::GetNames($parameter.ParameterType.DeclaredMembers[0].ReturnType)
163+
if ($parameterHelp.parameterValueGroup.parameterValue -ne $names) {
164+
# Parameter type in Help should match code
165+
It "help for $commandName has correct parameter type for $parameterName" {
166+
$parameterHelp.parameterValueGroup.parameterValue | Should be $names
167+
}
168+
$testparamserrors += 1
169+
}
170+
}
171+
else {
172+
# To avoid calling Trim method on a null object.
173+
$helpType = if ($parameterHelp.parameterValue) { $parameterHelp.parameterValue.Trim() }
174+
if ($helpType -ne $codeType) {
175+
# Parameter type in Help should match code
176+
It "help for $commandName has correct parameter type for $parameterName" {
177+
$helpType | Should be $codeType
178+
}
179+
$testparamserrors += 1
180+
}
181+
}
182+
}
183+
foreach ($helpParm in $HelpParameterNames) {
184+
$testparamsall += 1
185+
if ($helpParm -notin $parameterNames) {
186+
# Shouldn't find extra parameters in help.
187+
It "finds help parameter in code: $helpParm" {
188+
$helpParm -in $parameterNames | Should Be $true
189+
}
190+
$testparamserrors += 1
191+
}
192+
}
193+
if ($testparamserrors -eq 0) {
194+
It "Ran silently $testparamsall tests" {
195+
$testparamserrors | Should be 0
196+
}
197+
}
198+
}
199+
}
200+
}

0 commit comments

Comments
 (0)