769 lines
32 KiB
PowerShell
769 lines
32 KiB
PowerShell
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||
# Licensed under the MIT License.
|
||
|
||
#region privateFunctions
|
||
|
||
$script:psRepoPath = [string]::Empty
|
||
if ($null -ne (Get-Command -Name 'git' -ErrorAction Ignore)) {
|
||
$script:psRepoPath = git rev-parse --show-toplevel
|
||
}
|
||
|
||
function Get-AssemblyCoverageData([xml.xmlelement] $element)
|
||
{
|
||
$coverageSummary = (Get-CoverageSummary -element $element.Summary)
|
||
$classCoverage = Get-ClassCoverageData $element
|
||
$AssemblyCoverageData = [PSCustomObject] @{
|
||
AssemblyName = $element.ModuleName
|
||
CoverageSummary = $coverageSummary
|
||
Branch = $coverageSummary.BranchCoverage
|
||
Sequence = $coverageSummary.SequenceCoverage
|
||
ClassCoverage = $classCoverage
|
||
}
|
||
|
||
$AssemblyCoverageData | Add-Member -MemberType ScriptMethod -Name ToString -Value { "{0} ({1})" -f $this.AssemblyName,$this.CoverageSummary.BranchCoverage } -Force
|
||
$AssemblyCoverageData.PSTypeNames.Insert(0,"OpenCover.AssemblyCoverageData")
|
||
|
||
return $AssemblyCoverageData
|
||
}
|
||
|
||
function Get-ClassCoverageData([xml.xmlelement]$element)
|
||
{
|
||
$classes = [system.collections.arraylist]::new()
|
||
foreach ( $class in $element.classes.class )
|
||
{
|
||
# skip classes with names like <>f__AnonymousType6`4
|
||
if ( $class.fullname -match "<>" ) { continue }
|
||
$name = $class.fullname
|
||
$branch = $class.summary.branchcoverage
|
||
$sequence = $class.summary.sequenceCoverage
|
||
$o = [pscustomobject]@{ ClassName = $name; Branch = $branch; Sequence = $sequence}
|
||
$o.psobject.TypeNames.Insert(0, "ClassCoverageData")
|
||
$null = $classes.Add($o)
|
||
}
|
||
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
|
||
{
|
||
[CmdletBinding()]
|
||
param (
|
||
[Parameter(Mandatory=$true,Position=0,ValueFromPipeline=$true)]$CoverageData,
|
||
[Parameter()][string]$oldBase = "",
|
||
[Parameter()][string]$newBase = ""
|
||
)
|
||
|
||
PROCESS {
|
||
$file = $CoverageData.Path
|
||
$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") | Foreach-Object { $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 = @{}
|
||
$Deltas = new-object "System.Collections.ArrayList"
|
||
|
||
if ( $ClassName ) {
|
||
foreach ( $Class in $ClassName ) {
|
||
$c1 = $r1.Assembly.ClassCoverage | Where-Object {$_.ClassName -eq $Class }
|
||
$c2 = $r2.Assembly.ClassCoverage | Where-Object {$_.ClassName -eq $Class }
|
||
$ClassCoverageChange = [pscustomobject]@{
|
||
ClassName = $Class
|
||
Branch = $c2.Branch
|
||
BranchDelta = $c2.Branch - $c1.Branch
|
||
Sequence = $c2.Sequence
|
||
SequenceDelta = $c2.Sequence - $c1.sequence
|
||
}
|
||
$ClassCoverageChange.psobject.typenames.insert(0,"ClassCoverageDelta")
|
||
Write-Output $ClassCoverageChange
|
||
}
|
||
return
|
||
}
|
||
|
||
$r1.assembly | ForEach-Object { $h[$_.assemblyname] = @($_) }
|
||
$r2.assembly | ForEach-Object {
|
||
if($h.ContainsKey($_.assemblyname))
|
||
{
|
||
$h[$_.assemblyname] += $_
|
||
}
|
||
else
|
||
{
|
||
$h[$_.assemblyname] = @($_)
|
||
}
|
||
}
|
||
|
||
foreach($kvPair in $h.GetEnumerator())
|
||
{
|
||
$runs = @($h[$kvPair.Name])
|
||
$assemblyCoverageChange = (Get-AssemblyCoverageChange -r1 $runs[0] -r2 $runs[1])
|
||
$null = $Deltas.Add($assemblyCoverageChange)
|
||
}
|
||
|
||
$CoverageChange = [PSCustomObject] @{
|
||
Run1 = $r1
|
||
Run2 = $r2
|
||
Branch = $r2.CoverageSummary.BranchCoverage
|
||
Sequence = $r2.CoverageSummary.SequenceCoverage
|
||
BranchDelta = [double] ($r2.CoverageSummary.BranchCoverage - $r1.CoverageSummary.BranchCoverage)
|
||
SequenceDelta = [double] ($r2.CoverageSummary.SequenceCoverage - $r1.CoverageSummary.SequenceCoverage)
|
||
Deltas = $Deltas
|
||
}
|
||
$CoverageChange.PSTypeNames.Insert(0,"OpenCover.CoverageChange")
|
||
|
||
return $CoverageChange
|
||
}
|
||
|
||
function Get-AssemblyCoverageChange($r1, $r2)
|
||
{
|
||
if($null -eq $r1 -and $null -ne $r2)
|
||
{
|
||
$r1 = @{ AssemblyName = $r2.AssemblyName ; Branch = 0 ; Sequence = 0 }
|
||
}
|
||
elseif($null -eq $r2 -and $null -ne $r1)
|
||
{
|
||
$r2 = @{ AssemblyName = $r1.AssemblyName ; Branch = 0 ; Sequence = 0 }
|
||
}
|
||
|
||
if ( compare-object $r1.assemblyname $r2.assemblyname ) { throw "different assemblies" }
|
||
|
||
$AssemblyCoverageChange = [pscustomobject] @{
|
||
AssemblyName = $r1.AssemblyName
|
||
Branch = $r2.Branch
|
||
BranchDelta = $r2.Branch - $r1.Branch
|
||
Sequence = $r2.Sequence
|
||
SequenceDelta = $r2.Sequence - $r1.Sequence
|
||
}
|
||
$AssemblyCoverageChange.PSTypeNames.Insert(0,"OpenCover.AssemblyCoverageChange")
|
||
return $AssemblyCoverageChange
|
||
}
|
||
|
||
function Get-CoverageData($xmlPath)
|
||
{
|
||
[xml]$CoverageXml = get-content -readcount 0 $xmlPath
|
||
if ( $null -eq $CoverageXml.CoverageSession ) { throw "CoverageSession data not found" }
|
||
|
||
$assemblies = New-Object System.Collections.ArrayList
|
||
|
||
foreach( $module in $CoverageXml.CoverageSession.modules.module| Where-Object {$_.skippedDueTo -ne "MissingPdb"}) {
|
||
$assemblies.Add((Get-AssemblyCoverageData -element $module)) | Out-Null
|
||
}
|
||
|
||
$CoverageData = [PSCustomObject] @{
|
||
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 } }
|
||
$null = $CoverageXml
|
||
|
||
Add-Member -InputObject $CoverageData -MemberType ScriptMethod -Name GetFileCoverage -Value { param ( $name = ".*" ) @($this.FileCoverage.Values) | Where-Object {$_.Path -match "$name"} }
|
||
|
||
## Adding explicit garbage collection as the $CoverageXml object tends to be very large, in order of 1 GB.
|
||
[gc]::Collect()
|
||
|
||
return $CoverageData
|
||
}
|
||
|
||
function Get-CoverageSummary([xml.xmlelement] $element)
|
||
{
|
||
$CoverageSummary = [PSCustomObject] @{
|
||
NumSequencePoints = $element.numSequencePoints
|
||
VisitedSequencePoints = $element.visitedSequencePoints
|
||
NumBranchPoints = $element.numBranchPoints
|
||
VisitedBranchPoints = $element.visitedBranchPoints
|
||
SequenceCoverage = $element.sequenceCoverage
|
||
BranchCoverage = $element.branchCoverage
|
||
MaxCyclomaticComplexity = $element.maxCyclomaticComplexity
|
||
MinCyclomaticComplexity = $element.minCyclomaticComplexity
|
||
VisitedClasses = $element.visitedClasses
|
||
NumClasses = $element.numClasses
|
||
VisitedMethods = $element.visitedMethods
|
||
NumMethods = $element.numMethods
|
||
}
|
||
|
||
$CoverageSummary | Add-Member -MemberType ScriptMethod -Name ToString -Value { "Branch:{0,3} Sequence:{1,3}" -f $this.BranchCoverage,$this.SequenceCoverage } -Force
|
||
$CoverageSummary.PSTypeNames.Insert(0,"OpenCover.CoverageSummary")
|
||
|
||
return $CoverageSummary
|
||
}
|
||
|
||
# needed for PowerShell v4 as Archive module isn't available by default
|
||
function Expand-ZipArchive([string] $Path, [string] $DestinationPath)
|
||
{
|
||
try
|
||
{
|
||
Add-Type -AssemblyName System.IO.Compression
|
||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||
|
||
$fileStream = New-Object System.IO.FileStream -ArgumentList @($Path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read)
|
||
$zipArchive = New-Object System.IO.Compression.ZipArchive -ArgumentList @($fileStream, [System.IO.Compression.ZipArchiveMode]::Read, $false)
|
||
|
||
foreach($entry in $zipArchive.Entries)
|
||
{
|
||
$extractPath = (Join-Path $DestinationPath $entry.FullName)
|
||
|
||
$fileInfo = New-Object System.IO.FileInfo -ArgumentList $extractPath
|
||
if(-not $fileInfo.Directory.Exists) { New-Item -Path $fileInfo.Directory.FullName -ItemType Directory | Out-Null }
|
||
|
||
try
|
||
{
|
||
$newfileStream = [System.IO.File]::Create($extractPath)
|
||
$entry.Open().CopyTo($newfileStream)
|
||
}
|
||
finally
|
||
{
|
||
if($newfileStream) { $newfileStream.Dispose() }
|
||
}
|
||
}
|
||
}
|
||
finally
|
||
{
|
||
if($zipArchive) { $zipArchive.Dispose() }
|
||
if($fileStream) { $fileStream.Dispose() }
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
<#
|
||
.Synopsis
|
||
Get code coverage information for the supplied coverage file.
|
||
.Description
|
||
Coverage information from the supplied OpenCover XML file is displayed. The output object has options to show assembly coverage and summary.
|
||
.EXAMPLE
|
||
PS> $coverage = Get-CodeCoverage -CoverageXmlFile .\opencover.xml
|
||
PS> $cov.assembly
|
||
|
||
AssemblyName Branch Sequence CoverageSummary
|
||
------------ ------ -------- ---------------
|
||
powershell 100 100 Branch:100 Sequence:100
|
||
Microsoft.PowerShell.CoreCLR.AssemblyLoadContext 53.66 95.31 Branch:53.66 Sequence:95.31
|
||
Microsoft.PowerShell.ConsoleHost 36.53 38.40 Branch:36.53 Sequence:38.40
|
||
System.Management.Automation 42.18 44.11 Branch:42.18 Sequence:44.11
|
||
Microsoft.PowerShell.CoreCLR.Eventing 28.70 36.23 Branch:28.70 Sequence:36.23
|
||
Microsoft.PowerShell.Security 15.17 18.16 Branch:15.17 Sequence:18.16
|
||
Microsoft.PowerShell.Commands.Management 18.84 21.70 Branch:18.84 Sequence:21.70
|
||
Microsoft.PowerShell.Commands.Utility 62.38 64.54 Branch:62.38 Sequence:64.54
|
||
Microsoft.WSMan.Management 3.93 4.45 Branch:3.93 Sequence:4.45
|
||
Microsoft.WSMan.Runtime 0 0 Branch: 0 Sequence: 0
|
||
Microsoft.PowerShell.Commands.Diagnostics 44.96 49.93 Branch:44.96 Sequence:49.93
|
||
Microsoft.PowerShell.PSReadLine 7.12 9.94 Branch:7.12 Sequence:9.94
|
||
Microsoft.PowerShell.PackageManagement 59.77 62.04 Branch:59.77 Sequence:62.04
|
||
Microsoft.PackageManagement 41.73 44.47 Branch:41.73 Sequence:44.47
|
||
Microsoft.Management.Infrastructure.CimCmdlets 13.20 17.01 Branch:13.20 Sequence:17.01
|
||
Microsoft.PackageManagement.MetaProvider.PowerShell 54.79 57.90 Branch:54.79 Sequence:57.90
|
||
Microsoft.PackageManagement.NuGetProvider 62.36 65.37 Branch:62.36 Sequence:65.37
|
||
Microsoft.PackageManagement.CoreProviders 7.08 7.96 Branch:7.08 Sequence:7.96
|
||
Microsoft.PackageManagement.ArchiverProviders 0.53 0.56 Branch:0.53 Sequence:0.56
|
||
.EXAMPLE
|
||
PS> $coverage = Get-CodeCoverage -CoverageXmlFile .\opencover.xml
|
||
PS> $cov.CoverageSummary
|
||
|
||
NumSequencePoints : 337052
|
||
VisitedSequencePoints : 143209
|
||
NumBranchPoints : 115193
|
||
VisitedBranchPoints : 46132
|
||
MaxCyclomaticComplexity : 398
|
||
MinCyclomaticComplexity : 1
|
||
VisitedClasses : 2465
|
||
NumClasses : 3894
|
||
VisitedMethods : 17792
|
||
NumMethods : 37832
|
||
SequenceCoverage : 42.49
|
||
BranchCoverage : 40.05
|
||
#>
|
||
function Get-CodeCoverage
|
||
{
|
||
param ( [string]$CoverageXmlFile = "$HOME/Documents/OpenCover.xml" )
|
||
$xmlPath = (get-item $CoverageXmlFile).Fullname
|
||
(Get-CoverageData -xmlPath $xmlPath)
|
||
}
|
||
|
||
<#
|
||
.Synopsis
|
||
Compare results between two coverage runs.
|
||
.Description
|
||
Coverage information from the supplied OpenCover XML file is displayed. The output object has options to show assembly coverage and summary.
|
||
.EXAMPLE
|
||
$comp = Compare-CodeCoverage -RunFile1 .\OpenCover.xml -RunFile2 .\OpenCover.xml
|
||
$comp.Deltas | sort-object assemblyname | format-table
|
||
|
||
AssemblyName Branch BranchDelta Sequence SequenceDelta
|
||
------------ ------ ----------- -------- -------------
|
||
Microsoft.Management.Infrastructure.CimCmdlets 13.20 0 17.01 0
|
||
Microsoft.PackageManagement 41.73 0 44.47 0
|
||
Microsoft.PackageManagement.ArchiverProviders 0.53 0 0.56 0
|
||
Microsoft.PackageManagement.CoreProviders 7.08 0 7.96 0
|
||
Microsoft.PackageManagement.MetaProvider.PowerShell 54.79 0 57.90 0
|
||
Microsoft.PackageManagement.NuGetProvider 62.36 0 65.37 0
|
||
Microsoft.PowerShell.Commands.Diagnostics 44.96 0 49.93 0
|
||
Microsoft.PowerShell.Commands.Management 18.84 0 21.70 0
|
||
Microsoft.PowerShell.Commands.Utility 62.38 0 64.54 0
|
||
Microsoft.PowerShell.ConsoleHost 36.53 0 38.40 0
|
||
Microsoft.PowerShell.CoreCLR.AssemblyLoadContext 53.66 0 95.31 0
|
||
Microsoft.PowerShell.CoreCLR.Eventing 28.70 0 36.23 0
|
||
Microsoft.PowerShell.PackageManagement 59.77 0 62.04 0
|
||
Microsoft.PowerShell.PSReadLine 7.12 0 9.94 0
|
||
Microsoft.PowerShell.Security 15.17 0 18.16 0
|
||
Microsoft.WSMan.Management 3.93 0 4.45 0
|
||
Microsoft.WSMan.Runtime 0 0 0 0
|
||
powershell 100 0 100 0
|
||
System.Management.Automation 42.18 0 44.11 0
|
||
|
||
.EXAMPLE
|
||
$comp = Compare-CodeCoverage -Run1 $c -Run2 $c
|
||
$comp.Deltas | sort-object assemblyname | format-table
|
||
|
||
AssemblyName Branch BranchDelta Sequence SequenceDelta
|
||
------------ ------ ----------- -------- -------------
|
||
Microsoft.Management.Infrastructure.CimCmdlets 13.20 0 17.01 0
|
||
Microsoft.PackageManagement 41.73 0 44.47 0
|
||
Microsoft.PackageManagement.ArchiverProviders 0.53 0 0.56 0
|
||
Microsoft.PackageManagement.CoreProviders 7.08 0 7.96 0
|
||
Microsoft.PackageManagement.MetaProvider.PowerShell 54.79 0 57.90 0
|
||
Microsoft.PackageManagement.NuGetProvider 62.36 0 65.37 0
|
||
Microsoft.PowerShell.Commands.Diagnostics 44.96 0 49.93 0
|
||
Microsoft.PowerShell.Commands.Management 18.84 0 21.70 0
|
||
Microsoft.PowerShell.Commands.Utility 62.38 0 64.54 0
|
||
Microsoft.PowerShell.ConsoleHost 36.53 0 38.40 0
|
||
Microsoft.PowerShell.CoreCLR.AssemblyLoadContext 53.66 0 95.31 0
|
||
Microsoft.PowerShell.CoreCLR.Eventing 28.70 0 36.23 0
|
||
Microsoft.PowerShell.PackageManagement 59.77 0 62.04 0
|
||
Microsoft.PowerShell.PSReadLine 7.12 0 9.94 0
|
||
Microsoft.PowerShell.Security 15.17 0 18.16 0
|
||
Microsoft.WSMan.Management 3.93 0 4.45 0
|
||
Microsoft.WSMan.Runtime 0 0 0 0
|
||
powershell 100 0 100 0
|
||
System.Management.Automation 42.18 0 44.11 0
|
||
#>
|
||
function Compare-CodeCoverage
|
||
{
|
||
[CmdletBinding()]
|
||
param (
|
||
[Parameter(Mandatory=$true,Position=0,ParameterSetName="file")][string]$RunFile1,
|
||
[Parameter(Mandatory=$true,Position=1,ParameterSetName="file")][string]$RunFile2,
|
||
[Parameter(Mandatory=$true,Position=0,ParameterSetName="coverage")][Object]$Run1,
|
||
[Parameter(Mandatory=$true,Position=1,ParameterSetName="coverage")][Object]$Run2,
|
||
[Parameter()][String[]]$ClassName,
|
||
[Parameter()][switch]$Summary
|
||
)
|
||
|
||
if ( $PSCmdlet.ParameterSetName -eq "file" )
|
||
{
|
||
[string]$xmlPath1 = (get-item $Run1File).Fullname
|
||
$Run1 = (Get-CoverageData -xmlPath $xmlPath1)
|
||
|
||
[string]$xmlPath2 = (get-item $Run1File).Fullname
|
||
$Run2 = (Get-CoverageData -xmlPath $xmlPath2)
|
||
}
|
||
|
||
$change = Get-CodeCoverageChange -r1 $Run1 -r2 $Run2 -Class $ClassName
|
||
if ( $Summary -or $ClassName )
|
||
{
|
||
$change
|
||
}
|
||
else
|
||
{
|
||
$change.Deltas
|
||
}
|
||
}
|
||
|
||
function Compare-FileCoverage
|
||
{
|
||
param (
|
||
[Parameter(Position=0,Mandatory=$true)]$ReferenceCoverage,
|
||
[Parameter(Position=1,Mandatory=$true)]$DifferenceCoverage,
|
||
[Parameter(Position=2,Mandatory=$true)]$FileName
|
||
)
|
||
# create a couple of hashtables where the key is the path
|
||
# so we can compare file coverage
|
||
$reference = $ReferenceCoverage.GetFileCoverage($FileName) | Foreach-Object { $h = @{} } { $h[$_.path] = $_ } {$h}
|
||
$difference = $differenceCoverage.GetFileCoverage($FileName) | Foreach-Object { $h = @{}}{ $h[$_.path] = $_ }{$h }
|
||
# based on the paths, create objects which show the difference between the two runs
|
||
$reference.Keys | Sort-Object | Foreach-Object {
|
||
$referenceObject = $reference[$_]
|
||
$differenceObject = $difference[$_]
|
||
if ( $differenceObject )
|
||
{
|
||
$fileCoverageObject = [pscustomobject]@{
|
||
FileName = [io.path]::GetFileName($_)
|
||
FilePath = "$_"
|
||
ReferenceCoverage = $ReferenceObject.Coverage
|
||
DifferenceCoverage = $DifferenceObject.Coverage
|
||
CoverageDelta = $DifferenceObject.Coverage - $ReferenceObject.Coverage
|
||
}
|
||
$fileCoverageObject.psobject.typenames.Insert(0,"FileCoverageComparisonObject")
|
||
$fileCoverageObject
|
||
}
|
||
else
|
||
{
|
||
Write-Warning "skipping '$_', not found in difference"
|
||
}
|
||
}
|
||
}
|
||
|
||
<#
|
||
.Synopsis
|
||
Install OpenCover by downloading the 4.6.519 version.
|
||
.Description
|
||
Install OpenCover version 4.6.519.
|
||
#>
|
||
function Install-OpenCover
|
||
{
|
||
param (
|
||
[parameter()][string]$Version = "4.6.519",
|
||
[parameter()][string]$TargetDirectory = "$home",
|
||
[parameter()][switch]$Force
|
||
)
|
||
|
||
$filename = "opencover.${version}.zip"
|
||
$tempPath = "$env:TEMP/$Filename"
|
||
$packageUrl = "https://github.com/OpenCover/opencover/releases/download/${version}/${filename}"
|
||
if ( test-path $tempPath )
|
||
{
|
||
if ( $force )
|
||
{
|
||
remove-item -force $tempPath
|
||
}
|
||
else
|
||
{
|
||
throw "Package already exists at $tempPath, not continuing. Use -force to re-install"
|
||
}
|
||
}
|
||
if ( test-path "$TargetDirectory/OpenCover" )
|
||
{
|
||
if ( $force )
|
||
{
|
||
remove-item -recurse -force "$TargetDirectory/OpenCover"
|
||
}
|
||
else
|
||
{
|
||
throw "$TargetDirectory/OpenCover exists, not continuing. Use -force to re-install"
|
||
}
|
||
}
|
||
|
||
Invoke-WebRequest -Uri $packageUrl -OutFile "$tempPath"
|
||
if ( ! (test-path $tempPath) )
|
||
{
|
||
throw "Download failed: $packageUrl"
|
||
}
|
||
|
||
## We add ErrorAction as we do not have this module on PS v4 and below. Calling import-module will throw an error otherwise.
|
||
import-module Microsoft.PowerShell.Archive -ErrorAction SilentlyContinue
|
||
|
||
if ($null -ne (Get-Command Expand-Archive -ErrorAction Ignore)) {
|
||
Expand-Archive -Path $tempPath -DestinationPath "$TargetDirectory/OpenCover"
|
||
} else {
|
||
Expand-ZipArchive -Path $tempPath -DestinationPath "$TargetDirectory/OpenCover"
|
||
}
|
||
Remove-Item -force $tempPath
|
||
}
|
||
|
||
<#
|
||
.Synopsis
|
||
Invoke-OpenCover runs tests under OpenCover to collect code coverage.
|
||
.Description
|
||
Invoke-OpenCover runs tests under OpenCover by executing tests on PowerShell located at $PowerShellExeDirectory.
|
||
.EXAMPLE
|
||
Invoke-OpenCover -TestPath $pwd/test/powershell -PowerShellExeDirectory $pwd/src/powershell-win-core/bin/CodeCoverage/netcoreapp1.0/win7-x64
|
||
#>
|
||
function Invoke-OpenCover
|
||
{
|
||
[CmdletBinding(SupportsShouldProcess=$true)]
|
||
param (
|
||
[parameter()]$OutputLog = "$home/Documents/OpenCover.xml",
|
||
[parameter()]$TestPath = "${script:psRepoPath}/test/powershell",
|
||
[parameter()]$OpenCoverPath = "$home/OpenCover",
|
||
[parameter()]$PowerShellExeDirectory = "${script:psRepoPath}/src/powershell-win-core/bin/CodeCoverage/netcoreapp2.1/win7-x64/publish",
|
||
[parameter()]$PesterLogElevated = "$HOME/Documents/TestResultsElevated.xml",
|
||
[parameter()]$PesterLogUnelevated = "$HOME/Documents/TestResultsUnelevated.xml",
|
||
[parameter()]$PesterLogFormat = "NUnitXml",
|
||
[parameter()]$TestToolsModulesPath = "${script:psRepoPath}/test/tools/Modules",
|
||
[switch]$CIOnly,
|
||
[switch]$SuppressQuiet
|
||
)
|
||
|
||
# check for elevation
|
||
$identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
|
||
$principal = New-Object System.Security.Principal.WindowsPrincipal($identity)
|
||
$isElevated = $principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)
|
||
|
||
if(-not $isElevated)
|
||
{
|
||
throw 'Please run from an elevated PowerShell.'
|
||
}
|
||
|
||
# check to be sure that OpenCover is present
|
||
|
||
$OpenCoverBin = "$OpenCoverPath\opencover.console.exe"
|
||
|
||
if ( ! (test-path $OpenCoverBin))
|
||
{
|
||
# see if it's somewhere else in the path
|
||
$openCoverBin = (Get-Command -Name 'opencover.console' -ErrorAction Ignore).Source
|
||
if ($null -eq $openCoverBin) {
|
||
throw "$OpenCoverBin does not exist, use Install-OpenCover"
|
||
}
|
||
}
|
||
|
||
# check to be sure that pwsh.exe is present
|
||
$target = "${PowerShellExeDirectory}\pwsh.exe"
|
||
if ( ! (test-path $target) )
|
||
{
|
||
throw "$target does not exist, use 'Start-PSBuild -configuration CodeCoverage'"
|
||
}
|
||
|
||
# create the arguments for OpenCover
|
||
|
||
$updatedEnvPath = "${PowerShellExeDirectory}\Modules;$TestToolsModulesPath"
|
||
$testToolsExePath = (Resolve-Path(Join-Path $TestPath -ChildPath "..\tools\TestExe\bin")).Path
|
||
$testServiceExePath = (Resolve-Path(Join-Path $TestPath -ChildPath "..\tools\TestService\bin")).Path
|
||
$updatedProcessEnvPath = "${testServiceExePath};${testToolsExePath};${env:PATH}"
|
||
|
||
$startupArgs = "Set-ExecutionPolicy Bypass -Force -Scope Process; `$env:PSModulePath = '${updatedEnvPath}'; `$env:Path = '${updatedProcessEnvPath}';"
|
||
$targetArgs = "${startupArgs}", "Invoke-Pester","${TestPath}","-OutputFormat $PesterLogFormat"
|
||
|
||
if ( $CIOnly )
|
||
{
|
||
$targetArgsElevated = $targetArgs + @("-excludeTag @('Feature','Scenario','Slow')", "-Tag @('RequireAdminOnWindows')")
|
||
$targetArgsUnelevated = $targetArgs + @("-excludeTag @('Feature','Scenario','Slow','RequireAdminOnWindows')")
|
||
}
|
||
else
|
||
{
|
||
$targetArgsElevated = $targetArgs + @("-Tag @('RequireAdminOnWindows')")
|
||
$targetArgsUnelevated = $targetArgs + @("-excludeTag @('RequireAdminOnWindows')")
|
||
}
|
||
|
||
$targetArgsElevated += @("-OutputFile $PesterLogElevated")
|
||
$targetArgsUnelevated += @("-OutputFile $PesterLogUnelevated")
|
||
|
||
if(-not $SuppressQuiet)
|
||
{
|
||
$targetArgsElevated += @("-Show None")
|
||
$targetArgsUnelevated += @("-Show None")
|
||
}
|
||
|
||
$cmdlineElevated = CreateOpenCoverCmdline -target $target -outputLog $OutputLog -targetArgs $targetArgsElevated
|
||
$cmdlineUnelevated = CreateOpenCoverCmdline -target $target -outputLog $OutputLog -targetArgs $targetArgsUnelevated
|
||
|
||
if ( $PSCmdlet.ShouldProcess("$OpenCoverBin $cmdlineUnelevated") )
|
||
{
|
||
try
|
||
{
|
||
# invoke OpenCover elevated
|
||
# Write the command line to a file and then invoke file.
|
||
# '&' invoke caused issues with cmdline parameters for opencover.console.exe
|
||
$elevatedFile = "$env:temp\elevated.ps1"
|
||
"$OpenCoverBin $cmdlineElevated" | Out-File -FilePath $elevatedFile -force
|
||
powershell.exe -file $elevatedFile
|
||
|
||
# invoke OpenCover unelevated and poll for completion
|
||
$unelevatedFile = "$env:temp\unelevated.ps1"
|
||
"$openCoverBin $cmdlineUnelevated" | Out-File -FilePath $unelevatedFile -Force
|
||
runas.exe /trustlevel:0x20000 "powershell.exe -file $unelevatedFile"
|
||
# poll for process exit every 60 seconds
|
||
# timeout of 12 hours
|
||
# Runs currently take about 8-9 hours, we picked 12 hours to be substantially larger.
|
||
$timeOut = ([datetime]::Now).AddHours(12)
|
||
|
||
$openCoverExited = $false
|
||
|
||
while([datetime]::Now -lt $timeOut)
|
||
{
|
||
Start-Sleep -Seconds 60
|
||
$openCoverProcess = Get-Process "OpenCover.Console" -ErrorAction SilentlyContinue
|
||
|
||
if(-not $openCoverProcess)
|
||
{
|
||
#run must have completed.
|
||
$openCoverExited = $true
|
||
break
|
||
}
|
||
}
|
||
|
||
if(-not $openCoverExited)
|
||
{
|
||
throw "Opencover has not exited in 12 hours"
|
||
}
|
||
}
|
||
finally
|
||
{
|
||
Remove-Item $elevatedFile -force -ErrorAction SilentlyContinue
|
||
Remove-Item $unelevatedFile -force -ErrorAction SilentlyContinue
|
||
}
|
||
}
|
||
}
|
||
|
||
function CreateOpenCoverCmdline($target, $outputLog, $targetArgs)
|
||
{
|
||
$targetArgString = $targetArgs -join " "
|
||
|
||
$bytes = [System.Text.Encoding]::Unicode.GetBytes($targetArgString)
|
||
$base64targetArgs = [convert]::ToBase64String($bytes)
|
||
|
||
# the order seems to be important. Always keep -targetargs as the last parameter.
|
||
$cmdline = "-target:$target",
|
||
"-register:user",
|
||
"-output:${outputLog}",
|
||
"-nodefaultfilters",
|
||
"-oldstyle",
|
||
"-hideskipped:all",
|
||
"-mergeoutput",
|
||
"-filter:`"+[*]* -[Microsoft.PowerShell.PSReadLine]*`"",
|
||
"-targetargs:`"-NoProfile -EncodedCommand $base64targetArgs`""
|
||
|
||
$cmdlineAsString = $cmdline -join " "
|
||
|
||
return $cmdlineAsString
|
||
|
||
}
|