From f6208f3c4267f8587868aa73e8d5a3efb4ac9bec Mon Sep 17 00:00:00 2001 From: "James Truher [MSFT]" Date: Thu, 17 Aug 2017 13:39:23 -0700 Subject: [PATCH] Add File Coverage to coverage data (#4556) It is now possible to see the coverage based on the file and then format the data so a report generator is not needed. You can map the files from one location to another, so if you have a copy of the repo, but in a different location, you can still format the output in a way so it may be studied --- test/tools/OpenCover/OpenCover.psd1 | 2 +- test/tools/OpenCover/OpenCover.psm1 | 166 ++++++++++++++++++++++++++++ 2 files changed, 167 insertions(+), 1 deletion(-) diff --git a/test/tools/OpenCover/OpenCover.psd1 b/test/tools/OpenCover/OpenCover.psd1 index f66c71fbc..e0459d38c 100644 --- a/test/tools/OpenCover/OpenCover.psd1 +++ b/test/tools/OpenCover/OpenCover.psd1 @@ -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 = @() diff --git a/test/tools/OpenCover/OpenCover.psm1 b/test/tools/OpenCover/OpenCover.psm1 index b9187dc2d..fa7f8f335 100644 --- a/test/tools/OpenCover/OpenCover.psm1 +++ b/test/tools/OpenCover/OpenCover.psm1 @@ -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 = "", + [Parameter()][string]$newBase = "", + [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 = @{} @@ -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 } }