File: coverage_wrapper.ps1

package info (click to toggle)
ansible-core 2.19.4-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 32,960 kB
  • sloc: python: 181,477; cs: 4,929; sh: 4,697; xml: 34; makefile: 21
file content (155 lines) | stat: -rw-r--r-- 5,024 bytes parent folder | download | duplicates (2)
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
    }
}