diff --git a/Functions/Coverage.Tests.ps1 b/Functions/Coverage.Tests.ps1 index d1fd8b12f..f0bcdfe6b 100644 --- a/Functions/Coverage.Tests.ps1 +++ b/Functions/Coverage.Tests.ps1 @@ -88,7 +88,34 @@ InModuleScope Pester { $jaCoCoReportXml = $jaCoCoReportXml -replace 'start="[0-9]*"','start=""' $jaCoCoReportXml = $jaCoCoReportXml -replace 'dump="[0-9]*"','dump=""' $jaCoCoReportXml = $jaCoCoReportXml -replace "$([System.Environment]::NewLine)",'' - $jaCoCoReportXml | should -be '' + [String]$ReferenceReport = [String]'' + $jaCoCoReportXml | should -Be $ReferenceReport + } + Exit-CoverageAnalysis -PesterState $testState + } + + Context 'Entire file detailed coverage' { + $testState = New-PesterState -Path $root + + # Path deliberately duplicated to make sure the code doesn't produce multiple breakpoints for the same commands + Enter-CoverageAnalysis -CodeCoverage "$root\TestScript.ps1", "$root\TestScript.ps1" -PesterState $testState + + It 'Has the proper number of breakpoints defined' { + $testState.CommandCoverage.Count | Should -Be 7 + } + + $null = & "$root\TestScript.ps1" + $coverageReport = Get-CoverageReport -PesterState $testState + + It 'JaCoCo report must be correct'{ + [String]$jaCoCoReportXml = Get-JaCoCoReportXml -PesterState $testState -CoverageReport $coverageReport -DetailedCodeCoverage + $jaCoCoReportXml = $jaCoCoReportXml -replace 'Pester \([^\)]*','Pester (date' + $jaCoCoReportXml = $jaCoCoReportXml -replace 'start="[0-9]*"','start=""' + $jaCoCoReportXml = $jaCoCoReportXml -replace 'dump="[0-9]*"','dump=""' + $jaCoCoReportXml = $jaCoCoReportXml -replace "$([System.Environment]::NewLine)",'' + $jaCoCoReportXml = $jaCoCoReportXml.Replace($root,'') + [String]$ReferenceReport = "{0}{1}{2}" -f [String]'' + $jaCoCoReportXml | should -Be $ReferenceReport } Exit-CoverageAnalysis -PesterState $testState } diff --git a/Functions/Coverage.ps1 b/Functions/Coverage.ps1 index 07509d401..73123ab75 100644 --- a/Functions/Coverage.ps1 +++ b/Functions/Coverage.ps1 @@ -488,6 +488,7 @@ function Get-CoverageReport $missedCommands = @(Get-CoverageMissedCommands -CommandCoverage $PesterState.CommandCoverage | & $SafeCommands['Select-Object'] File, Line, Function, Command) $hitCommands = @(Get-CoverageHitCommands -CommandCoverage $PesterState.CommandCoverage | & $SafeCommands['Select-Object'] File, Line, Function, Command) + $allCommands = @($PesterState.CommandCoverage | & $SafeCommands['Select-Object'] File, Line, Function, Command, Breakpoint) $analyzedFiles = @($PesterState.CommandCoverage | & $SafeCommands['Select-Object'] -ExpandProperty File -Unique) $fileCount = $analyzedFiles.Count @@ -500,6 +501,7 @@ function Get-CoverageReport NumberOfCommandsMissed = $missedCommands.Count MissedCommands = $missedCommands HitCommands = $hitCommands + AllCommands = $allCommands AnalyzedFiles = $analyzedFiles } } @@ -580,7 +582,9 @@ function Get-JaCoCoReportXml { [parameter(Mandatory=$true)] $PesterState, [parameter(Mandatory=$true)] - [object] $CoverageReport + [object] $CoverageReport, + + [Switch]$DetailedCodeCoverage ) if ($null -eq $CoverageReport -or ($pester.Show -eq [Pester.OutputTypes]::None) -or $CoverageReport.NumberOfCommandsAnalyzed -eq 0) @@ -611,7 +615,11 @@ function Get-JaCoCoReportXml { $jaCoCoReport = "$([System.Environment]::NewLine)" $jaCoCoReport += "$([System.Environment]::NewLine)" $jaCoCoReport += "$([System.Environment]::NewLine)" - $jaCoCoReport += "$([System.Environment]::NewLine)" + if ($DetailedCodeCoverage) + { + $jaCoCoReport += "$([System.Environment]::NewLine)" + } + $jaCoCoReport += "" $jaCoCoReport += "$([System.Environment]::NewLine)" $jaCoCoReport += "$([System.Environment]::NewLine)" $jaCoCoReport += "$([System.Environment]::NewLine)" @@ -621,6 +629,80 @@ function Get-JaCoCoReportXml { $jaCoCoReportXml.report.name = "Pester ($now)" $jaCoCoReportXml.report.sessioninfo.start=$startTime.ToString() $jaCoCoReportXml.report.sessioninfo.dump=$endTime.ToString() + + if ($DetailedCodeCoverage) + { + + $fileName = "" + $lineNr = -1 + $report = $jaCoCoReportXml.ChildNodes[1] + $package = $report.ChildNodes[1] + $mi = 0 + $ci = 0 + $missed = 0 + $covered = 0 + $line = $null + foreach($row in $CoverageReport.AllCommands) + { + if ($sourceName -ne $row.File) + { + $xmlFile = $jaCoCoReportXml.CreateElement("sourcefile") + $xmlName = $jaCoCoReportXml.CreateAttribute("name") + $xmlName.value = $row.File + $null = $xmlFile.Attributes.Append($xmlName) + $null = $package.AppendChild($xmlFile) + $sourceName = $row.File + $lineNr = -1; + $counter = $jaCoCoReportXml.CreateElement("counter") + + $missed = 0 + $covered = 0 + + $typeCounter = $counter.Attributes.Append($jaCoCoReportXml.CreateAttribute("type")) + $typeCounter.Value = "LINE" + + $miCounter = $counter.Attributes.Append($jaCoCoReportXml.CreateAttribute("missed")) + $miCounter.value = $missed + + $ciCounter = $counter.Attributes.Append($jaCoCoReportXml.CreateAttribute("covered")) + $ciCounter.value = $covered + + $null = $xmlFile.AppendChild($counter) + } + + if ($lineNr -ne $row.Line) + { + $line = $jaCoCoReportXml.CreateElement("line") + $nr = $jaCoCoReportXml.CreateAttribute("nr") + $nr.value = $row.Line + $mi = 0 + $ci = 0 + $null = $line.Attributes.Append($nr) + $ciLine = $line.Attributes.Append($jaCoCoReportXml.CreateAttribute("ci")) + $ciLine.value = 0 + $miLine = $line.Attributes.Append($jaCoCoReportXml.CreateAttribute("mi")) + $miLine.value = 0 + $null = $xmlFile.InsertBefore($line, $counter) + $lineNr = $row.Line + } + + if ($row.Breakpoint.HitCount -eq 0) + { + $mi += 1 + $miLine.value = $mi + $missed += 1 + $miCounter.value = $missed + } + else + { + $ci += 1 + $ciLine.value = $ci + $covered += 1 + $ciCounter.value = $covered + } + } + } + $jaCoCoReportXml.report.counter[0].missed = $CoverageReport.MissedCommands.Count.ToString() $jaCoCoReportXml.report.counter[0].covered = $CoverageReport.HitCommands.Count.ToString() $jaCoCoReportXml.report.counter[1].missed = $missedLines.ToString() diff --git a/Functions/Describe.ps1 b/Functions/Describe.ps1 index 2e8e166ad..559064c5f 100644 --- a/Functions/Describe.ps1 +++ b/Functions/Describe.ps1 @@ -25,6 +25,13 @@ Optional parameter containing an array of strings. When calling Invoke-Pester, it is possible to specify a -Tag parameter which will only execute Describe blocks containing the same Tag. +.PARAMETER CodeCoverage +Adds a code coverage report to the Pester tests. Takes strings or hash table values. + +A code coverage report lists the lines of code that did and did not run during +a Pester test. This report does not tell whether code was tested; only whether +the code ran during the test. + .EXAMPLE function Add-Numbers($a, $b) { return $a + $b @@ -69,6 +76,8 @@ about_TestDrive [Alias('Tags')] [string[]] $Tag=@(), + [object[]] $CodeCoverage = @(), + [Parameter(Position = 1)] [ValidateNotNull()] [ScriptBlock] $Fixture = $(Throw "No test script block is provided. (Have you put the open curly brace on the next line?)") @@ -82,7 +91,17 @@ about_TestDrive $script:mockTable = @{} } - DescribeImpl @PSBoundParameters -CommandUsed 'Describe' -Pester $Pester -DescribeOutputBlock ${function:Write-Describe} -TestOutputBlock ${function:Write-PesterResult} + if ($Pester.FindCodeCoverage) + { + foreach($cc in $CodeCoverage) + { + $Pester.CodeCoverage += $cc + } + } + else + { + DescribeImpl @PSBoundParameters -CommandUsed 'Describe' -Pester $Pester -DescribeOutputBlock ${function:Write-Describe} -TestOutputBlock ${function:Write-PesterResult} + } } function DescribeImpl { @@ -93,6 +112,8 @@ function DescribeImpl { [Alias('Tags')] $Tag=@(), + [object[]] $CodeCoverage = @(), + [Parameter(Position = 1)] [ValidateNotNull()] [ScriptBlock] $Fixture = $(Throw "No test script block is provided. (Have you put the open curly brace on the next line?)"), diff --git a/Functions/PesterState.ps1 b/Functions/PesterState.ps1 index e3e413edd..ed7fec501 100644 --- a/Functions/PesterState.ps1 +++ b/Functions/PesterState.ps1 @@ -54,6 +54,9 @@ function New-PesterState $script:Show = $Show $script:InTest = $false + $script:FindCodeCoverage = $false + $script:CodeCoverage = @() + $script:TestResult = @() $script:TotalCount = 0 @@ -333,6 +336,8 @@ function New-PesterState "TestResult", "SessionState", "CommandCoverage", + "FindCodeCoverage", + "CodeCoverage", "Strict", "Show", "Time", diff --git a/Pester.Tests.ps1 b/Pester.Tests.ps1 index 5de23b0b5..53d138a9e 100644 --- a/Pester.Tests.ps1 +++ b/Pester.Tests.ps1 @@ -6,7 +6,7 @@ $manifestPath = (Join-Path $here 'Pester.psd1') $changeLogPath = (Join-Path $here 'CHANGELOG.md') # DO NOT CHANGE THIS TAG NAME; IT AFFECTS THE CI BUILD. - +# Describe -Tags 'VersionChecks' "Pester manifest and changelog" { $script:manifest = $null $script:tagVersion = $null diff --git a/Pester.psm1 b/Pester.psm1 index 8e094b9ae..101e5935a 100644 --- a/Pester.psm1 +++ b/Pester.psm1 @@ -604,6 +604,9 @@ Default value is: JaCoCo. Currently supported formats are: - JaCoCo - this XML file format is compatible with the VSTS/TFS +.PARAMETER DetailedCodeCoverage +Add the sourcefile names and lines covered and missed to the codecoverage file. + .PARAMETER Strict Makes Pending and Skipped tests to Failed tests. Useful for continuous integration where you need to make sure all tests passed. @@ -779,6 +782,8 @@ New-PesterOption [ValidateSet('JaCoCo')] [String]$CodeCoverageOutputFileFormat = "JaCoCo", + [Switch]$DetailedCodeCoverage = $false, + [Switch]$Strict, [Parameter(Mandatory = $true, ParameterSetName = 'NewOutputSet')] @@ -824,7 +829,6 @@ New-PesterOption try { - Enter-CoverageAnalysis -CodeCoverage $CodeCoverage -PesterState $pester Write-PesterStart $pester $Script $invokeTestScript = { @@ -843,6 +847,34 @@ New-PesterOption $testScripts = @(ResolveTestScripts $Script) + + if ($DetailedCodeCoverage) + { + $pester.FindCodeCoverage = $true + $pester.CodeCoverage = $CodeCoverage + + # find describe codecoverage here + foreach ($testScript in $testScripts) + { + try + { + do + { + & $invokeTestScript -Path $testScript.Path -Arguments $testScript.Arguments -Parameters $testScript.Parameters + } until ($true) + } + catch + { } + } + + + $pester.FindCodeCoverage = $false + $CodeCoverage = $pester.CodeCoverage + } + + + Enter-CoverageAnalysis -CodeCoverage $CodeCoverage -PesterState $pester + foreach ($testScript in $testScripts) { try @@ -875,10 +907,13 @@ New-PesterOption $pester | Write-PesterReport $coverageReport = Get-CoverageReport -PesterState $pester - Write-CoverageReport -CoverageReport $coverageReport + if ($DetailedCodeCoverage -eq $false) + { + Write-CoverageReport -CoverageReport $coverageReport + } if ((& $script:SafeCommands['Get-Variable'] -Name CodeCoverageOutputFile -ValueOnly -ErrorAction $script:IgnoreErrorPreference) ` -and (& $script:SafeCommands['Get-Variable'] -Name CodeCoverageOutputFileFormat -ValueOnly -ErrorAction $script:IgnoreErrorPreference) -eq 'JaCoCo') { - $jaCoCoReport = Get-JaCoCoReportXml -PesterState $pester -CoverageReport $coverageReport + $jaCoCoReport = Get-JaCoCoReportXml -PesterState $pester -CoverageReport $coverageReport -DetailedCodeCoverage:$DetailedCodeCoverage $jaCoCoReport | & $SafeCommands['Out-File'] $CodeCoverageOutputFile -Encoding utf8 } Exit-CoverageAnalysis -PesterState $pester diff --git a/report.xsd b/report.xsd new file mode 100644 index 000000000..b47e1586b --- /dev/null +++ b/report.xsd @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file