Skip to content
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
2 changes: 1 addition & 1 deletion test/tools/OpenCover/OpenCover.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Copyright = '(c) Microsoft Corporation. All rights reserved.'
Description = 'Module to install OpenCover and run Powershell tests to collect code coverage'
DotNetFrameworkVersion = 4.5
FormatsToProcess = @('OpenCover.Format.ps1xml')
FunctionsToExport = @('Get-CodeCoverage','Compare-CodeCoverage', 'Install-OpenCover', 'Invoke-OpenCover')
FunctionsToExport = @('Get-CodeCoverage','Compare-CodeCoverage', 'Install-OpenCover', 'Invoke-OpenCover','Format-FileCoverage')
CmdletsToExport = @()
VariablesToExport = @()
AliasesToExport = @()
Expand Down
166 changes: 166 additions & 0 deletions test/tools/OpenCover/OpenCover.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,171 @@ function Get-ClassCoverageData([xml.xmlelement]$element)
return $classes
}

# region FileCoverage

class FileCoverage
{
[string]$Path
[Collections.Generic.HashSet[int]]$Hit
[Collections.Generic.HashSet[int]]$Miss
[int]$SequencePointCount = 0
[Double]$Coverage
FileCoverage([string]$p) {
$this.Path = $p
$this.Hit = [Collections.Generic.HashSet[int]]::new()
$this.Miss = [Collections.Generic.HashSet[int]]::new()
}
}

<#
.Synopsis
Format the coverage data for a file
.Description
Show the lines which were hit or not in a specific file. Line numbers are included in the output.
If a line was hit during a test run a '+' will follow the line number, if a line was missed, a '-'
will follow the line number. If a line is not hittable, it will not show '+' or '-'.

You can map file locations with the -oldBase and -newBase parameters (see example below), so you can
view coverage on a system with a different file layout. It is obvious to note that if files are different
between the systems, the results will be misleading
.EXAMPLE
PS> $coverage = Get-CodeCoverage -CoverageXmlFile .\opencover.xml
PS> Format-FileCoverage -FileCoverageData $coverage.FileCoverage -filter "CredSSP.cs"

