As I resolved at the end of last year, I am doing more with Pester in 2022. I'm getting a bit more comfortable with Pester 5 and as my tests grow in complexity I am embracing the use of tags. You can add tags to different Pester test elements. Then when you invoke a Pester test, you can filter and only run specific tests by their tag. As I was working, I realized it would be helpful to be able to identify all of the tags in a test script. After a bit of work, I came up with a PowerShell function.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
My initial thought was to use regular expressions and look for lines with -tag.
$path = "C:\scripts\psfunctiontools\tests\psfunctiontools.tests.ps1"
[regex]$rx="(?<=-tag\s)(\w(,)?(\s)?)*"
$c = Get-Content $path
#make all tags lower case
([System.Text.RegularExpressions.Regex]::Matches($c,$rx,"IgnoreCase")).foreach({($_.value -split ",").Trim().toLower()}) |
Select-Object -Unique | Sort-Object
This code works but is a bit of a brute-force approach. I had to take into account multiple tags and syntax variations. So I went back to the PowerShell AST.
Parsing the file is simple enough.
$AST = [System.Management.Automation.Language.Parser]::ParseFile(
$path,
[ref]$astTokens,
[ref]$astErr
)
In a Pester test, tags are technically part of a -Tag parameter for a Pester command like Describe or It. This means I can discover these command elements.
$tags = $AST.FindAll({
$args[0] -is [System.Management.Automation.Language.CommandParameterAst] -AND $args[0].parametername -eq 'tag' },
$true
)
Now for the tricky part. Here's a sample object.
It would be nice if the Argument property contained the values "help", and "acceptance" which you can see in the parent. But that would be too easy. However, I can use the Parent property and its CommandElements.
I'm showing you the last two elements. The Tag parameter value is the element after the parameter element. This might make it clearer.
for ($i = 0;$i -lt $tags[0].Parent.CommandElements.count;$i++) {
if ($tags[0].parent.CommandElements[$i].parametername -eq 'tag') {
$tags[0].parent.CommandElements[$i+1].extent.text.split(",").trim().tolower()
}
}
I can loop through all the command elements using a For enumeration loop. If the parametername property is 'tag', then get the next element ($i+1) and use the Extent.Text property. It might be an array so I'll split it and trim up extra spaces.
All I need to do now is step back and do this for all tag elements with a set of nested For loops.
The last step is to get unique items. Notice that I'm also making every entry lowercase to ensure I'm truly getting unique values.
Get-PesterTag
With this core functionality, I built a PowerShell function around it.
Function Get-PesterTag {
[cmdletbinding()]
[OutputType("pesterTag")]
Param(
[Parameter(
Position = 0,
Mandatory,
HelpMessage = "Specify a Pester test file",
ValueFromPipeline
)]
[ValidateScript({
#validate file exits
if (Test-Path $_) {
#now test for extension
if ($_ -match "\.ps1$") {
$True
}
else {
Throw "The filename must end in '.ps1'."
}
}
else {
Throw "Cannot find file $_."
}
})]
[string]$Path
)
Begin {
Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Starting $($myinvocation.mycommand)"
New-Variable astTokens -Force
New-Variable astErr -Force
} #begin
Process {
Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Getting tags from $Path "
$AST = [System.Management.Automation.Language.Parser]::ParseFile(
$path,
[ref]$astTokens,
[ref]$astErr
)
$tags = $AST.FindAll({
$args[0] -is [System.Management.Automation.Language.CommandParameterAst] -AND $args[0].parametername -eq 'tag' },
$true
)
$all = for ($j = 0; $j -lt $tags.count; $j++) {
for ($i = 0; $i -lt $tags[$j].Parent.CommandElements.count; $i++) {
if ($tags[$j].parent.CommandElements[$i].parametername -eq 'tag') {
$tags[$j].parent.CommandElements[$i + 1].extent.text.split(",").trim().tolower()
}
}
}
if ($all) {
[pscustomobject]@{
PSTypename = "pesterTag"
Path = $Path
Tags = $all | Select-Object -Unique | Sort-Object
}
}
else {
Write-Warning "No tags found in $Path"
}
} #process
End {
Write-Verbose "[$((Get-Date).TimeofDay) END ] Ending $($myinvocation.mycommand)"
} #end
} #close Get-PesterTag
The function writes a custom object to the pipeline which includes the file name.
This was very helpful as I discovered a misspelled tag name. The function includes parameter validation on the path. The validation script first verifies the file exists and then that it ends in .ps1. I used to separate these out into two separate validation tests, but there's no reason not to combine them. The only other thing I could do is create a custom format file for the pesterTag object type. But I'll leave that to you. I need to get back to working on my tests.
3 thoughts on “Discovering Pester Tags with the PowerShell AST”
Comments are closed.