# encoding: ascii
# api: powershell
# title: WCF code coverage
# description: Script for running unit tests over WCF services to get code coverage for the whole service stack
# version: 10.0
# type: script
# author: sixeyed
# license: CC0
# function: get-apppool
# x-poshcode-id: 3141
# x-archived: 2012-02-01T10:20:01
# x-published: 2012-01-04T10:33:00
#
#
param($msBuildTarget, $configurationName, [bool]$deleteInstrumentedAssemblies) 

#-------------------------------------
# Script to compile coverage for a WCF
# solution running in IIS.
# See:
# http://geekswithblogs.net/EltonStoneman/archive/2011/10/14/end-to-end-wcf-code-coverage-with-powershell.aspx 
#-------------------------------------

#-------------------------------------
# Function to get the running app pool 
#-------------------------------------
function get-apppool{
    [regex]$pattern="-ap ""($appPoolName)"""
    gwmi win32_process -filter 'name="w3wp.exe"' | % {
        $name=$_.name
        $cmd = $pattern.Match($_.commandline).Groups[1].Value
        $procid = $_.ProcessId
        New-Object psobject | Add-Member -MemberType noteproperty -PassThru Name $name |
            Add-Member -MemberType noteproperty -PassThru AppPoolID $cmd |
            Add-Member -MemberType noteproperty -PassThru PID $procid 
    }
}

#---------------------------------------
# Function to get the id of the app pool 
#---------------------------------------
function get-apppoolpid{
	$appPoolPid = 0
	$appPools = get-apppool
	foreach ($appPool in $appPools){
		if ($appPool.AppPoolID -eq "$appPoolName"){
			$appPoolPid = $appPool.PID
		}
	}
	write-host "$solutionFriendlyName app pool PID: $appPoolPID"
	return $appPoolPid
}

#------------------------------------------------
# Starts the app pool by making a service request
#------------------------------------------------
function start-apppool{
	#ping the service to start a new app pool process:
	$uri = new-object System.Uri("$wakeUpServiceUrl")
	$client = new-object System.Net.WebClient
	$client.DownloadString($uri) | out-null
}

#-------------------
# Kills the app pool
#-------------------
function kill-apppool{
	$procId = get-apppoolpid
	if ($procId -ne 0){
		write-host "Killing app pool process"
		$proc = [System.Diagnostics.Process]::GetProcessById($procId)
		$proc.Kill()      
	}
}

#------------------------------------------------------------------------------
# Instruments an assembly for code coverage, excluding the specified namespaces
#------------------------------------------------------------------------------
function instrument2([string]$assemblyName, [string[]]$excludes){
	$assy = "$websiteBinDirectory\$assemblyName"
	$excludeLine = ""
	if ($excludes -ne $null){
        foreach ($x in $excludes){
		    write-host "Excluding: $x.*"	     
		    $excludeLine = "$excludeLine-exclude:$x.* "
	  }
	}
    $cmd = "C:\Program Files\Microsoft Visual Studio 10.0\Team Tools\Performance Tools\vsinstr.exe"
    write-host "Executing $cmd /coverage $excludeLine $assy" 
	& $cmd /coverage "$excludeLine" "$assy" 
}

