Show-ScriptCoverage.ps1

function Show-ScriptCoverage
{
    <#
    .Synopsis
        Shows script coverage for a file or series of files
    .Description
        This adds a breakpoint to each line of a script file and then runs the files or runs a command
        This will output what lines within the file were hit
    .Example
        # Start out by making en empty directory
        New-Item .\TestDirectory -ItemType Directory -Force
         
        # Now let's create two scripts. The first script has
        # a 50/50 chance of saying "Hello World" or "Goodbye World"
        '
        if ((0,1 | Get-Random)) {
            "Hello World"
        } else {
            "Goodbye World"
        }
        ' |
            Out-File .\TestDirectory\Chance.ps1
         
        # The second script will say Hello World or Goodbye World depending
        # on if it was passed a parameter
        '
        param ($a)
        if ($a) {
            "Hello World"
        } else {
            "Goodbye World"
        }
        ' > .\TestDirectory\Parameter.ps1
 
        # We pipe the output of Get-ChildItem into Show-ScriptCoverage. This will run
        # each of the scripts, and return a property bag containing:
        # - The name of the file that is being instrumented
        # - The output of the script
        # - Any errors encountered running the script
        # - A visual representation of what lines in the script were hit
        # By Piping it into Select-Object and expanding the coverage property, we'll
        # just see what lines were hit
        Get-ChildItem .\TestDirectory |
            Show-ScriptCoverage |
            Select-Object -ExpandProperty Coverage
         
        # Cool. Now let's be polite and clean up the files we created
        Remove-Item .\TestDirectory -Recurse -Force
    #>
    
    [CmdletBinding(DefaultParameterSetName="NoArguments")]
    param(
    # The file to instrument
    [Parameter(Mandatory=$true,Position=0,ValueFromPipelineByPropertyName=$true)]
    [Alias('FullName')]
    [Alias('Path')]
    [String]
    $File,
    
    # The arguments to pass to the script file or command
    [Parameter(ParameterSetName='ArgumentList',
        HelpMessage='The arguments to pass to the script',
        ValueFromRemainingArguments=$true)]
    [PSObject[]]
    $Args,
    
    # A dictionary of parameters to pass to the file
    [Parameter(ParameterSetName='ParameterDictionary',
        HelpMessage='A hashtable of parameters to to pass to the script')]    
    [Hashtable]
    $Parameter,
    
    # If this is set, the scripts will not be dot-sourced. Otherwise, all scripts
    # will be dot-sourced
    [Switch]
    $DoNotDotSource,
    
    # The command you are going to run.
    [ScriptBlock]
    $Command = {}
    )
    
    
    begin {
        $breakpoints = New-Object System.Collections.ArrayList     
        $ScriptContents = @{}
    }
    
    process {
        $WarningPreference = "SilentlyContinue"
        
        $scriptContent = @(Get-Content -ErrorAction SilentlyContinue $file)
        $realFile = Resolve-Path $file
        $ScriptContents["$RealFile"] = $ScriptContent
        for ($i = 1; $i -le $scriptContent.Count; $i++)
        {
            if ($scriptContent[$i - 1].Trim())
            {
                $sb = [ScriptBLock]::Create({
if (-not ($global:ScriptCoverageProfiler)) {
    $global:ScriptCoverageProfiler = @{}
}                
                }.ToString() + "
if (-not (`$global:ScriptCoverageProfiler[`"$file.$i`"])) {
    `$global:ScriptCoverageProfiler[`"$file.$i`"] = @()
}
`$global:ScriptCoverageProfiler[`"$file.$i`"] += Get-Date
continue
"
)
                $breakpoints +=                     
                    Set-PSBreakpoint -ErrorAction SilentlyContinue -Script $file -Line $i -action $sb
                
            }
        }
        if (-not "$Command") {
            # This section must remain in this area, even though the code is duplicated in end
            # If it is not, then the arguments will not be correctly passed down to the script files
            # you are instrumenting
            if ($DoNotDotSource) {            
                if ($psCmdlet.ParameterSetName -eq "ArgumentList") {
                    $error.Clear()                    
                    $output = & $realFile @args
                    $errors = @($error)
                } elseif ($psCmdlet.ParameterSetName -eq "ParameterDictionary") {
                    $error.Clear()
                    $output = & $realFile @parameter
                    $errors = @($error)
                } else {
                    $error.Clear()
                    $output = & $realFile
                    $errors = @($error)
                }
            } else {
                if ($psCmdlet.ParameterSetName -eq "ArgumentList") {
                    $error.Clear()
                    $output = . $realFile @args
                    $errors = @($error)
                } elseif ($psCmdlet.ParameterSetName -eq "ParameterDictionary") {
                    $error.Clear()
                    $output = . $realFile @parameter
                    $errors = @($error)
                } else {
                    $error.Clear()
                    $output = . $realFile
                    $errors = @($error)
                }            
            }
            $linesHit = @{}

            Get-PSBreakpoint -Script $realFile |
                Where-Object { $_.HitCount -and $_.Line} |
                ForEach-Object {
                    $LinesHit[$_.Line] = '*' 
                }

            $PercentCovered = $linesHit.Count * 100/ $scriptContent.Count
            

            $scriptCoverageObject = 
                New-Object PSObject -Property @{
                    Output = $Output
                    Errors = $Errors
                    File = "$RealFile"
                    PercentCovered = $PercentCovered
                    Coverage = for ($i = 1; $i -le $scriptContent.Count; $i++) {             
                        "{0,2} : {1,1} : {2}" -f $i,$linesHit[$i],$scriptContent[$i -1]
                    }
                }        
            $scriptCoverageObject.pstypenames.clear()
            $scriptCoverageObject.pstypenames.add('ScriptCoverage')
            $scriptCoverageObject
        } else {
            
        }
    
    }
    end {    
        if ("$command") {
            # This section must remain in this area, even though the code is duplicated in end
            # If it is not, then the arguments will not be correctly passed down to the script files
            # you are instrumenting
            
            if (-not $DoNotDotSource) {
                foreach ($realFile in $ScriptContents.Keys) {
                    . $realFile
                }
            }
            if ($psCmdlet.ParameterSetName -eq "ArgumentList") {
                $error.Clear()                    
                $output = & $Command @args
                $errors = @($error)
            } elseif ($psCmdlet.ParameterSetName -eq "ParameterDictionary") {
                $error.Clear()
                $output = & $Command @parameter
                $errors = @($error)
            } else {
                $error.Clear()
                $output = & $Command
                $errors = @($error)
            }
            
            foreach ($realFile in $ScriptContents.Keys) { 
                $linesHit = @{}

                Get-PSBreakpoint -Script $realFile |
                    Where-Object { $_.HitCount -and $_.Line} |
                    ForEach-Object {
                        $LinesHit[$_.Line] = '*' 
                    }
                    
                New-Object PSObject -Property @{
                    Output = $Output
                    Errors = $Errors
                    File = "$realFile"
                    Coverage = for ($i = 1; $i -le $scriptContents["$realFile"].Count; $i++) {
                        "{0,4} : {1,1} : {2}" -f $i,$linesHit[$i],$scriptContents["$realFile"][$i -1]
                    }
                }
            }
        } else {
        
            
        }
        
        
        $WarningPreference = "Continue"
        $breakpoints |
             Remove-PSBreakpoint -ErrorAction SilentlyContinue
    
    }
}