1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
|
# (c) 2025 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
using namespace System.Collections.Generic
using namespace System.IO
using namespace System.Management.Automation
using namespace System.Management.Automation.Language
using namespace System.Reflection
using namespace System.Text
param(
[Parameter(Mandatory)]
[string]
$ModuleName,
[Parameter(Mandatory)]
[string]
$OutputPath,
[Parameter(Mandatory)]
[string]
$PathFilter
)
# Required to be set for psrp so we can set a breakpoint in the remote runspace
$Host.Runspace.Debugger.SetDebugMode([DebugModes]::RemoteScript)
Function New-CoverageBreakpointsForScriptBlock {
Param (
[Parameter(Mandatory)]
[string]
$ScriptName,
[Parameter(Mandatory)]
[ScriptBlockAst]
$ScriptBlockAst,
[Parameter(Mandatory)]
[String]
$AnsiblePath
)
$predicate = {
$args[0] -is [CommandBaseAst]
}
$scriptCmds = $ScriptBlockAst.FindAll($predicate, $true)
# Create an object that tracks the Ansible path of the file and the breakpoints that have been set in it
$info = [PSCustomObject]@{
Path = $AnsiblePath
Breakpoints = [List[Breakpoint]]@()
}
# LineBreakpoint was only made public in PowerShell 6.0 so we need to use
# reflection to achieve the same thing in 5.1.
$lineCtor = if ($PSVersionTable.PSVersion -lt '6.0') {
[LineBreakpoint].GetConstructor(
[BindingFlags]'NonPublic, Instance',
$null,
[type[]]@([string], [int], [int], [scriptblock]),
$null)
}
else {
[LineBreakpoint]::new
}
# Keep track of lines that are already scanned. PowerShell can contains multiple commands in 1 line
$scannedLines = [HashSet[int]]@()
foreach ($cmd in $scriptCmds) {
if (-not $scannedLines.Add($cmd.Extent.StartLineNumber)) {
continue
}
# Action is explicitly $null as it will slow down the runtime quite dramatically.
$b = $lineCtor.Invoke(@($ScriptName, $cmd.Extent.StartLineNumber, $cmd.Extent.StartColumnNumber, $null))
$info.Breakpoints.Add($b)
}
[Runspace]::DefaultRunspace.Debugger.SetBreakpoints($info.Breakpoints)
$info
}
Function Compare-PathFilterPattern {
Param (
[String[]]$Patterns,
[String]$Path
)
foreach ($pattern in $Patterns) {
if ($Path -like $pattern) {
return $true
}
}
return $false
}
$actionInfo = Get-NextAnsibleAction
$actionParams = $actionInfo.Parameters
# A PS Breakpoint needs a path to be associated with the ScriptBlock, luckily
# the Get-AnsibleScript does this for us.
$breakpointInfo = @()
try {
$coveragePathFilter = $PathFilter.Split(":", [StringSplitOptions]::RemoveEmptyEntries)
$breakpointInfo = @(
foreach ($scriptName in @($ModuleName; $actionParams.PowerShellModules)) {
# We don't use -IncludeScriptBlock as the script might be untrusted
# and will run under CLM. While we recreate the ScriptBlock here it
# is only to get the AST that contains the statements and their
# line numbers to create the breakpoint info for.
$scriptInfo = Get-AnsibleScript -Name $scriptName
if (Compare-PathFilterPattern -Patterns $coveragePathFilter -Path $scriptInfo.Path) {
$covParams = @{
ScriptName = $scriptInfo.Name
ScriptBlockAst = [ScriptBlock]::Create($scriptInfo.Script).Ast
AnsiblePath = $scriptInfo.Path
}
New-CoverageBreakpointsForScriptBlock @covParams
}
}
)
if ($breakpointInfo) {
$actionParams.Breakpoints = $breakpointInfo.Breakpoints
}
try {
& $actionInfo.ScriptBlock @actionParams
}
finally {
# Processing here is kept to an absolute minimum to make sure each task runtime is kept as small as
# possible. Once all the tests have been run ansible-test will collect this info and process it locally in
# one go.
$coverageInfo = @{}
foreach ($info in $breakpointInfo) {
$coverageInfo[$info.Path] = $info.Breakpoints | Select-Object -Property Line, HitCount
}
$psVersion = "$($PSVersionTable.PSVersion.Major).$($PSVersionTable.PSVersion.Minor)"
$coverageOutputPath = "$OutputPath=powershell-$psVersion=coverage.$($env:COMPUTERNAME).$PID.$(Get-Random)"
$codeCovJson = ConvertTo-Json -InputObject $coverageInfo -Compress
# Ansible controller expects these files to be UTF-8 without a BOM, use .NET for this.
$utf8 = [UTF8Encoding]::new($false)
[File]::WriteAllText($coverageOutputPath, $codeCovJson, $utf8)
}
}
finally {
foreach ($b in $breakpointInfo.Breakpoints) {
Remove-PSBreakpoint -Breakpoint $b
}
}
|