#------------------------------------------------------------------------------
# Instruments an assembly for code coverage, excluding the specified namespaces
#------------------------------------------------------------------------------
function instrument([string]$assemblyName, [string[]]$excludes){
	$assy = "$websiteBinDirectory\$assemblyName"

	#ES - this doesn't work as the ":" in exlucde gets parsed out by PS:
	#$excludeParms = ""
	#$args
	#foreach($exclude in $args){
	#	$excludeParms = $excludeParms + '/exclude:' + $exclude + '.* '
	#}
	#$excludeParms
	#& 'C:\Program Files\Microsoft Visual Studio 10.0\Team Tools\Performance Tools\vsinstr.exe' /coverage $excludeParms $assy 

	if ($excludes -eq $null){
		write-host "Excluding 0 funcspecs"
		& 'C:\Program Files\Microsoft Visual Studio 10.0\Team Tools\Performance Tools\vsinstr.exe' /coverage $assy 
	}
	elseif ($excludes.Length -eq 0){
		write-host "Excluding 0 funcspecs"
		& 'C:\Program Files\Microsoft Visual Studio 10.0\Team Tools\Performance Tools\vsinstr.exe' /coverage $assy 
	}
	elseif ($excludes.Length -eq 1){
		write-host "Excluding 1 funcspecs"
		$exclude1 = '-exclude:' + $excludes[0] 
		& 'C:\Program Files\Microsoft Visual Studio 10.0\Team Tools\Performance Tools\vsinstr.exe' /coverage $exclude1 $assy 
	}
	elseif ($excludes.Length -eq 2){
		write-host "Excluding 2 funcspecs"
		$exclude1 = '-exclude:' + $excludes[0]
		$exclude2 = '-exclude:' + $excludes[1]
		& 'C:\Program Files\Microsoft Visual Studio 10.0\Team Tools\Performance Tools\vsinstr.exe' /coverage $exclude1 $exclude2  $assy 
	}
	elseif ($excludes.Length -eq 3){
		write-host "Excluding 3 funcspecs"
		$exclude1 = '-exclude:' + $excludes[0]
		$exclude2 = '-exclude:' + $excludes[1]
		$exclude3 = '-exclude:' + $excludes[2]
		& 'C:\Program Files\Microsoft Visual Studio 10.0\Team Tools\Performance Tools\vsinstr.exe' /coverage $exclude1 $exclude2 $exclude3 $assy 
	}
	elseif ($excludes.Length -eq 4){
		write-host "Excluding 4 funcspecs"
		$exclude1 = '-exclude:' + $excludes[0]
		$exclude2 = '-exclude:' + $excludes[1]
		$exclude3 = '-exclude:' + $excludes[2]
		$exclude4 = '-exclude:' + $excludes[3]
		& 'C:\Program Files\Microsoft Visual Studio 10.0\Team Tools\Performance Tools\vsinstr.exe' /coverage $exclude1 $exclude2 $exclude3 $exclude4 $assy 
	}
	elseif ($excludes.Length -eq 5){
		write-host "Excluding 5 funcspecs"
		$exclude1 = '-exclude:' + $excludes[0]
		$exclude2 = '-exclude:' + $excludes[1]
		$exclude3 = '-exclude:' + $excludes[2]
		$exclude4 = '-exclude:' + $excludes[3]
		$exclude5 = '-exclude:' + $excludes[4]		
		& 'C:\Program Files\Microsoft Visual Studio 10.0\Team Tools\Performance Tools\vsinstr.exe' /coverage $exclude1 $exclude2 $exclude3 $exclude4 $exclude5 $assy 
	}	
	elseif ($excludes.Length -eq 6){
		write-host "Excluding 6 funcspecs"
		$exclude1 = '-exclude:' + $excludes[0]
		$exclude2 = '-exclude:' + $excludes[1]
		$exclude3 = '-exclude:' + $excludes[2]
		$exclude4 = '-exclude:' + $excludes[3]
		$exclude5 = '-exclude:' + $excludes[4]		
		$exclude6 = '-exclude:' + $excludes[5]		
		& 'C:\Program Files\Microsoft Visual Studio 10.0\Team Tools\Performance Tools\vsinstr.exe' /coverage $exclude1 $exclude2 $exclude3 $exclude4 $exclude5 $exclude6 $assy 
	}
}

#-----------------------------------
# Starts instrumenting W3WP app pool
#------------------------------------
function start-instrumentation{
	#set instrumentation on:
	&  'C:\Program Files\Microsoft Visual Studio 10.0\Team Tools\Performance Tools\vsperfclrenv' /globaltraceon 

	#restart the app pool and store the ID:
	kill-apppool
	start-apppool
	$procId = get-apppoolpid

	#start instrumenting:
	& 'C:\Program Files\Microsoft Visual Studio 10.0\Team Tools\Performance Tools\vsperfcmd' /START:COVERAGE /OUTPUT:$coverageOutputPath /CS /USER:$appPoolIdentity
	& 'C:\Program Files\Microsoft Visual Studio 10.0\Team Tools\Performance Tools\vsperfcmd' /ATTACH:$procId
	& 'C:\Program Files\Microsoft Visual Studio 10.0\Team Tools\Performance Tools\vsperfcmd' /status
}

