Functions/Coverage.ps1
if ($PSVersionTable.PSVersion.Major -le 2) { function Exit-CoverageAnalysis { } function Get-CoverageReport { } function Write-CoverageReport { } function Enter-CoverageAnalysis { param ( $CodeCoverage ) if ($CodeCoverage) { & $SafeCommands['Write-Error'] 'Code coverage analysis requires PowerShell 3.0 or later.' } } return } function Enter-CoverageAnalysis { [CmdletBinding()] param ( [object[]] $CodeCoverage, [object] $PesterState ) $coverageInfo = foreach ($object in $CodeCoverage) { Get-CoverageInfoFromUserInput -InputObject $object } $PesterState.CommandCoverage = @(Get-CoverageBreakpoints -CoverageInfo $coverageInfo) } function Exit-CoverageAnalysis { param ([object] $PesterState) & $SafeCommands['Set-StrictMode'] -Off # PSScriptAnalyzer it will flag this line because $null is on the LHS of -ne. # BUT that is correct in this case. We are filtering the list of breakpoints # to only get those that are not $null # (like if we did $breakpoints | where {$_ -ne $null}) # so DON'T change this. $breakpoints = @($PesterState.CommandCoverage.Breakpoint) -ne $null if ($breakpoints.Count -gt 0) { & $SafeCommands['Remove-PSBreakpoint'] -Breakpoint $breakpoints } } function Get-CoverageInfoFromUserInput { param ( [Parameter(Mandatory = $true)] [object] $InputObject ) if ($InputObject -is [System.Collections.IDictionary]) { $unresolvedCoverageInfo = Get-CoverageInfoFromDictionary -Dictionary $InputObject } else { $Path = $InputObject -as [string] # Auto-detect IncludeTests-value from path-input $IncludeTests = $Path -match '\.tests\.ps1$' $unresolvedCoverageInfo = New-CoverageInfo -Path $Path -IncludeTests $IncludeTests } Resolve-CoverageInfo -UnresolvedCoverageInfo $unresolvedCoverageInfo } function New-CoverageInfo { param ([string] $Path, [string] $Class = $null, [string] $Function = $null, [int] $StartLine = 0, [int] $EndLine = 0, [bool] $IncludeTests = $false) return [pscustomobject]@{ Path = $Path Class = $Class Function = $Function StartLine = $StartLine EndLine = $EndLine IncludeTests = $IncludeTests } } function Get-CoverageInfoFromDictionary { param ([System.Collections.IDictionary] $Dictionary) [string] $path = Get-DictionaryValueFromFirstKeyFound -Dictionary $Dictionary -Key 'Path', 'p' if ([string]::IsNullOrEmpty($path)) { throw "Coverage value '$Dictionary' is missing required Path key." } $startLine = Get-DictionaryValueFromFirstKeyFound -Dictionary $Dictionary -Key 'StartLine', 'Start', 's' $endLine = Get-DictionaryValueFromFirstKeyFound -Dictionary $Dictionary -Key 'EndLine', 'End', 'e' [string] $class = Get-DictionaryValueFromFirstKeyFound -Dictionary $Dictionary -Key 'Class', 'c' [string] $function = Get-DictionaryValueFromFirstKeyFound -Dictionary $Dictionary -Key 'Function', 'f' $includeTests = Get-DictionaryValueFromFirstKeyFound -Dictionary $Dictionary -Key 'IncludeTests' $startLine = Convert-UnknownValueToInt -Value $startLine -DefaultValue 0 $endLine = Convert-UnknownValueToInt -Value $endLine -DefaultValue 0 [bool] $includeTests = Convert-UnknownValueToInt -Value $includeTests -DefaultValue 0 return New-CoverageInfo -Path $path -StartLine $startLine -EndLine $endLine -Class $class -Function $function -IncludeTests $includeTests } function Convert-UnknownValueToInt { param ([object] $Value, [int] $DefaultValue = 0) try { return [int] $Value } catch { return $DefaultValue } } function Resolve-CoverageInfo { param ([psobject] $UnresolvedCoverageInfo) $path = $UnresolvedCoverageInfo.Path $testsPattern = '\.tests\.ps1$' $includeTests = $UnresolvedCoverageInfo.IncludeTests try { $resolvedPaths = & $SafeCommands['Resolve-Path'] -Path $path -ErrorAction Stop | & $SafeCommands['Where-Object'] { $includeTests -or $_.Path -notmatch $testsPattern } } catch { & $SafeCommands['Write-Error'] "Could not resolve coverage path '$path': $($_.Exception.Message)" return } $filePaths = foreach ($resolvedPath in $resolvedPaths) { $item = & $SafeCommands['Get-Item'] -LiteralPath $resolvedPath if ($item -is [System.IO.FileInfo] -and ('.ps1', '.psm1') -contains $item.Extension) { $item.FullName } elseif (-not $item.PsIsContainer) { & $SafeCommands['Write-Warning'] "CodeCoverage path '$path' resolved to a non-PowerShell file '$($item.FullName)'; this path will not be part of the coverage report." } } $params = @{ StartLine = $UnresolvedCoverageInfo.StartLine EndLine = $UnresolvedCoverageInfo.EndLine Class = $UnresolvedCoverageInfo.Class Function = $UnresolvedCoverageInfo.Function } foreach ($filePath in $filePaths) { $params['Path'] = $filePath New-CoverageInfo @params } } function Get-CoverageBreakpoints { [CmdletBinding()] param ( [object[]] $CoverageInfo ) $fileGroups = @($CoverageInfo | & $SafeCommands['Group-Object'] -Property Path) foreach ($fileGroup in $fileGroups) { & $SafeCommands['Write-Verbose'] "Initializing code coverage analysis for file '$($fileGroup.Name)'" $totalCommands = 0 $analyzedCommands = 0 :commandLoop foreach ($command in Get-CommandsInFile -Path $fileGroup.Name) { $totalCommands++ foreach ($coverageInfoObject in $fileGroup.Group) { if (Test-CoverageOverlapsCommand -CoverageInfo $coverageInfoObject -Command $command) { $analyzedCommands++ New-CoverageBreakpoint -Command $command continue commandLoop } } } & $SafeCommands['Write-Verbose'] "Analyzing $analyzedCommands of $totalCommands commands in file '$($fileGroup.Name)' for code coverage" } } function Get-CommandsInFile { param ([string] $Path) $errors = $null $tokens = $null $ast = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref] $tokens, [ref] $errors) if ($PSVersionTable.PSVersion.Major -ge 5) { # In PowerShell 5.0, dynamic keywords for DSC configurations are represented by the DynamicKeywordStatementAst # class. They still trigger breakpoints, but are not a child class of CommandBaseAst anymore. $predicate = { $args[0] -is [System.Management.Automation.Language.DynamicKeywordStatementAst] -or $args[0] -is [System.Management.Automation.Language.CommandBaseAst] } } else { $predicate = { $args[0] -is [System.Management.Automation.Language.CommandBaseAst] } } $searchNestedScriptBlocks = $true $ast.FindAll($predicate, $searchNestedScriptBlocks) } function Test-CoverageOverlapsCommand { param ([object] $CoverageInfo, [System.Management.Automation.Language.Ast] $Command) if ($CoverageInfo.Class -or $CoverageInfo.Function) { Test-CommandInScope -Command $Command -Class $CoverageInfo.Class -Function $CoverageInfo.Function } else { Test-CoverageOverlapsCommandByLineNumber @PSBoundParameters } } function Test-CommandInScope { param ([System.Management.Automation.Language.Ast] $Command, [string] $Class, [string] $Function) $classResult = !$Class $functionResult = !$Function for ($ast = $Command; $null -ne $ast; $ast = $ast.Parent) { if (!$classResult -and $PSVersionTable.PSVersion.Major -ge 5) { # Classes have been introduced in PowerShell 5.0 $classAst = $ast -as [System.Management.Automation.Language.TypeDefinitionAst] if ($null -ne $classAst -and $classAst.Name -like $Class) { $classResult = $true } } if (!$functionResult) { $functionAst = $ast -as [System.Management.Automation.Language.FunctionDefinitionAst] if ($null -ne $functionAst -and $functionAst.Name -like $Function) { $functionResult = $true } } if ($classResult -and $functionResult) { return $true } } return $false } function Test-CoverageOverlapsCommandByLineNumber { param ([object] $CoverageInfo, [System.Management.Automation.Language.Ast] $Command) $commandStart = $Command.Extent.StartLineNumber $commandEnd = $Command.Extent.EndLineNumber $coverStart = $CoverageInfo.StartLine $coverEnd = $CoverageInfo.EndLine # An EndLine value of 0 means to cover the entire rest of the file from StartLine # (which may also be 0) if ($coverEnd -le 0) { $coverEnd = [int]::MaxValue } return (Test-RangeContainsValue -Value $commandStart -Min $coverStart -Max $coverEnd) -or (Test-RangeContainsValue -Value $commandEnd -Min $coverStart -Max $coverEnd) } function Test-RangeContainsValue { param ([int] $Value, [int] $Min, [int] $Max) return $Value -ge $Min -and $Value -le $Max } function New-CoverageBreakpoint { param ([System.Management.Automation.Language.Ast] $Command) if (IsIgnoredCommand -Command $Command) { return } $params = @{ Script = $Command.Extent.File Line = $Command.Extent.StartLineNumber Column = $Command.Extent.StartColumnNumber Action = { } } $breakpoint = & $SafeCommands['Set-PSBreakpoint'] @params [pscustomobject] @{ File = $Command.Extent.File Class = Get-ParentClassName -Ast $Command Function = Get-ParentFunctionName -Ast $Command StartLine = $Command.Extent.StartLineNumber EndLine = $Command.Extent.EndLineNumber StartColumn = $Command.Extent.StartColumnNumber EndColumn = $Command.Extent.EndColumnNumber Command = Get-CoverageCommandText -Ast $Command Breakpoint = $breakpoint } } Function Get-AstTopParent { param( [System.Management.Automation.Language.Ast] $Ast, [int] $MaxDepth = 30 ) if ([string]::IsNullOrEmpty($Ast.Parent)) { return $Ast } elseif ($MaxDepth -le 0) { & $SafeCommands['Write-Verbose'] "Max depth reached, moving on" return $null } else { $MaxDepth-- Get-AstTopParent -Ast $Ast.Parent -MaxDepth $MaxDepth } } function IsIgnoredCommand { param ([System.Management.Automation.Language.Ast] $Command) if (-not $Command.Extent.File) { # This can happen if the script contains "configuration" or any similarly implemented # dynamic keyword. PowerShell modifies the script code and reparses it in memory, leading # to AST elements with no File in their Extent. return $true } if ($PSVersionTable.PSVersion.Major -ge 4) { if ($Command.Extent.Text -eq 'Configuration') { # More DSC voodoo. Calls to "configuration" generate breakpoints, but their HitCount # stays zero (even though they are executed.) For now, ignore them, unless we can come # up with a better solution. return $true } if (IsChildOfHashtableDynamicKeyword -Command $Command) { # The lines inside DSC resource declarations don't trigger their breakpoints when executed, # just like the "configuration" keyword itself. I don't know why, at this point, but just like # configuration, we'll ignore it so it doesn't clutter up the coverage analysis with useless junk. return $true } } if ($Command.Extent.Text -match '^{?& \$wrappedCmd @PSBoundParameters ?}?$' -and (Get-AstTopParent -Ast $Command) -like '*$steppablePipeline.Begin($PSCmdlet)*$steppablePipeline.Process($_)*$steppablePipeline.End()*' ) { # Fix for proxy function wrapped pipeline command. PowerShell does not increment the hit count when # these functions are executed using the steppable pipeline; further, these checks are redundant, as # all steppable pipeline constituents already get breakpoints set. This checks to ensure the top parent # node of the command contains all three constituents of the steppable pipeline before ignoring it. return $true } if (IsClosingLoopCondition -Command $Command) { # For some reason, the closing expressions of do/while and do/until loops don't trigger their breakpoints. # To avoid useless clutter, we'll ignore those lines as well. return $true } return $false } function IsChildOfHashtableDynamicKeyword { param ([System.Management.Automation.Language.Ast] $Command) for ($ast = $Command.Parent; $null -ne $ast; $ast = $ast.Parent) { if ($PSVersionTable.PSVersion.Major -ge 5) { # The ast behaves differently for DSC resources with version 5+. There's a new DynamicKeywordStatementAst class, # and they no longer are represented by CommandAst objects. if ($ast -is [System.Management.Automation.Language.DynamicKeywordStatementAst] -and $ast.CommandElements[-1] -is [System.Management.Automation.Language.HashtableAst]) { return $true } } else { if ($ast -is [System.Management.Automation.Language.CommandAst] -and $null -ne $ast.DefiningKeyword -and $ast.DefiningKeyword.BodyMode -eq [System.Management.Automation.Language.DynamicKeywordBodyMode]::Hashtable) { return $true } } } return $false } function IsClosingLoopCondition { param ([System.Management.Automation.Language.Ast] $Command) $ast = $Command while ($null -ne $ast.Parent) { if (($ast.Parent -is [System.Management.Automation.Language.DoWhileStatementAst] -or $ast.Parent -is [System.Management.Automation.Language.DoUntilStatementAst]) -and $ast.Parent.Condition -eq $ast) { return $true } $ast = $ast.Parent } return $false } function Get-ParentClassName { param ([System.Management.Automation.Language.Ast] $Ast) if ($PSVersionTable.PSVersion.Major -ge 5) { # Classes have been introduced in PowerShell 5.0 $parent = $Ast.Parent while ($null -ne $parent -and $parent -isnot [System.Management.Automation.Language.TypeDefinitionAst]) { $parent = $parent.Parent } } if ($null -eq $parent) { return '' } else { return $parent.Name } } function Get-ParentFunctionName { param ([System.Management.Automation.Language.Ast] $Ast) $parent = $Ast.Parent while ($null -ne $parent -and $parent -isnot [System.Management.Automation.Language.FunctionDefinitionAst]) { $parent = $parent.Parent } if ($null -eq $parent) { return '' } else { return $parent.Name } } function Get-CoverageCommandText { param ([System.Management.Automation.Language.Ast] $Ast) $reportParentExtentTypes = @( [System.Management.Automation.Language.ReturnStatementAst] [System.Management.Automation.Language.ThrowStatementAst] [System.Management.Automation.Language.AssignmentStatementAst] [System.Management.Automation.Language.IfStatementAst] ) $parent = Get-ParentNonPipelineAst -Ast $Ast if ($null -ne $parent) { if ($parent -is [System.Management.Automation.Language.HashtableAst]) { return Get-KeyValuePairText -HashtableAst $parent -ChildAst $Ast } elseif ($reportParentExtentTypes -contains $parent.GetType()) { return $parent.Extent.Text } } return $Ast.Extent.Text } function Get-ParentNonPipelineAst { param ([System.Management.Automation.Language.Ast] $Ast) $parent = $null if ($null -ne $Ast) { $parent = $Ast.Parent } while ($parent -is [System.Management.Automation.Language.PipelineAst]) { $parent = $parent.Parent } return $parent } function Get-KeyValuePairText { param ( [System.Management.Automation.Language.HashtableAst] $HashtableAst, [System.Management.Automation.Language.Ast] $ChildAst ) & $SafeCommands['Set-StrictMode'] -Off foreach ($keyValuePair in $HashtableAst.KeyValuePairs) { if ($keyValuePair.Item2.PipelineElements -contains $ChildAst) { return '{0} = {1}' -f $keyValuePair.Item1.Extent.Text, $keyValuePair.Item2.Extent.Text } } # This shouldn't happen, but just in case, default to the old output of just the expression. return $ChildAst.Extent.Text } function Get-CoverageMissedCommands { param ([object[]] $CommandCoverage) $CommandCoverage | & $SafeCommands['Where-Object'] { $_.Breakpoint.HitCount -eq 0 } } function Get-CoverageHitCommands { param ([object[]] $CommandCoverage) $CommandCoverage | & $SafeCommands['Where-Object'] { $_.Breakpoint.HitCount -gt 0 } } function Get-CoverageReport { param ([object] $PesterState) $properties = @( 'File' @{ Name = 'Line'; Expression = { $_.StartLine } } 'StartLine' 'EndLine' 'StartColumn' 'EndColumn' 'Class' 'Function' 'Command' @{ Name = 'HitCount'; Expression = { $_.Breakpoint.HitCount } } ) $missedCommands = @(Get-CoverageMissedCommands -CommandCoverage $PesterState.CommandCoverage | & $SafeCommands['Select-Object'] $properties) $hitCommands = @(Get-CoverageHitCommands -CommandCoverage $PesterState.CommandCoverage | & $SafeCommands['Select-Object'] $properties) $analyzedFiles = @($PesterState.CommandCoverage | & $SafeCommands['Select-Object'] -ExpandProperty File -Unique) [pscustomobject] @{ NumberOfCommandsAnalyzed = $PesterState.CommandCoverage.Count NumberOfFilesAnalyzed = $analyzedFiles.Count NumberOfCommandsExecuted = $hitCommands.Count NumberOfCommandsMissed = $missedCommands.Count MissedCommands = $missedCommands HitCommands = $hitCommands AnalyzedFiles = $analyzedFiles } } function Get-CommonParentPath { param ([string[]] $Path) $pathsToTest = @( $Path | Normalize-Path | & $SafeCommands['Select-Object'] -Unique ) if ($pathsToTest.Count -gt 0) { $parentPath = & $SafeCommands['Split-Path'] -Path $pathsToTest[0] -Parent while ($parentPath.Length -gt 0) { $nonMatches = $pathsToTest -notmatch "^$([regex]::Escape($parentPath))" if ($nonMatches.Count -eq 0) { return $parentPath } else { $parentPath = & $SafeCommands['Split-Path'] -Path $parentPath -Parent } } } return [string]::Empty } function Get-RelativePath { param ( [string] $Path, [string] $RelativeTo ) return $Path -replace "^$([regex]::Escape("$RelativeTo$([System.IO.Path]::DirectorySeparatorChar)"))?" } function Normalize-Path { [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('PSPath', 'FullName')] [string[]] $Path ) # Split-Path and Join-Path will replace any AltDirectorySeparatorChar instances with the DirectorySeparatorChar # (Even if it's not the one that the split / join happens on.) So splitting / rejoining a path will give us # consistent separators for later string comparison. process { if ($null -ne $Path) { foreach ($p in $Path) { $normalizedPath = & $SafeCommands['Split-Path'] $p -Leaf if ($normalizedPath -ne $p) { $parent = & $SafeCommands['Split-Path'] $p -Parent $normalizedPath = & $SafeCommands['Join-Path'] $parent $normalizedPath } $normalizedPath } } } } function Get-JaCoCoReportXml { param ( [parameter(Mandatory = $true)] $PesterState, [parameter(Mandatory = $true)] [object] $CoverageReport ) if ($null -eq $CoverageReport -or ($pester.Show -eq [Pester.OutputTypes]::None) -or $CoverageReport.NumberOfCommandsAnalyzed -eq 0) { return } $now = & $SafeCommands['Get-Date'] $nineteenSeventy = & $SafeCommands['Get-Date'] -Date "01/01/1970" [long] $endTime = [math]::Floor((New-TimeSpan -start $nineteenSeventy -end $now).TotalMilliseconds) [long] $startTime = [math]::Floor($endTime - $PesterState.Time.TotalMilliseconds) $folderGroups = $PesterState.CommandCoverage | & $SafeCommands["Group-Object"] -Property { & $SafeCommands["Split-Path"] $_.File -Parent } $packageList = & $SafeCommands['New-Object'] System.Collections.Generic.List[psobject] $report = @{ Instruction = @{ Missed = 0; Covered = 0 } Line = @{ Missed = 0; Covered = 0 } Method = @{ Missed = 0; Covered = 0 } Class = @{ Missed = 0; Covered = 0 } } foreach ($folderGroup in $folderGroups) { $package = @{ Name = $folderGroup.Name Classes = [ordered] @{ } Instruction = @{ Missed = 0; Covered = 0 } Line = @{ Missed = 0; Covered = 0 } Method = @{ Missed = 0; Covered = 0 } Class = @{ Missed = 0; Covered = 0 } } foreach ($command in $folderGroup.Group) { $file = $command.File $function = $command.Function if (!$function) { $function = '<script>' } $line = $command.StartLine.ToString() $missed = if ($command.Breakpoint.HitCount) { 0 } else { 1 } $covered = if ($command.Breakpoint.HitCount) { 1 } else { 0 } if (!$package.Classes.Contains($file)) { $package.Class.Missed += $missed $package.Class.Covered += $covered $package.Classes.$file = @{ Methods = [ordered] @{ } Lines = [ordered] @{ } Instruction = @{ Missed = 0; Covered = 0 } Line = @{ Missed = 0; Covered = 0 } Method = @{ Missed = 0; Covered = 0 } Class = @{ Missed = $missed; Covered = $covered } } } if (!$package.Classes.$file.Methods.Contains($function)) { $package.Method.Missed += $missed $package.Method.Covered += $covered $package.Classes.$file.Method.Missed += $missed $package.Classes.$file.Method.Covered += $covered $package.Classes.$file.Methods.$function = @{ FirstLine = $line Instruction = @{ Missed = 0; Covered = 0 } Line = @{ Missed = 0; Covered = 0 } Method = @{ Missed = $missed; Covered = $covered } } } if (!$package.Classes.$file.Lines.Contains($line)) { $package.Line.Missed += $missed $package.Line.Covered += $covered $package.Classes.$file.Line.Missed += $missed $package.Classes.$file.Line.Covered += $covered $package.Classes.$file.Methods.$function.Line.Missed += $missed $package.Classes.$file.Methods.$function.Line.Covered += $covered $package.Classes.$file.Lines.$line = @{ Instruction = @{ Missed = 0; Covered = 0 } } } $package.Instruction.Missed += $missed $package.Instruction.Covered += $covered $package.Classes.$file.Instruction.Missed += $missed $package.Classes.$file.Instruction.Covered += $covered $package.Classes.$file.Methods.$function.Instruction.Missed += $missed $package.Classes.$file.Methods.$function.Instruction.Covered += $covered $package.Classes.$file.Lines.$line.Instruction.Missed += $missed $package.Classes.$file.Lines.$line.Instruction.Covered += $covered } $report.Class.Missed += $package.Class.Missed $report.Class.Covered += $package.Class.Covered $report.Method.Missed += $package.Method.Missed $report.Method.Covered += $package.Method.Covered $report.Line.Missed += $package.Line.Missed $report.Line.Covered += $package.Line.Covered $report.Instruction.Missed += $package.Instruction.Missed $report.Instruction.Covered += $package.Instruction.Covered $packageList.Add($package) } $commonParent = Get-CommonParentPath -Path $CoverageReport.AnalyzedFiles $commonParentLeaf = & $SafeCommands["Split-Path"] $commonParent -Leaf # the JaCoCo xml format without the doctype, as the XML stuff does not like DTD's. $jaCoCoReport = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>' $jaCoCoReport += '<report name="">' $jaCoCoReport += '<sessioninfo id="this" start="" dump="" />' $jaCoCoReport += '</report>' [xml] $jaCoCoReportXml = $jaCoCoReport $reportElement = $jaCoCoReportXml.report $reportElement.name = "Pester ($now)" $reportElement.sessioninfo.start = $startTime.ToString() $reportElement.sessioninfo.dump = $endTime.ToString() foreach ($package in $packageList) { $packageRelativePath = Get-RelativePath -Path $package.Name -RelativeTo $commonParent if ($null -eq $packageRelativePath) { $packageName = $commonParentLeaf } else { $packageName = "{0}/{1}" -f $commonParentLeaf, $($packageRelativePath.Replace("\", "/")) } $packageElement = Add-XmlElement $reportElement "package" @{ name = ($packageName -replace "/$", "") } foreach ($file in $package.Classes.Keys) { $class = $package.Classes.$file $classElementRelativePath = (Get-RelativePath -Path $file -RelativeTo $commonParent).Replace("\", "/") $classElementName = "{0}/{1}" -f $commonParentLeaf, $classElementRelativePath $classElementName = $classElementName.Substring(0, $($classElementName.LastIndexOf("."))) $classElement = Add-XmlElement $packageElement 'class' -Attributes ([ordered] @{ name = $classElementName sourcefilename = (& $SafeCommands["Split-Path"] -Path $classElementRelativePath -Leaf) }) foreach ($function in $class.Methods.Keys) { $method = $class.Methods.$function $methodElement = Add-XmlElement $classElement 'method' -Attributes ([ordered] @{ name = $function desc = '()' line = $method.FirstLine }) Add-JaCoCoCounter Instruction $method $methodElement Add-JaCoCoCounter Line $method $methodElement Add-JaCoCoCounter Method $method $methodElement } Add-JaCoCoCounter Instruction $class $classElement Add-JaCoCoCounter Line $class $classElement Add-JaCoCoCounter Method $class $classElement Add-JaCoCoCounter Class $class $classElement } foreach ($file in $package.Classes.Keys) { $class = $package.Classes.$file $sourceFileElement = Add-XmlElement $packageElement 'sourcefile' -Attributes ([ordered] @{ name = (& $SafeCommands["Split-Path"] -Path $file -Leaf) }) foreach ($line in $class.Lines.Keys) { $null = Add-XmlElement $sourceFileElement 'line' -Attributes ([ordered] @{ nr = $line mi = $class.Lines.$line.Instruction.Missed ci = $class.Lines.$line.Instruction.Covered mb = 0 cb = 0 }) } Add-JaCoCoCounter Instruction $class $sourceFileElement Add-JaCoCoCounter Line $class $sourceFileElement Add-JaCoCoCounter Method $class $sourceFileElement Add-JaCoCoCounter Class $class $sourceFileElement } Add-JaCoCoCounter Instruction $package $packageElement Add-JaCoCoCounter Line $package $packageElement Add-JaCoCoCounter Method $package $packageElement Add-JaCoCoCounter Class $package $packageElement } Add-JaCoCoCounter Instruction $report $reportElement Add-JaCoCoCounter Line $report $reportElement Add-JaCoCoCounter Method $report $reportElement Add-JaCoCoCounter Class $report $reportElement # There is no pretty way to insert the Doctype, as microsoft has deprecated the DTD stuff. $jaCoCoReportDocType = '<!DOCTYPE report PUBLIC "-//JACOCO//DTD Report 1.1//EN" "report.dtd">' return $jaCocoReportXml.OuterXml.Insert(54, $jaCoCoReportDocType) } function Add-XmlElement { param ( [parameter(Mandatory = $true)] [System.Xml.XmlNode] $Parent, [parameter(Mandatory = $true)] [string] $Name, [System.Collections.IDictionary] $Attributes ) $element = $Parent.AppendChild($Parent.OwnerDocument.CreateElement($Name)) if ($Attributes) { foreach ($key in $Attributes.Keys) { $attribute = $element.Attributes.Append($Parent.OwnerDocument.CreateAttribute($key)) $attribute.Value = $Attributes.$key } } return $element } function Add-JaCoCoCounter { param ( [parameter(Mandatory = $true)] [ValidateSet('Instruction', 'Line', 'Method', 'Class')] [string] $Type, [parameter(Mandatory = $true)] [System.Collections.IDictionary] $Data, [parameter(Mandatory = $true)] [System.Xml.XmlNode] $Parent ) if ($Data.$Type.Missed -isnot [int] -or $Data.$Type.Covered -isnot [int]) { throw 'Counter data expected' } $null = Add-XmlElement $Parent 'counter' -Attributes ([ordered] @{ type = $Type.ToUpperInvariant() missed = $Data.$Type.Missed covered = $Data.$Type.Covered }) } # SIG # Begin signature block # MIIZbgYJKoZIhvcNAQcCoIIZXzCCGVsCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB # gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR # AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUx9mSRhnMwRI8yNmGBpMN4np3 # HIigghR8MIIE/jCCA+agAwIBAgIQDUJK4L46iP9gQCHOFADw3TANBgkqhkiG9w0B # AQsFADByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYD # VQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEyIEFz # c3VyZWQgSUQgVGltZXN0YW1waW5nIENBMB4XDTIxMDEwMTAwMDAwMFoXDTMxMDEw # NjAwMDAwMFowSDELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMu # MSAwHgYDVQQDExdEaWdpQ2VydCBUaW1lc3RhbXAgMjAyMTCCASIwDQYJKoZIhvcN # AQEBBQADggEPADCCAQoCggEBAMLmYYRnxYr1DQikRcpja1HXOhFCvQp1dU2UtAxQ # tSYQ/h3Ib5FrDJbnGlxI70Tlv5thzRWRYlq4/2cLnGP9NmqB+in43Stwhd4CGPN4 # bbx9+cdtCT2+anaH6Yq9+IRdHnbJ5MZ2djpT0dHTWjaPxqPhLxs6t2HWc+xObTOK # fF1FLUuxUOZBOjdWhtyTI433UCXoZObd048vV7WHIOsOjizVI9r0TXhG4wODMSlK # XAwxikqMiMX3MFr5FK8VX2xDSQn9JiNT9o1j6BqrW7EdMMKbaYK02/xWVLwfoYer # vnpbCiAvSwnJlaeNsvrWY4tOpXIc7p96AXP4Gdb+DUmEvQECAwEAAaOCAbgwggG0 # MA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB/wQMMAoGCCsG # AQUFBwMIMEEGA1UdIAQ6MDgwNgYJYIZIAYb9bAcBMCkwJwYIKwYBBQUHAgEWG2h0 # dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAfBgNVHSMEGDAWgBT0tuEgHf4prtLk # YaWyoiWyyBc1bjAdBgNVHQ4EFgQUNkSGjqS6sGa+vCgtHUQ23eNqerwwcQYDVR0f # BGowaDAyoDCgLoYsaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL3NoYTItYXNzdXJl # ZC10cy5jcmwwMqAwoC6GLGh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9zaGEyLWFz # c3VyZWQtdHMuY3JsMIGFBggrBgEFBQcBAQR5MHcwJAYIKwYBBQUHMAGGGGh0dHA6 # Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBPBggrBgEFBQcwAoZDaHR0cDovL2NhY2VydHMu # ZGlnaWNlcnQuY29tL0RpZ2lDZXJ0U0hBMkFzc3VyZWRJRFRpbWVzdGFtcGluZ0NB # LmNydDANBgkqhkiG9w0BAQsFAAOCAQEASBzctemaI7znGucgDo5nRv1CclF0CiNH # o6uS0iXEcFm+FKDlJ4GlTRQVGQd58NEEw4bZO73+RAJmTe1ppA/2uHDPYuj1UUp4 # eTZ6J7fz51Kfk6ftQ55757TdQSKJ+4eiRgNO/PT+t2R3Y18jUmmDgvoaU+2QzI2h # F3MN9PNlOXBL85zWenvaDLw9MtAby/Vh/HUIAHa8gQ74wOFcz8QRcucbZEnYIpp1 # FUL1LTI4gdr0YKK6tFL7XOBhJCVPst/JKahzQ1HavWPWH1ub9y4bTxMd90oNcX6X # t/Q/hOvB46NJofrOp79Wz7pZdmGJX36ntI5nePk2mOHLKNpbh6aKLzCCBQ0wggP1 # oAMCAQICEAPBBFtdmcD9x4iXgGyKU+cwDQYJKoZIhvcNAQELBQAwcjELMAkGA1UE # BhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2lj # ZXJ0LmNvbTExMC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVkIElEIENvZGUg # U2lnbmluZyBDQTAeFw0yMTAxMjgwMDAwMDBaFw0yNDAxMzEyMzU5NTlaMEsxCzAJ # BgNVBAYTAkNaMQ4wDAYDVQQHEwVQcmFoYTEVMBMGA1UECgwMSmFrdWIgSmFyZcWh # MRUwEwYDVQQDDAxKYWt1YiBKYXJlxaEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw # ggEKAoIBAQC154nkzSQ95K1yCflVL3iFQhzw/25ht4o506hK7ATFgvP71ZI+YHce # gvVoMtaMhJp2/EzXsgxDF7EZBR4Hl9vNbIpLAFSVCK4GBD0DxNCDFrJTPtNohgsA # STNMcK6t0iunh7MEkaYt1yPgiISA1vcQUMKi51WSUxeWnsUNTkJDZkyM61fETbhy # CI66xLItaf3OWdyjiOFPq2n8yx+eg1w7GCC/eNYVAjzqtSmiE/xv6Qoj7z9qFyS1 # pAO4cxDRLAD9IcCiYmHOJVgsho3/u4QNNm72ghz7iiRAO5lDoBcZIiLS5RKxJwMG # nnYbIiAuISZmv4PtrkcSu81Lzmtu81idAgMBAAGjggHEMIIBwDAfBgNVHSMEGDAW # gBRaxLl7KgqjpepxA8Bg+S32ZXUOWDAdBgNVHQ4EFgQUF2nEZEX1uTrPSD3h5VSJ # 8g9ef20wDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMHcGA1Ud # HwRwMG4wNaAzoDGGL2h0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9zaGEyLWFzc3Vy # ZWQtY3MtZzEuY3JsMDWgM6Axhi9odHRwOi8vY3JsNC5kaWdpY2VydC5jb20vc2hh # Mi1hc3N1cmVkLWNzLWcxLmNybDBLBgNVHSAERDBCMDYGCWCGSAGG/WwDATApMCcG # CCsGAQUFBwIBFhtodHRwOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwCAYGZ4EMAQQB # MIGEBggrBgEFBQcBAQR4MHYwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2lj # ZXJ0LmNvbTBOBggrBgEFBQcwAoZCaHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29t # L0RpZ2lDZXJ0U0hBMkFzc3VyZWRJRENvZGVTaWduaW5nQ0EuY3J0MAwGA1UdEwEB # /wQCMAAwDQYJKoZIhvcNAQELBQADggEBANuuPZ7LhU/v1GDprUhYch3/fov3sIxp # wshFvufWbGxUYzxEVQSsZLdFVUzAdsCz3fKInY2ihCwqIoU5vbwKFvV1zvizat/r # aNn5aa8H34NjEXPHQiyNykOk9CdFgk+zZn+YpeyzBMAEvQR4uB4eDv1USWkwdXPB # VVZcjM0xEsx9H/ZZRSEGS0x3ue+shvZdPRzoWcuiK8hNcbFZr15hMGi4s0F9IxTZ # QzoSpNJsBA/vMmkbp2SWeENn49BNx8q760e+ELMfuSBltKs8S2hB9TLrpko3nIvp # l1323zyR6ZpWK1/FHbGkHRsSJKvOOBdlSL08+KM2kNzXez88eUae+1YwggUwMIIE # GKADAgECAhAECRgbX9W7ZnVTQ7VvlVAIMA0GCSqGSIb3DQEBCwUAMGUxCzAJBgNV # BAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdp # Y2VydC5jb20xJDAiBgNVBAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAe # Fw0xMzEwMjIxMjAwMDBaFw0yODEwMjIxMjAwMDBaMHIxCzAJBgNVBAYTAlVTMRUw # EwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x # MTAvBgNVBAMTKERpZ2lDZXJ0IFNIQTIgQXNzdXJlZCBJRCBDb2RlIFNpZ25pbmcg # Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQD407Mcfw4Rr2d3B9ML # MUkZz9D7RZmxOttE9X/lqJ3bMtdx6nadBS63j/qSQ8Cl+YnUNxnXtqrwnIal2CWs # DnkoOn7p0WfTxvspJ8fTeyOU5JEjlpB3gvmhhCNmElQzUHSxKCa7JGnCwlLyFGeK # iUXULaGj6YgsIJWuHEqHCN8M9eJNYBi+qsSyrnAxZjNxPqxwoqvOf+l8y5Kh5Tsx # HM/q8grkV7tKtel05iv+bMt+dDk2DZDv5LVOpKnqagqrhPOsZ061xPeM0SAlI+sI # ZD5SlsHyDxL0xY4PwaLoLFH3c7y9hbFig3NBggfkOItqcyDQD2RzPJ6fpjOp/Rnf # JZPRAgMBAAGjggHNMIIByTASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQE # AwIBhjATBgNVHSUEDDAKBggrBgEFBQcDAzB5BggrBgEFBQcBAQRtMGswJAYIKwYB # BQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBDBggrBgEFBQcwAoY3aHR0 # cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENB # LmNydDCBgQYDVR0fBHoweDA6oDigNoY0aHR0cDovL2NybDQuZGlnaWNlcnQuY29t # L0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNybDA6oDigNoY0aHR0cDovL2NybDMu # ZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNybDBPBgNVHSAE # SDBGMDgGCmCGSAGG/WwAAgQwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGln # aWNlcnQuY29tL0NQUzAKBghghkgBhv1sAzAdBgNVHQ4EFgQUWsS5eyoKo6XqcQPA # YPkt9mV1DlgwHwYDVR0jBBgwFoAUReuir/SSy4IxLVGLp6chnfNtyA8wDQYJKoZI # hvcNAQELBQADggEBAD7sDVoks/Mi0RXILHwlKXaoHV0cLToaxO8wYdd+C2D9wz0P # xK+L/e8q3yBVN7Dh9tGSdQ9RtG6ljlriXiSBThCk7j9xjmMOE0ut119EefM2FAaK # 95xGTlz/kLEbBw6RFfu6r7VRwo0kriTGxycqoSkoGjpxKAI8LpGjwCUR4pwUR6F6 # aGivm6dcIFzZcbEMj7uo+MUSaJ/PQMtARKUT8OZkDCUIQjKyNookAv4vcn4c10lF # luhZHen6dGRrsutmQ9qzsIzV6Q3d9gEgzpkxYz0IGhizgZtPxpMQBvwHgfqL2vmC # SfdibqFT+hKUGIUukpHqaGxEMrJmoecYpJpkUe8wggUxMIIEGaADAgECAhAKoSXW # 1jIbfkHkBdo2l8IVMA0GCSqGSIb3DQEBCwUAMGUxCzAJBgNVBAYTAlVTMRUwEwYD # VQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAi # BgNVBAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0xNjAxMDcxMjAw # MDBaFw0zMTAxMDcxMjAwMDBaMHIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdp # Q2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAvBgNVBAMTKERp # Z2lDZXJ0IFNIQTIgQXNzdXJlZCBJRCBUaW1lc3RhbXBpbmcgQ0EwggEiMA0GCSqG # SIb3DQEBAQUAA4IBDwAwggEKAoIBAQC90DLuS82Pf92puoKZxTlUKFe2I0rEDgdF # M1EQfdD5fU1ofue2oPSNs4jkl79jIZCYvxO8V9PD4X4I1moUADj3Lh477sym9jJZ # /l9lP+Cb6+NGRwYaVX4LJ37AovWg4N4iPw7/fpX786O6Ij4YrBHk8JkDbTuFfAnT # 7l3ImgtU46gJcWvgzyIQD3XPcXJOCq3fQDpct1HhoXkUxk0kIzBdvOw8YGqsLwfM # /fDqR9mIUF79Zm5WYScpiYRR5oLnRlD9lCosp+R1PrqYD4R/nzEU1q3V8mTLex4F # 0IQZchfxFwbvPc3WTe8GQv2iUypPhR3EHTyvz9qsEPXdrKzpVv+TAgMBAAGjggHO # MIIByjAdBgNVHQ4EFgQU9LbhIB3+Ka7S5GGlsqIlssgXNW4wHwYDVR0jBBgwFoAU # Reuir/SSy4IxLVGLp6chnfNtyA8wEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8B # Af8EBAMCAYYwEwYDVR0lBAwwCgYIKwYBBQUHAwgweQYIKwYBBQUHAQEEbTBrMCQG # CCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQwYIKwYBBQUHMAKG # N2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJv # b3RDQS5jcnQwgYEGA1UdHwR6MHgwOqA4oDaGNGh0dHA6Ly9jcmw0LmRpZ2ljZXJ0 # LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwOqA4oDaGNGh0dHA6Ly9j # cmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwUAYD # VR0gBEkwRzA4BgpghkgBhv1sAAIEMCowKAYIKwYBBQUHAgEWHGh0dHBzOi8vd3d3 # LmRpZ2ljZXJ0LmNvbS9DUFMwCwYJYIZIAYb9bAcBMA0GCSqGSIb3DQEBCwUAA4IB # AQBxlRLpUYdWac3v3dp8qmN6s3jPBjdAhO9LhL/KzwMC/cWnww4gQiyvd/MrHwwh # Wiq3BTQdaq6Z+CeiZr8JqmDfdqQ6kw/4stHYfBli6F6CJR7Euhx7LCHi1lssFDVD # BGiy23UC4HLHmNY8ZOUfSBAYX4k4YU1iRiSHY4yRUiyvKYnleB/WCxSlgNcSR3Cz # ddWThZN+tpJn+1Nhiaj1a5bA9FhpDXzIAbG5KHW3mWOFIoxhynmUfln8jA/jb7UB # JrZspe6HUSHkWGCbugwtK22ixH67xCUrRwIIfEmuE7bhfEJCKMYYVs9BNLZmXbZ0 # e/VWMyIvIjayS6JKldj1po5SMYIEXDCCBFgCAQEwgYYwcjELMAkGA1UEBhMCVVMx # FTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNv # bTExMC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVkIElEIENvZGUgU2lnbmlu # ZyBDQQIQA8EEW12ZwP3HiJeAbIpT5zAJBgUrDgMCGgUAoHgwGAYKKwYBBAGCNwIB # DDEKMAigAoAAoQKAADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgorBgEE # AYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAjBgkqhkiG9w0BCQQxFgQULf7LJjInWgnY # MEqsQyAgDTtM8oswDQYJKoZIhvcNAQEBBQAEggEATXdhjND3VN7QmZASuEAmZEcr # JAfR1motpB7eTWrTlp2M54fPmNG8n7UAcFp6oDaGyFOio/6tVX+hvKLwx6rn7MAU # AfI19Fa4+50z20KXyJ1KAbjF291CAVLTn3qgzuXALtN7Z3BxIlF71qvZ1uSUUIE1 # HUv4xE2UAqvoy4PNtSeFQtPGkdKPSPFOkhpo1kvkLm04B5IVCiw906wyWdbDoWUa # pk3RadEyLBJwrHHWBFJr3IUZjACE3ZcK47gvVbyevYfd43sVT6u2dvDV1DGWYynv # ttl+A8fEijL5fHM3oI9Bz1JOyohieO4aBggGb9kyIFAQXgjCzuandiknKM0nCqGC # AjAwggIsBgkqhkiG9w0BCQYxggIdMIICGQIBATCBhjByMQswCQYDVQQGEwJVUzEV # MBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29t # MTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEyIEFzc3VyZWQgSUQgVGltZXN0YW1waW5n # IENBAhANQkrgvjqI/2BAIc4UAPDdMA0GCWCGSAFlAwQCAQUAoGkwGAYJKoZIhvcN # AQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjEwNTI5MTIyODAyWjAv # BgkqhkiG9w0BCQQxIgQg610+9WOHn6W5n/he5u6Ju0Jbql9g9hZ6ptk2YdGaKMUw # DQYJKoZIhvcNAQEBBQAEggEAJDF6g0q9tvv6YbFzlRD+H9lkrTW2lqNoXrnvWU5v # kEsCpL+oXTwVhQOsJRz7LuvMFNJYMwtutbFpUcGXVFx7dqUE7CsEbZYIFIttmPLs # CppLzfIvpScEm41EwacJNXedmClsY9AKCsy3ntJmKvtTst0GRI/JNNt/jIbEMnV3 # 6LHKJiLZBm50pqElW35zyhef0WcfQQTk7sHRqEkBK4YsBijTrREylZqtmkEOySfx # 26F5KIRLj0MUoR20bK0Wdx82oF5fAOrfwOPEXd6SNtgverUgcML8AgiM8o6R8Erh # 0SAwqK7O2r7g3553CE6KYgaAB6SITs4kCK35PUWL70UIiA== # SIG # End signature block |