...
0790 try
0791 + {
0792 // ServiceController.Start will return before the service is actually started
0793 // This API will wait forever
0794 + serviceController.WaitForStatus(
0795 + targetStatus,
0796 + new TimeSpan(20000000) // 2 seconds
0797 + );
0798 + return true; // service reached target status
0799 }
0800 - catch (System.ServiceProcess.TimeoutException) // still waiting
0801 - {
0802 - if (serviceController.Status != pendingStatus
...

.EXAMPLE
Map the file location from C:\projects\powershell-f975h to /users/james/src
PS> $coverage = Get-CodeCoverage -CoverageXmlFile .\opencover.xml
PS> $formatArgs = @{
FileCoverageData = $coverage.FileCoverage
filter = "Service.cs"
oldBase = "C:\\projects\\powershell-f975h"
newBase = "/users/james/src"
}
PS> Format-FileCoverage @formatArgs

...
0790 try
0791 + {
0792 // ServiceController.Start will return before the service is actually started
0793 // This API will wait forever
0794 + serviceController.WaitForStatus(
0795 + targetStatus,
0796 + new TimeSpan(20000000) // 2 seconds
0797 + );
0798 + return true; // service reached target status
0799 }
0800 - catch (System.ServiceProcess.TimeoutException) // still waiting
0801 - {
0802 - if (serviceController.Status != pendingStatus
...
#>
function Format-FileCoverage
{
param (
[Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)]$FileCoverageData,
[Parameter()][string]$oldBase = "",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be have a default value for this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

under most circumstances (for example, when running on the system where the data was collected), it's not needed at all, so I don't want to do any substitution. By setting it to default of "" and "" (for old and new) no substitution takes place and everything works as expected.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is precedence here with Invoke-OpenCover, we try to find powershell root directory and build path.

Copy link
Collaborator Author

@JamesWTruher JamesWTruher Aug 15, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it's the wrong thing to do. This may be used completely outside of a repo unlike invoke-opencover. consider the scenario inspecting coverage on a remote machine where the paths are not the same as what is in the file. so I can say -oldbase f:/github/blah -newwbase \\system\share\blah If you're on the system where you collected the data, you don't to substitute anything, doing a replacement is unneeded. In this scenario (which is quite real, since I work on mac). The path in the log is wrong for me, and my repo may not be the same as where the coverage was gathered. I don't need a lot of complicated, fragile logic when I can just supply it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Closed.

[Parameter()][string]$newBase = "",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe use git to find project root for the default value?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see above - generally don't want to change the path, but sometimes I do. setting this to "" makes it work nearly all the time, but when I'm working on my mac which has different path structure, I can still look at the coverage data. In this case I may not be in a repo at all.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above about precedence in Invoke-OpenCover.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Closed.

[Parameter()][string]$filter = ".*"
)

$files = @($fileCoverageData.Keys) | Where-Object { $_ -match $filter }

foreach ( $file in $files ) {
$coverageData = $FileCoverageData[$file]
$filepath = $file -replace "$oldBase","${newBase}"
if ( test-path $filepath ) {
$content = get-content $filepath
for($i = 0; $i -lt $content.length; $i++ ) {
if ( $coverageData.Hit -contains ($i+1)) {
$sign = "+"
}
elseif ( $coverageData.Miss -contains ($i+1)) {
$sign = "-"
}
else {
$sign = " "
}
$outputline = "{0:0000} {1} {2}" -f ($i+1),$sign,$content[$i]
if ( $sign -eq "+" ) { write-host -fore green $outputline }
elseif ( $sign -eq "-" ) { write-host -fore red $outputline }
else { write-host -fore white $outputline }
}
}
else {
Write-Error "Cannot find $filepath"
}
}
}

function Get-FileCoverageData([xml]$CoverageData)
{
$result = [Collections.Generic.Dictionary[string,FileCoverage]]::new()
$count = 0
Write-Progress "collecting files"
$filehash = $CoverageData.SelectNodes(".//File") | %{ $h = @{} } { $h[$_.uid] = $_.fullpath } { $h }
Write-Progress "collecting sequence points"
$nodes = $CoverageData.SelectNodes(".//SequencePoint")
$ncount = $nodes.count
Write-Progress "scanning sequence points"
foreach($point in $nodes) {
$fileid = $point.fileid
$filepath = $filehash[$fileid]
$s = [int]$point.sl
$e = [int]$point.el
$filedata = $null
if ( ! $result.TryGetValue($filepath, [ref]$filedata) ) {
$filedata = [FileCoverage]::new($filepath)
$null = $result.Add($filepath, $filedata)
}

for($i = $s; $i -le $e; $i++) {
if ( $point.vc -eq "0" ) {
$null = $filedata.Miss.Add($i)
}
else {
$null = $filedata.Hit.Add($i)
}
}
if ( (++$count % 50000) -eq 0 ) { Write-Progress "$count of $ncount" }
}

# Almost done, we're looking at two runs, and one run might have missed a line that
# was hit in another run, so go throw each one of the collections and remove any
# hit from the miss collection
Write-Progress "Cleanup up collections"
foreach ( $key in $result.keys ) {
$collection = $null
if ( $result.TryGetValue($key, [ref]$collection ) ) {
foreach ( $hit in $collection.hit ) {
$null = $collection.miss.remove($hit)
}
$collection.SequencePointCount = $collection.Hit.Count + $Collection.Miss.Count
$collection.Coverage = $collection.Hit.Count/$collection.SequencePointCount*100
}
else {
Write-Error "Could not find '$key'"
}
}
# now return $result
$result
}
#endregion
function Get-CodeCoverageChange($r1, $r2, [string[]]$ClassName)
{
$h = @{}
Expand Down Expand Up @@ -134,6 +299,7 @@ function Get-CoverageData($xmlPath)
CoverageLogFile = $xmlPath
CoverageSummary = (Get-CoverageSummary -element $CoverageXml.CoverageSession.Summary)
Assembly = $assemblies
FileCoverage = Get-FileCoverageData $CoverageXml
}
$CoverageData.PSTypeNames.Insert(0,"OpenCover.CoverageData")
Add-Member -InputObject $CoverageData -MemberType ScriptMethod -Name GetClassCoverage -Value { param ( $name ) $this.assembly.classcoverage | Where-Object {$_.classname -match $name } }
Expand Down