#--------------------
# Stops instrumenting
#--------------------
function stop-instrumentation{
	#stop instrumenting & reset app pool:
	& 'C:\Program Files\Microsoft Visual Studio 10.0\Team Tools\Performance Tools\vsperfcmd' /DETACH
	kill-apppool
	& 'C:\Program Files\Microsoft Visual Studio 10.0\Team Tools\Performance Tools\vsperfcmd' /SHUTDOWN 
}

#------------------------
# Exports coverage to XML
#------------------------
function export-coverage{
	#export the coverage file to XML:
	[System.Reflection.Assembly]::LoadFile("C:\Program Files\Microsoft Visual Studio 10.0\Common7\IDE\PrivateAssemblies\Microsoft.VisualStudio.Coverage.Analysis.dll")
	$coverage = [Microsoft.VisualStudio.Coverage.Analysis.CoverageInfo]::CreateFromFile("$coverageOutputPath")
	$dataSet = $coverage.BuildDataSet()
	$dataSet.WriteXml("$coverageOutputPath" + 'xml')
}

#----------------------------------------------------------------------------------
# Deletes instrumented assemblies, and reinstates original un-instrumented versions
#----------------------------------------------------------------------------------
function delete-intstrumentedassemblies{
	$dir = new-object IO.DirectoryInfo($websiteBinDirectory)
	$originalAssemblies = $dir.GetFiles("*.dll.orig")
	foreach ($originalAssembly in $originalAssemblies){
		$targetName = $originalAssembly.FullName.TrimEnd(".orig".ToCharArray())
		#overwrite the instrumented DLL with the original:
		[IO.File]::Copy($originalAssembly.FullName, $targetName, $true)
		[IO.File]::Delete($originalAssembly.FullName)
		#delete the instrumented PDB:
		$instrumentedPdbName = $originalAssembly.FullName.TrimEnd(".dll.orig".ToCharArray()) + ".instr.pdb"
		[IO.File]::Delete($instrumentedPdbName)
	}
}

#--------------
# Script begins
#--------------

#set variables:
$solutionFriendlyName = 'XYZ'
$wakeUpServiceUrl = 'http://localhost/x.y.z/Service.svc'
$appPoolName = 'ap_XYZ'
$appPoolIdentity = 'domain\svc_user'
$websiteBinDirectory = 'c:\websites\xyz\x.y.z.Services\bin'
$coverageOutputPath = "Test.coverage"

#instrument assemblies:
# - instrument assembly so ALL namespaces are included in coverage
instrument   "x.y.z.Services.dll"  
# - instrument assembly so anything from the Ignore1 and Ignore2 namespaces are excluded from coverage
instrument   "x.y.z.Core.dll"   'x.y.z.Core.Ignore1.*' , 'x.y.z.Core.Ignore2.*'  

write-host "Before starting instrumentation, last exit code: $LASTEXITCODE"

#instrument W3WP, run tests & export results:
start-instrumentation

write-host "Before running tests, last exit code: $LASTEXITCODE"
& 'C:\Windows\Microsoft.NET\Framework\v4.0.30319\msbuild.exe' Build.proj /t:$msBuildTarget /p:ConfigurationName=$configurationName

$realExitCode = $LASTEXITCODE

write-host "Before stopping instrumentation, last exit code: $LASTEXITCODE, real exit code: $realExitCode"
stop-instrumentation
export-coverage
export-coverage

if ($deleteInstrumentedAssemblies -eq $true){
	delete-intstrumentedassemblies
}

write-host "Before existing, last exit code: $LASTEXITCODE, real exit code: $realExitCode"
exit $realExitCode

#------------
# Script ends
#------------
