Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[rel/5.x.x] Implement Cobertura coverage format #2572

Merged
merged 10 commits into from
Nov 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ if ($Clean) {
, ("$PSScriptRoot/src/schemas/JUnit4/*.xsd", "$PSScriptRoot/bin/schemas/JUnit4/")
, ("$PSScriptRoot/src/schemas/NUnit25/*.xsd", "$PSScriptRoot/bin/schemas/NUnit25/")
, ("$PSScriptRoot/src/schemas/NUnit3/*.xsd", "$PSScriptRoot/bin/schemas/NUnit3/")
, ("$PSScriptRoot/src/schemas/Cobertura/*.dtd", "$PSScriptRoot/bin/schemas/Cobertura/")
, ("$PSScriptRoot/src/schemas/JaCoCo/*.dtd", "$PSScriptRoot/bin/schemas/JaCoCo/")
, ("$PSScriptRoot/src/csharp/Pester/bin/$Configuration/net452/Pester.dll", "$PSScriptRoot/bin/bin/net452/")
, ("$PSScriptRoot/src/csharp/Pester/bin/$Configuration/net452/Pester.pdb", "$PSScriptRoot/bin/bin/net452/")
Expand Down
1 change: 1 addition & 0 deletions publish/filesToPublish.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
'bin/netstandard2.0/Pester.pdb'
'en-US/about_Pester.help.txt'
'en-US/about_PesterConfiguration.help.txt'
'schemas/Cobertura/coverage-loose.dtd'
'schemas/JaCoCo/report.dtd'
'schemas/JUnit4/junit_schema_4.xsd'
'schemas/NUnit25/nunit_schema_2.5.xsd'
Expand Down
2 changes: 1 addition & 1 deletion src/csharp/Pester/CodeCoverageConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public static CodeCoverageConfiguration ShallowClone(CodeCoverageConfiguration c
public CodeCoverageConfiguration() : base("CodeCoverage configuration.")
{
Enabled = new BoolOption("Enable CodeCoverage.", false);
OutputFormat = new StringOption("Format to use for code coverage report. Possible values: JaCoCo, CoverageGutters", "JaCoCo");
OutputFormat = new StringOption("Format to use for code coverage report. Possible values: JaCoCo, CoverageGutters, Cobertura", "JaCoCo");
OutputPath = new StringOption("Path relative to the current directory where code coverage report is saved.", "coverage.xml");
OutputEncoding = new StringOption("Encoding of the output file.", "UTF8");
Path = new StringArrayOption("Directories or files to be used for code coverage, by default the Path(s) from general settings are used, unless overridden here.", new string[0]);
Expand Down
2 changes: 1 addition & 1 deletion src/en-US/about_PesterConfiguration.help.txt
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ SECTIONS AND OPTIONS
Type: bool
Default value: $false

OutputFormat: Format to use for code coverage report. Possible values: JaCoCo, CoverageGutters
OutputFormat: Format to use for code coverage report. Possible values: JaCoCo, CoverageGutters, Cobertura
Type: string
Default value: 'JaCoCo'

Expand Down
14 changes: 7 additions & 7 deletions src/functions/Coverage.Plugin.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -145,11 +145,11 @@

$configuration = $run.PluginConfiguration.Coverage

if ($configuration.OutputFormat -in 'JaCoCo', 'CoverageGutters') {
[xml] $jaCoCoReport = [xml] (Get-JaCoCoReportXml -CommandCoverage $breakpoints -TotalMilliseconds $totalMilliseconds -CoverageReport $coverageReport -Format $configuration.OutputFormat)
}
else {
throw "CodeCoverage.CoverageFormat '$($configuration.OutputFormat)' is not valid, please review your configuration."
$coverageXmlReport = switch ($configuration.OutputFormat) {
'JaCoCo' { [xml](Get-JaCoCoReportXml -CommandCoverage $breakpoints -TotalMilliseconds $totalMilliseconds -CoverageReport $coverageReport -Format 'JaCoCo') }
'CoverageGutters' { [xml](Get-JaCoCoReportXml -CommandCoverage $breakpoints -TotalMilliseconds $totalMilliseconds -CoverageReport $coverageReport -Format 'CoverageGutters') }
'Cobertura' { [xml](Get-CoberturaReportXml -CoverageReport $coverageReport -TotalMilliseconds $totalMilliseconds) }
default { throw "CodeCoverage.CoverageFormat '$($configuration.OutputFormat)' is not valid, please review your configuration." }
}

$settings = [Xml.XmlWriterSettings] @{
Expand All @@ -163,7 +163,7 @@
$stringWriter = [Pester.Factory]::CreateStringWriter()
$xmlWriter = [Xml.XmlWriter]::Create($stringWriter, $settings)

$jaCocoReport.WriteContentTo($xmlWriter)
$coverageXmlReport.WriteContentTo($xmlWriter)

$xmlWriter.Flush()
$stringWriter.Flush()
Expand Down Expand Up @@ -216,7 +216,7 @@
}

function Resolve-CodeCoverageConfiguration {
$supportedFormats = 'JaCoCo', 'CoverageGutters'
$supportedFormats = 'JaCoCo', 'CoverageGutters', 'Cobertura'
if ($PesterPreference.CodeCoverage.OutputFormat.Value -notin $supportedFormats) {
throw (Get-StringOptionErrorMessage -OptionPath 'CodeCoverage.OutputFormat' -SupportedValues $supportedFormats -Value $PesterPreference.CodeCoverage.OutputFormat.Value)
}
Expand Down
262 changes: 250 additions & 12 deletions src/functions/Coverage.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -231,12 +231,12 @@ function Get-CodeCoverageFilePaths {
$testsPattern = "*$($PesterPreference.Run.TestExtension.Value)"

[string[]] $filteredFiles = @(foreach ($file in (& $SafeCommands['Get-ChildItem'] -LiteralPath $Paths -File -Recurse:$RecursePaths)) {
if (('.ps1', '.psm1') -contains $file.Extension -and ($IncludeTests -or $file.Name -notlike $testsPattern)) {
$file.FullName
}
})
if (('.ps1', '.psm1') -contains $file.Extension -and ($IncludeTests -or $file.Name -notlike $testsPattern)) {
$file.FullName
}
})

$uniqueFiles = & $SafeCommands['New-Object'] -TypeName 'System.Collections.Generic.HashSet[string]' -ArgumentList (,$filteredFiles)
$uniqueFiles = & $SafeCommands['New-Object'] -TypeName 'System.Collections.Generic.HashSet[string]' -ArgumentList (, $filteredFiles)
return $uniqueFiles
}

Expand Down Expand Up @@ -452,6 +452,14 @@ function IsIgnoredCommand {
return $true
}

if ($PSVersionTable.PSVersion.Major -ge 5) {
if ($Command -is [System.Management.Automation.Language.CommandExpressionAst] -and
$Command.Expression[0] -is [System.Management.Automation.Language.BaseCtorInvokeMemberExpressionAst]) {
# Calls to inherited "base(...)" constructor does not trigger breakpoint or tracer hit, ignore.
return $true
}
}

return $false
}

Expand Down Expand Up @@ -804,9 +812,10 @@ function Get-JaCoCoReportXml {
return [string]::Empty
}

$now = & $SafeCommands['Get-Date']
$nineteenSeventy = & $SafeCommands['Get-Date'] -Date "01/01/1970"
[long] $endTime = [math]::Floor((New-TimeSpan -start $nineteenSeventy -end $now).TotalMilliseconds)
# Report uses unix epoch time format (milliseconds since midnight 1/1/1970 UTC)
$nineteenSeventy = & $SafeCommands['New-Object'] 'System.DateTime' -ArgumentList @(1970, 1, 1, 0, 0, 0, [System.DateTimeKind]::Utc)
$now = [DateTime]::Now.ToUniversalTime()
[long] $endTime = [math]::Floor(($now - $nineteenSeventy).TotalMilliseconds)
[long] $startTime = [math]::Floor($endTime - $TotalMilliseconds)

$folderGroups = $CommandCoverage | & $SafeCommands["Group-Object"] -Property {
Expand Down Expand Up @@ -1028,6 +1037,226 @@ function Get-JaCoCoReportXml {
return $xml
}

function Get-CoberturaReportXml {
param (
[parameter(Mandatory = $true)]
[object] $CoverageReport,
[parameter(Mandatory = $true)]
[long] $TotalMilliseconds
)

if ($null -eq $CoverageReport -or $CoverageReport.NumberOfCommandsAnalyzed -eq 0) {
return [string]::Empty
}

# Report uses unix epoch time format (milliseconds since midnight 1/1/1970 UTC)
$nineteenSeventy = & $SafeCommands['New-Object'] 'System.DateTime' -ArgumentList @(1970, 1, 1, 0, 0, 0, [System.DateTimeKind]::Utc)
$now = [DateTime]::Now.ToUniversalTime()
[long] $endTime = [math]::Floor(($now - $nineteenSeventy).TotalMilliseconds)
[long] $startTime = [math]::Floor($endTime - $TotalMilliseconds)

$commonRoot = Get-CommonParentPath -Path $CoverageReport.AnalyzedFiles

$allLines = [System.Collections.Generic.List[object]]@()
$allLines.AddRange($CoverageReport.MissedCommands)
$allLines.AddRange($CoverageReport.HitCommands)
$packages = @{}
foreach ($command in $allLines) {
$package = & $SafeCommands["Split-Path"] $command.File -Parent
if (!$packages[$package]) {
$packages[$package] = @{
Classes = @{}
}
}

$class = $command.File
if (!$packages[$package].Classes[$class]) {
$packages[$package].Classes[$class] = @{
Methods = @{}
Lines = @{}
}
}

if (!$packages[$package].Classes[$class].Lines[$command.Line]) {
$packages[$package].Classes[$class].Lines[$command.Line] = [ordered]@{ number = $command.Line ; hits = 0 }
}
$packages[$package].Classes[$class].Lines[$command.Line].hits += $command.HitCount

$method = $command.Function
if (!$method) {
continue
}

if (!$packages[$package].Classes[$class].Methods[$method]) {
$packages[$package].Classes[$class].Methods[$method] = @{}
}

if (!$packages[$package].Classes[$class].Methods[$method][$command.Line]) {
$packages[$package].Classes[$class].Methods[$method][$command.Line] = [ordered]@{ number = $command.Line ; hits = 0 }
}
$packages[$package].Classes[$class].Methods[$method][$command.Line].hits += $command.HitCount
}

$packages = foreach ($packageGroup in $packages.GetEnumerator()) {
$classGroups = $packageGroup.Value.Classes
$classes = foreach ($classGroup in $classGroups.GetEnumerator()) {
$methodGroups = $classGroup.Value.Methods
$methods = foreach ($methodGroup in $methodGroups.GetEnumerator()) {
$lines = ([object[]]$methodGroup.Value.Values) | New-LineNode
$coveredLines = foreach ($line in $lines) { if (0 -lt $line.attributes.hits) { $line } }

$method = [ordered]@{
name = 'method'
attributes = [ordered]@{
name = $methodGroup.Name
signature = '()'
}
children = [ordered]@{
lines = $lines | & $SafeCommands["Sort-Object"] { [int]$_.attributes.number }
}
totalLines = $lines.Length
coveredLines = $coveredLines.Length
}

$method
}

$lines = ([object[]]$classGroup.Value.Lines.Values) | New-LineNode
$coveredLines = foreach ($line in $lines) { if (0 -lt $line.attributes.hits) { $line } }

$lineRate = Get-LineRate -CoveredLines $coveredLines.Length -TotalLines $lines.Length
$filename = $classGroup.Name.Substring($commonRoot.Length).Replace('\', '/').TrimStart('/')

$class = [ordered]@{
name = 'class'
attributes = [ordered]@{
name = (& $SafeCommands["Split-Path"] $classGroup.Name -Leaf)
filename = $filename
'line-rate' = $lineRate
'branch-rate' = 1
}
children = [ordered]@{
methods = $methods | & $SafeCommands["Sort-Object"] { $_.attributes.name }
lines = $lines | & $SafeCommands["Sort-Object"] { [int]$_.attributes.number }
}
totalLines = $lines.Length
coveredLines = $coveredLines.Length
}

$class
}

$totalLines = ($classes.totalLines | & $SafeCommands["Measure-Object"] -Sum).Sum
$coveredLines = ($classes.coveredLines | & $SafeCommands["Measure-Object"] -Sum).Sum
$lineRate = Get-LineRate -CoveredLines $coveredLines -TotalLines $totalLines
$packageName = $packageGroup.Name.Substring($commonRoot.Length).Replace('\', '/').TrimStart('/')

$package = [ordered]@{
name = 'package'
attributes = [ordered]@{
name = $packageName
'line-rate' = $lineRate
'branch-rate' = 0
}
children = [ordered]@{
classes = $classes | & $SafeCommands["Sort-Object"] { $_.attributes.name }
}
totalLines = $totalLines
coveredLines = $coveredLines
}

$package
}

$totalLines = ($packages.totalLines | & $SafeCommands["Measure-Object"] -Sum).Sum
$coveredLines = ($packages.coveredLines | & $SafeCommands["Measure-Object"] -Sum).Sum
$lineRate = Get-LineRate -CoveredLines $coveredLines -TotalLines $totalLines

$coverage = [ordered]@{
name = 'coverage'
attributes = [ordered]@{
'lines-valid' = $totalLines
'lines-covered' = $coveredLines
'line-rate' = $lineRate
'branches-valid' = 0
'branches-covered' = 0
'branch-rate' = 1
timestamp = $startTime
version = 0.1
}
children = [ordered]@{
sources = [ordered]@{
name = 'source'
value = $commonRoot.Replace('\', '/')
}
packages = $packages | & $SafeCommands["Sort-Object"] { $_.attributes.name }
}
}

$xmlDeclaration = '<?xml version="1.0" ?>'
$docType = '<!DOCTYPE coverage SYSTEM "coverage-loose.dtd">'
$coverageXml = ConvertTo-XmlElement -Node $coverage
$document = "$xmlDeclaration`n$docType`n$($coverageXml.OuterXml)"

$document
}

function New-LineNode {
param(
[parameter(Mandatory = $true, ValueFromPipeline = $true)] [object] $LineObject
)

process {
[ordered]@{
name = 'line'
attributes = $LineObject
}
}
}

function Get-LineRate {
param(
[parameter(Mandatory = $true)] [int] $CoveredLines,
[parameter(Mandatory = $true)] [int] $TotalLines
)

[double]$denominator = if ($TotalLines) { $TotalLines } else { 1 }

$CoveredLines / $denominator
}

function ConvertTo-XmlElement {
param(
[parameter(Mandatory = $true)] [object] $Node
)

$element = ([xml]"<$($Node.name)/>").DocumentElement
if ($node.attributes) {
$attributes = $node.attributes
foreach ($attr in $attributes.GetEnumerator()) {
$element.SetAttribute($attr.Name, $attr.Value)
}
}
if ($node.children) {
$children = $node.children
foreach ($child in $children.GetEnumerator()) {
$childElement = ([xml]"<$($child.Name)/>").DocumentElement
foreach ($value in $child.Value) {
$childXml = ConvertTo-XmlElement $value
$importedChildXml = $childElement.OwnerDocument.ImportNode($childXml, $true)
$null = $childElement.AppendChild($importedChildXml)
}
$importedChild = $element.OwnerDocument.ImportNode($childElement, $true)
$null = $element.AppendChild($importedChild)
}
}
if ($node.value) {
$element.InnerText = $node.value
}

$element
}

function Add-XmlElement {
param (
[parameter(Mandatory = $true)] [System.Xml.XmlNode] $Parent,
Expand All @@ -1036,14 +1265,23 @@ function Add-XmlElement {
)
$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
}
Add-XmlAttribute -Element $element -Attributes $Attributes
}
return $element
}

function Add-XmlAttribute {
param(
[parameter(Mandatory = $true)] [System.Xml.XmlNode] $Element,
[parameter(Mandatory = $true)] [System.Collections.IDictionary] $Attributes
)

foreach ($key in $Attributes.Keys) {
$attribute = $Element.Attributes.Append($Element.OwnerDocument.CreateAttribute($key))
$attribute.Value = $Attributes.$key
}
}

function Add-JaCoCoCounter {
param (
[parameter(Mandatory = $true)] [ValidateSet('Instruction', 'Line', 'Method', 'Class')] [string] $Type,
Expand Down
Loading