Skip to content

Instantly share code, notes, and snippets.

@robbratton
Last active January 30, 2024 19:52
Show Gist options
  • Save robbratton/dcd1e9c41cf9d5eb737e8c81608ce9cb to your computer and use it in GitHub Desktop.
Save robbratton/dcd1e9c41cf9d5eb737e8c81608ce9cb to your computer and use it in GitHub Desktop.
Sample Launch file for Visual Studio Code for PowerShell

Sample launch.json for PowerShell in Visual Studio Code with Testing Scripts

This example includes configurations to start PowerShell in various ways including checking syntax and running tests with code coverage

Requirements

Folders

  • The includes scripts must be in this folder: C:\git\PerfAndMon-Tools\Scripts\Tests

Visual Studio Code

  • Visual Studio Code 1.85+ must be installed.
  • The PowerShell extension installed must be installed in Visual Studio Code
  • The Pester extension(s) installed must be installed in Visual Studio Code

PowerShell

  • PowerShell 7+ must be installed.
  • Pester 5+ must be installed.
  • PowerShell ScriptAnalyzer must be installed.
Import-Module PNMExcel
Import-Module PNMNLog
$ScriptFolder = (Split-Path $PSCommandPath)
$OutputSuffix = Get-DateTimeForFilename
$CheckFolder = "C:\git\PerfAndMon-Tools"
[void](New-NLogLogger -LogDirectory $ScriptFolder)
$ErrorActionPreference = 'stop'
# Get Aliases
$Aliases = @(Get-ChildItem -Path $CheckFolder -Filter '*.psm1' -Recurse | ForEach-Object {
$FileInfo = $_
(Select-String -Path $_ -Pattern '^New-alias -Name ["''](.+)["''] -Value ["''](.+)["'']') | ForEach-Object {
if ($_ -Match '-Name ["''](.+)["''] -Value ["''](.+)["'']') {
@{FileInfo = $FileInfo; NewName = $Matches[2]; OldName = $matches[1] }
}
}
}) | Sort-Object NewName, OldName | Select-Object NewName, OldName, FileInfo
Write-ExcelFile -OutputItems $Aliases -Path "$($ScriptFolder)\Aliases_$($OutputSuffix).xlsx" -MaxWidth 200
# Get Alias Tests
$Tests = @(Get-ChildItem -Path $CheckFolder -Filter '*Tests.ps1' -Recurse | ForEach-Object {
$FileInfo = $_
(Select-String -Path $_ -Pattern 'Describe ["'']<_> \(alias\)["'']') | ForEach-Object {
if ($_ -Match '@\(["''](.+)["''], ["''](.+)["'']\)' ) {
@{FileInfo = $FileInfo; NewName = $Matches[1]; OldName = $matches[2] }
}
}
}) | Sort-Object NewName, OldName | Select-Object NewName, OldName, FileInfo
Write-ExcelFile -OutputItems $Tests -Path "$($ScriptFolder)\Tests_$($OutputSuffix).xlsx" -MaxWidth 200
Write-Output ''
Write-Output '-----------------------------------'
Write-Output ''
Write-Output 'ALIASES'
Write-Output ''
#$Aliases | Format-table -Autosize
$Aliases | Group-Object -Property FileInfo | ForEach-Object {
$GroupItem = $_
Write-Host "File '$($GroupItem.Name)' Total Count $($GroupItem.Count)"
foreach ($alias in $GroupItem.Group) {
$test = $Tests | Where-Object { $_.OldName -eq $alias.OldName -or $_.NewName -eq $alias.NewName }
if (!$test) {
Write-Host " Alias $($alias.OldName) is not tested"
}
}
Write-Host ''
}
Write-Output ''
Write-Output '-----------------------------------'
Write-Output ''
Write-Output 'TESTS'
Write-Output ''
#$Tests | Format-table -Autosize
$Tests | Group-Object -Property FileInfo | ForEach-Object {
$GroupItem = $_
Write-Host "FIle '$($GroupItem.Name)' Total Count $($GroupItem.Count)"
foreach ($test in $GroupItem.Group) {
$alias = $Aliases | Where-Object { $_.OldName -eq $test.OldName -or $_.NewName -eq $test.NewName }
if (!$alias) {
Write-Host " Test $($test.OldName) is not for an alias"
}
}
Write-Host ''
}
{
"version": "0.2.0",
"configurations": [
{
"type": "PowerShell",
"request": "launch",
"name": "PowerShell Launch (current file)",
"script": "${file}",
"args": [],
"cwd": "${file}"
},
{
"type": "PowerShell",
"request": "attach",
"name": "PowerShell Attach to Host Process",
"processId": "${command.PickPSHostProcess}",
"runspaceId": 1
},
{
"type": "PowerShell",
"request": "launch",
"name": "PowerShell Interactive Session",
"cwd": "${workspaceRoot}"
},
{
"type": "PowerShell",
"request": "launch",
"name": "Check PowerShell Syntax (Folder) - Job",
"script": "Start-Job { C:\\git\\perfAndMon-Tools\\scripts\\Tests\\testSyntax.ps1 -path '${fileDirname}' -CheckType 'ScriptAnalyzer' -Recurse $true }",
"cwd": "${file}"
},
{
"type": "PowerShell",
"request": "launch",
"name": "Check PowerShell Syntax (Folder)",
"script": "C:\\git\\perfAndMon-Tools\\scripts\\Tests\\testSyntax.ps1 -path '${fileDirname}' -CheckType 'ScriptAnalyzer' -Recurse $true",
"cwd": "${file}"
},
{
"type": "PowerShell",
"request": "launch",
"name": "Run Pester with Coverage (Folder) - JOB",
"script": "Start-Job { C:\\git\\PerfAndMon-Tools\\Scripts\\Tests\\RunTests.ps1 -RunPath '${fileDirname}' -CodeCoverageEnabled $true -TestResultEnabled $true }",
"cwd": "${file}"
},
{
"type": "PowerShell",
"request": "launch",
"name": "Run Pester with Coverage (Folder)",
"script": "C:\\git\\PerfAndMon-Tools\\Scripts\\Tests\\RunTests.ps1 -RunPath '${fileDirname}' -CodeCoverageEnabled $true -TestResultEnabled $true",
"cwd": "${file}"
}
]
}
# WARNING: If this doesn't work inside of VS Code, run it from a command prompt.
[CmdletBinding()]
param (
[Parameter(Mandatory = $false)][ValidateNotNull()][String] $RunPath = 'C:\git\PerfAndMon-Tools\Scripts\Utilities\DeployScript',
[Parameter(Mandatory = $false)][String[]] $RunExcludePath = $null,
[Parameter(Mandatory = $false)][nullable[bool]] $CodeCoverageEnabled = $False,
[Parameter(Mandatory = $false)][nullable[bool]] $TestResultEnabled = $true,
[Parameter(Mandatory = $false)][String] $OutputVerbosity = 'Normal',
[Parameter(Mandatory = $false)][String] $OutputRenderMode = 'PlainText'
)
begin {
# ---------- Configuration ----------
#Requires -Version 7.3
Set-StrictMode -Version 3.0
$ErrorActionPreference = 'Stop' # Normal
#$ErrorActionPreference = 'Inquire' # For debugging
#$ErrorActionPreference = 'break' # For debugging
# ---------- Module Imports ----------
Import-Module "$PsScriptRoot\PNMTests.psm1"
}
process {
Write-Output 'Starting a test run...'
#$scriptPath = $MyInvocation.MyCommand.Path | Split-Path
Push-Location $RunPath
try {
$container = New-PesterContainer -Path $RunPath #-Data @()
# Reference: https://pester-docs.netlify.app/docs/commands/New-PesterConfiguration
# Reference: https://pester.dev/docs/usage/Configuration
$configuration = New-PesterConfiguration
$configuration.Run.Container = $container
# if (-not [string]::IsNullOrWhiteSpace($RunPath)) {
# #$configuration.Run.Path = "." # Default
# $configuration.Run.Path = $RunPath
# }
if ($null -ne $RunExcludePath) {
#$configuration.Run.ExcludePath = @() # Default
$configuration.Run.ExcludePath = $RunExcludePath #@("c\PerfandMon-Tools\Scripts\Tests", "c\PerfandMon-Tools\Scripts\Prototypes")
}
if ($null -ne $CodeCoverageEnabled) {
$configuration.CodeCoverage.Enabled = $CodeCoverageEnabled
}
if ($null -ne $TestResultEnabled) {
$configuration.TestResult.Enabled = $TestResultEnabled
}
if (-not [string]::IsNullOrWhiteSpace($OutputVerbosity)) {
#$configuration.Output.Verbosity = "Normal" # Default
$configuration.Output.Verbosity = $OutputVerbosity
}
if (-not [string]::IsNullOrWhiteSpace($OutputRenderMode)) {
#$Configuration.Output.RenderMode = "Auto" # Default
$Configuration.Output.RenderMode = $OutputRenderMode
}
# Write configuration to JSON file
$pesterConfigPath = "$($Env:UserProfile)\AppData\Local\PesterConfig.json"
Write-Output "Running Pester with the settings in '$($pesterConfigPath)'"
#$JsonContent = ConvertTo-Json -InputObject $Configuration -Depth 100
#Set-Content -Path $pesterConfigPath -Value $JsonContent
#Invoke-Item $pesterConfigPath
if ($configuration.TestResult.Enabled) {
$testResultsFolder = Join-Path -Path (ResolvePathForce -Path $RunPath) -ChildPath 'TestResults'
$configuration.TestResult.OutputPath = Join-Path -Path $TestResultsFolder -ChildPath 'testResults.xml'
if (-not (Test-Path -Path $testResultsFolder -PathType Container -ErrorAction SilentlyContinue)) {
New-Item -Path $testResultsFolder -ItemType Directory | Out-Null
}
else {
Write-Output 'Removing old test result files'
Remove-Item -Path "$testResultsFolder\*.*" -Recurse
}
}
if ($configuration.CodeCoverage.Enabled) {
$coverageFolder = Join-Path -Path (ResolvePathForce -Path $RunPath) -ChildPath 'Coverage'
$configuration.CodeCoverage.OutputPath = Join-Path -Path $coverageFolder -ChildPath 'coverage.xml'
if (-not (Test-Path -Path $coverageFolder -PathType Container)) {
New-Item -Path $coverageFolder -ItemType Directory | Out-Null
}
else {
Write-Output 'Removing old coverage files'
Remove-Item -Path "$coverageFolder\*.*" -Recurse
}
}
try {
Write-Output 'Invoking Pester...'
Invoke-Pester -Configuration $Configuration #-verbose -debug
Write-Output 'Pester finished.'
}
catch {
Write-Warning $PSitem
$exception = [System.Exception]::new('Running Pester failed.', $PSItem.Exception)
throw $exception
}
# if ($configuration.TestResult.Enabled) {
# if (Test-Path $configuration.TestResult.OutputPath.Value -ErrorAction SilentlyContinue ) {
# Invoke-Item (ResolvePathForce -Path $configuration.TestResult.OutputPath.Value)
# }
# }
if ($configuration.CodeCoverage.Enabled) {
Write-Output 'Generating the Coverage Report...'
& C:\bin\ReportGenerator\net7.0\ReportGenerator.exe "-reports:$($configuration.CodeCoverage.OutputPath.Value)" "-SourceDirs:$($configuration.Run.Path.Value)" "-TargetDir:$($coverageFolder)" '-License:ewogICJMaWNlbnNlIjogewogICAgIklkIjogImVjYjNhYzVkLTM2YWMtNDQ2Mi05MjlkLWEyNjU1NjA1OTc3ZiIsCiAgICAiTG9naW4iOiAicm9iYnJhdHRvbiIsCiAgICAiTmFtZSI6ICJSb2JlcnQgRS4gQnJhdHRvbiIsCiAgICAiRW1haWwiOiBudWxsLAogICAgIkxpY2Vuc2VUeXBlIjogIlBybyIsCiAgICAiSXNzdWVkQXQiOiAiMjAyMy0wNC0wN1QxMzozODoyMy42Nzc3Njk0WiIKICB9LAogICJTaWduYXR1cmUiOiAiQ2tsTTh1RUFwdlx1MDAyQlhcdTAwMkJLVXJXZi9QQ3pMM0NmTGFYNEtEZ0V1Q1BTUlo2bmJrNW9HcFQ5VTFUXHUwMDJCUUdaamIyQ1JtbGlrNk00Skg5Y1VxaXNsY2svbEx3amIvZVlKVUNRUFJucEdxbjdCUDFpNjA3OUVPSksvaWZReXNQT2Rlc2JMT1x1MDAyQnhhME9hS2x0RmxJbkR6ZUsxTlhwei91SkJQXHUwMDJCRFx1MDAyQnd0ekVlaXkwMVpuL0p5dFh0MFpoengvNm82QzJUcFx1MDAyQko2ancxa0Q4Yy83dHlZWUtKWVdtaWhcdTAwMkJcdTAwMkIxU3Z1cE05dDFPV1x1MDAyQkx6VWpZNzZIRU9pa1pvQTlJcURXMUlPQlFBMGpzQ21SQVZnVEhaQ0I5TnF4S1FRVElmS3NQbWN5MGk2NEtEZGVaUXVJR2lCSWg5UWNcdTAwMkI5RDZwL1VPQ0liZTBnTk5scFhSVUZaMEkzMHAvQ0xMa2dMYXJ1aFJsdz09Igp9'
$coverageFile = "$($coverageFolder)\Index.html"
if (-not (Test-Path $coverageFile -PathType Leaf -ErrorAction SilentlyContinue )) {
Throw 'Coverage HTML file does not exist.'
}
Write-Output 'Launching the Coverage Report...'
Invoke-Item $CoverageFile
}
}
catch {
Write-Error "The script failed: $($PSItem)"
}
Finally {
Pop-Location
}
}
end {
Write-Output 'Script is finished.'
}
pwsh -NonInteractive -commandwithargs "C:\git\PerfAndMon-Tools\Scripts\Tests\RunTests.ps1 -RunPath 'C:\git\PerfAndMon-Tools\Scripts\Modules' -CodeCoverageEnabled $False"
pwsh -NonInteractive -commandwithargs "C:\git\PerfAndMon-Tools\Scripts\Tests\RunTests.ps1 -RunPath 'C:\git\PerfAndMon-Tools\Scripts\Modules' -CodeCoverageEnabled $True"
pwsh -NonInteractive -commandwithargs "C:\git\PerfAndMon-Tools\Scripts\Tests\RunTests.ps1 -RunPath '.' -CodeCoverageEnabled $True"
# get-Module | Remove-Module -Force
Write-Output 'Finding PSD files...'
$PSDFiles = Get-ChildItem 'C:\git\PerfAndMon-Tools\Scripts\Modules' -Filter '*.psd1' -Recurse
Write-Output "Testing $($PsdFiles.Count) PSDs..."
Write-Output ''
ForEach ($PsdFile in $PsdFiles) {
Write-Verbose "Testing manifest $($PSDFile.Name)"
do {
try {
Test-ModuleManifest $PSDFile.FullName -ErrorAction Stop
#Write-Output "$($PsdFile.Name) succeeded."
$Repeat = $false
}
catch {
Write-Warning "$($PsdFile.Name) failed. $PSItem"
#Read-Host "Press enter to try again."
$Repeat = $true
}
} while ($Repeat)
}
<#
.SYNOPSIS
Test the syntax of a source file
.INPUTS
A PowerShell source file.
.OUTPUTS
A message saying there were no syntax errors.
OR
A temp Excel file with the list of errors.
#>
[CmdletBinding()]
param (
# The PS1 or PSM1 file to be checked.
[Parameter(Mandatory = $false)][String] $Path = '.',
[Parameter(Mandatory = $false)][boolean] $Recurse = $false,
# Method of checking files' syntax: ScriptAnalyzer or ParseInput
[Parameter(Mandatory = $false)][String][ValidateSet('ScriptAnalyzer', 'ParseInput')] $CheckType = 'ScriptAnalyzer'
)
$ErrorActionPreference = 'stop'
Write-Output "Checking the path '$($Path)' with tool '$($CheckType)' recursively $($Recurse)..."
Switch ($CheckType) {
'ScriptAnalyzer' {
$result = @(Invoke-ScriptAnalyzer $Path -Recurse:$Recurse) #-verbose
if (!$result) {
Write-Output 'No errors were found.'
}
else {
Write-Output "$($result.Count) issues were found. Loading Excel document."
$result | Select-Object * | Export-Excel #.\Analyzer.xlsx
}
}
'ParseInput' {
$files = @(Get-ChildItem $Path -Recurse:$Recurse -Include @('*.ps1', '*.psm1'))
$allErrors = @()
foreach ($file in $files) {
Write-Output "Checking $($file)..."
$inputScript = Get-Content $file
$Errors = @()
[void][System.Management.Automation.Language.Parser]::ParseInput($inputScript, [ref]$null, [ref]$Errors)
$TempErrors = $Errors
$TempErrors | Add-Member -Name 'file' -MemberType NoteProperty -Value $file
$AllErrors += $TempErrors
}
if ($AllErrors.Count -eq 0) {
Write-Output 'No errors were found.'
}
else {
$AllErrors | Select-Object * | Export-Excel
}
}
default {
throw 'Invalid check type'
}
}
<#
.SYNOPSIS
Test the syntax of a source file
.INPUTS
A PowerShell source file.
.OUTPUTS
A message saying there were no syntax errors.
OR
A temp Excel file with the list of errors.
#>
[CmdletBinding()]
param (
# The PS1 or PSM1 file to be checked.
[Parameter(Mandatory = $false)][String] $Path = '.',
[Parameter(Mandatory = $false)][boolean] $Recurse = $false,
# Method of checking files' syntax: ScriptAnalyzer or ParseInput
[Parameter(Mandatory = $false)][String][ValidateSet('ScriptAnalyzer', 'ParseInput')] $CheckType = 'ScriptAnalyzer'
)
$ErrorActionPreference = 'stop'
Write-Output "Checking the path '$($Path)' with tool '$($CheckType)' recursively $($Recurse)..."
Switch ($CheckType) {
'ScriptAnalyzer' {
$result = @(Invoke-ScriptAnalyzer $Path -Recurse:$Recurse) #-verbose
if (!$result) {
Write-Output 'No errors were found.'
}
else {
Write-Output "$($result.Count) issues were found. Loading Excel document."
$result | Select-Object * | Export-Excel #.\Analyzer.xlsx
}
}
'ParseInput' {
$files = @(Get-ChildItem $Path -Recurse:$Recurse -Include @('*.ps1', '*.psm1'))
$allErrors = @()
foreach ($file in $files) {
Write-Output "Checking $($file)..."
$inputScript = Get-Content $file
$Errors = @()
[void][System.Management.Automation.Language.Parser]::ParseInput($inputScript, [ref]$null, [ref]$Errors)
$TempErrors = $Errors
$TempErrors | Add-Member -Name 'file' -MemberType NoteProperty -Value $file
$AllErrors += $TempErrors
}
if ($AllErrors.Count -eq 0) {
Write-Output 'No errors were found.'
}
else {
$AllErrors | Select-Object * | Export-Excel
}
}
default {
throw 'Invalid check type'
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment