Paul Selles
Computers and cats
Tag Archives: Powershell
Threading: Task Parallelism and Task Waiting with EventWaitHandle, ManualResetEvent, and AutoResetEvent
December 20, 2013
Posted by on Introduction to the Parallel Task Library
Since the introduction of Parallelism with the .NET Framework 4, we’ve had access to new run time libraries that give .NET developers control over multithreading in their projects [1]. Working with with Tasks allows the developer concentrate on performing a specific tasks (for the lack of a better word) rather than concentrating on low level worker threads. For .NET developers, this means working with the Parallel Task Library (PTL) where MSDN identifies a task as being
an asynchronous operation. In some ways, a task resembles a thread or ThreadPool work item, but at a higher level of abstraction [2].
Traditional Task Waiting
Here is a really simple Tasking example where we are going to want to print a list in a certain order to the Console [3]. We want the console output to have a certain order, established by a for loop, and a certain hierarchy (modelled after xml). Here is an example of a function using a asynchronous task with no wait handler:
static void NoWaitHandlerExample(int events=1) { // Open Events Console.WriteLine("Start Events"); for (int i = 0; i { // Open Close StartEvent Id=eventId+1 Console.WriteLine("\t\tStart Task: " + (eventId + 1).ToString()); Thread.Sleep(_random.Next(500)); Console.WriteLine("\t\tEnd Task: " + (eventId + 1).ToString()); }); // Close Event Id=eventId+1 Console.WriteLine("\tEnd Event: " + (eventId + 1).ToString()); } // Close Events Console.WriteLine("End Events"); }
If we are to call the function with 5 events, here is a sample of the output:
Start Events
Start Event: 1
End Event: 1
Start Event: 2
End Event: 2
Start Event: 3
End Event: 3
Start Event: 4
End Event: 4
Start Event: 5
Start Task: 1
End Event: 5
End Events
Start Task: 3
Start Task: 4
Start Task: 2
Start Task: 5
End Task: 4
End Task: 1
End Task: 2
End Task: 3
End Task: 5
As you can see neither the order nor the hierarchy are preserved and most errors are caused by tasks starting and ending prior to the main thread (with the exception of the first task which started before the the main thread finished). To fix this we can implement a task waiting solution as outlined in the TPL [2].
static void TaskWaitExample(int events=1) { // Open Events Console.WriteLine("Start Events"); for (int i = 0; i { // Open Close StartEvent Id=eventId+1 Console.WriteLine("\t\tStart Task: " + (eventId + 1).ToString()); Thread.Sleep(_random.Next(500)); Console.WriteLine("\t\tEnd Task: " + (eventId + 1).ToString()); }); // *** Wait for task to complete *** task.Wait(); // Close Event Id=eventId+1 Console.WriteLine("\tEnd Event: " + (eventId + 1).ToString()); } // Close Events Console.WriteLine("End Events");
The only difference is highlighted with *** in the comments which from this point on will represent task waiting events. In the code above, we wait for each task to complete before continuing. As for the results:
Start Events
Start Event: 1
Start Task: 1
End Task: 1
End Event: 1
Start Event: 2
Start Task: 2
End Task: 2
End Event: 2
Start Event: 3
Start Task: 3
End Task: 3
End Event: 3
Start Event: 4
Start Task: 4
End Task: 4
End Event: 4
Start Event: 5
Start Task: 5
End Task: 5
End Event: 5
End Events
The output preserve both order and hierarchy.
Task Waiting Through Event Wait Handling using EventWaitHandle, ManualResetEvent, and AutoResetEvent Classes
A very powerful but sometimes overlooked tool for task wait handling is to use the System.Threading Namespace,Threading Objects EventWaitHandle, ManualResetEvent, and AutoResetEvent [4],[5],[6],[7],[8]. The principle behind these classes is the same, the event object has two basic states signalled or not signalled, for convenience sake I will refer to them as ON or OFF. An OFF state will block the current thread wherever at the WaitOne method. The thread will continue to be blocked until the object calls the Set method, which sets the state to ON. We can then reset the signal back to OFF. The difference between the two EventResetMode (ManualReset and AutoReset) is how the signal is reset [9]. For the AuotReset, the signal is reset to OFF once the thread becomes unblock, this is trigger by calling the Set method and the unblocking WaitOne, if there are not waiting threads, the signal will remain ON until Reset is called. For the ManualReset, the signal remains OFF until the Reset method is called. The MSDN entry of the EventWaitHandle implementation of methods Set, WaitOne, and Reset should provides more details on thread signalling [6],[10],[11],[12]. WaitOne methods come in different flavours that allow for more functionality such as timeouts.
We can rewrite our program to use the ManuaResetEvent Class:
static void ManualResetEventExample(int events=1) { // *** New reset event unsignaled by default ManualResetEvent manualResetEvent = new ManualResetEvent(false); // Open Events Console.WriteLine("Start Events"); for (int i = 0; i { // Open Close StartEvent Id=eventId+1 Console.WriteLine("\t\tStart Task: " + (eventId + 1).ToString()); Thread.Sleep(_random.Next(500)); Console.WriteLine("\t\tEnd Task: " + (eventId + 1).ToString()); // *** Set: Sets ManualResetEvent to signaled *** manualResetEvent.Set(); }); // *** WaitOne: Block thread until ManualResetEvent is signalled *** manualResetEvent.WaitOne(); // Close Event Id=eventId+1 Console.WriteLine("\tEnd Event: " + (eventId + 1).ToString()); // *** Reset: Set state to not signalled *** manualResetEvent.Reset(); } // Close Events Console.WriteLine("End Events"); }
and the AutoResetEvent Class:
static void AutoResetEventExample(int events=1) { // *** New reset event unsignaled by default AutoResetEvent autoResetEvent = new AutoResetEvent(false); // Open Events Console.WriteLine("Start Events"); for (int i = 0; i { // Open Close StartEvent Id=eventId+1 Console.WriteLine("\t\tStart Task: " + (eventId + 1).ToString()); Thread.Sleep(_random.Next(500)); Console.WriteLine("\t\tEnd Task: " + (eventId + 1).ToString()); // *** Set: Sets AutoResetEvent to signalled *** autoResetEvent.Set(); }); // *** WaitOne: Block thread until AutoResetEvent is signaled and clear signalled state *** autoResetEvent.WaitOne(); // Close Event Id=eventId+1 Console.WriteLine("\tEnd Event: " + (eventId + 1).ToString()); } // Close Events Console.WriteLine("End Events"); }
The EventWaitHandle becomes trivial as we state if the reset is manual or automatic in with the EventResetMode enumerator. We can reuse the code above with the following constructors.
// *** New reset event unsignaled by default and set for a manual EventResetMode EventWaitHandle manualResetEvent = new EventWaitHandle(false, EventResetMode.ManualReset); // *** New reset event unsignaled by default and set for an automatic EventResetMode EventWaitHandle autoResetEvent = new EventWaitHandle(false, EventResetMode.AutolReset);
For short wait times where performance is a concerned and are not planning on using all the features that come with ManualResetEvent Class, you can use ManualResetEventSlim [13]. ManualResetEventSlim uses busy spinning while waiting for an event to become signalled. After a certain time limit ManualResetEventSlim will revert to work like ManualResetEvent class.
Server Example
Now that we are more comfortable with using these wait events we can start working on a real world example. For this example we will be creating a simple TCP server that will accept a connection, read a string, print the string it to the Console, and reverse the string, then finally return it to the caller. The class is an extension of an example provided by the MSDN Library under the NetworkStream.BeginRead Method [14].
Now without further ado, here is our simple server example making full use of the ManualResetEvent class:
public class ManualResetEventTcpServer { // *** Listener wait handler, initial state is Initial State is notsignales (false) private static ManualResetEvent ManualResetEvent = new ManualResetEvent(false); // Buffer length private int _bufferLength = 2048; // Will be access by multiple threads private bool _running; // Start listener loop public void Start() { _running = true; TcpListener listener = new TcpListener(IPAddress.Any, 3000); listener.Start(); try { while (_running) { // *** Reset to unsignalled ManualResetEvent.Reset(); listener.BeginAcceptTcpClient( ListenerCallback, listener); // *** Block until signalled ManualResetEvent.WaitOne(); } } catch {} } // Stop listener loop private void Stop() { _running = false; } // Async BeginAcceptTcpClient listener callback private void ListenerCallback(IAsyncResult ar) { try { // *** Signal ManualResetEvent.Set(); // Get listener and end async AcceptTcpClient TcpListener listener = ar.AsyncState as TcpListener; TcpClient client = listener.EndAcceptTcpClient(ar); using (NetworkStream stream = client.GetStream()) { // Read message string message = ReadMessage(stream); // Write to console Console.WriteLine(message); // Reverse and return message WriteMessage(stream, new string(message.ToArray().Reverse().ToArray())); } } catch {} } // Read message private string ReadMessage(NetworkStream networkStream) { // Incoming buffer and message string byte[] buffer = new byte[_bufferLength]; string message = string.Empty; // Loop through the entire stream in case the message is bigger than our buffer while (networkStream.DataAvailable) { int bytesRead = networkStream.Read(buffer, 0, _bufferLength); message += Encoding.Default.GetString(buffer, 0, bytesRead); } return message; } // Return message private void WriteMessage(NetworkStream networkStream, string message) { // Incoming buffer and message string byte[] buffer = Encoding.UTF8.GetBytes(message); networkStream.Write(buffer, 0, buffer.Length); } }
This class can be attached to a Console Application where Start and Stop will start and stop the listener. The server is currently configured to listen on port 3000, find a port that works with you.
Looking though the class above your attention should be focused on both the Start function and the ListenerCallbackFunction, as they are the only two function using our wait handler. Within the listener we are looping over the TcpListener Class asynchronous method BeginAcceptTcpClient [15],[14]. We want to be able to handle multiple Tcp clients but we don’t want to be spinning in the loop, so before we begin to accept a client we will Reset the wait handler to an OFF state. The asynchronous method BeginAcceptTcpClient is then called and the main thread is blocked at WaitOne. Once our listener picks up a client the callback function will be called and the wait handler will be set to the ON state, removing the block at WaitOne, and the main thread can continue through the while loop.
Of course this is no fun without a client so to mix things up here is our Powershell client:
#Simple Tcp Client Set-StrictMode -Version Latest ## Message as argurment $Message = "$Args" ## Create message buffer $BufferOut = [System.Text.Encoding]::UTF8.GetBytes($Message); ## Address and port $Address = "PAULS-W530-W8.iQmetrixHO.local" $DnsAddress = [System.Net.Dns]::GetHostAddresses($Address) $Port = 3000 ## Connect Tcp Client $Client = New-Object System.Net.Sockets.TcpClient $Client.Connect($DnsAddress,$Port) ## Get stream and write message buffer $Stream = $Client.GetStream() $Stream.Write($BufferOut, 0, $BufferOut.Length) $Stream.Flush() ## Setup input buffer $BufferInLength = 1024 $BufferIn = New-Object byte[] $BufferInLength $BufferIndex = 0 ## Set a read date timeout $TimeOut = [TimeSpan]"00:00:15" $StartTime = Get-Date do { if ($Stream.CanRead -and $Stream.DataAvailable) { break } } while (((Get-Date) - $StartTime) -le $TimeOut) if (!($Stream.CanRead -and $Stream.DataAvailable)) { Write-Error "Timeout" } ## Read data while ($Stream.DataAvailable) { $BytesRead = $Stream.Read($BufferIn, 0, $BufferInLength) $Message = [System.Text.Encoding]::UTF8.GetString($BufferIn, 0, $BytesRead) Write-Host $Message -NoNewline } Write-Host ""
Paul
References
[1] Parallel Programming in the .NET Framework. MSDN Library
[2] Task Parallelism (Task Parallel Library). MSDN Library
[3] Console Class. MSDN Library
[4] System.Threading Namespace. MSDN Library
[5] EventWaitHandle, AutoResetEvent, CountdownEvent, ManualResetEvent. MSDN Library
[6] EventWaitHandle Class. MSDN Library
[7] ManualResetEvent Class. MSDN Library
[8] AutoResetEvent Class. MSDN Library
[9] EventResetMode Enumeration. MSDN Library
[10] EventWaitHandle.Set. MSDN Library
[11] EventWaitHandle.WaitOne. MSDN Library
[12] EventWaitHandle.Reset. MSDN Library
[13] ManualResetEventSlim Class. MSDN Library
[14] NetworkStream.BeginRead Method. MSDN Library
[15] TcpListener Class. MSDN Library
Invoke-MsTest PowerShell Module
August 1, 2013
Posted by on I have recently created Invoke-MsTest a great PowerShell module for Visual Studio unit testing. I have made he project available on CodePlex (https://invokemstest.codeplex.com/). The module runs MsTest.exe behind the scenes and will return the results as a PSObject or a Boolean. This is a great tool to incorporate into your CI flow.
This project was inspired by the Invoke-MsBuild by my colleague Daniel Schroeder.
The project is available at on CodePlex but here is a sample script, it is strongly recommended that you download it from the CodePlex as the code in this blog is not guaranteed to up up to date.
function Invoke-MsTest { <# .SYNOPSIS Run tests for a given Visual Studio solution or project using MsTest .DESCRIPTION Parses each Visual Studio solution or project and extracts test containers. Test each file using MsTest.exe to create a single .trx file. Optionally user may choose to only run specific test names. Optionally user may view or keep the MsTest.exe command window open to view results. .PARAMETER Path The path of the Visual Studio solution or project to build (e.g. a .sln or .csproj file). .PARAMETER Tests A string array with the test names to be ran. By Default the script will run all test associated with the project or solution. .PARAMETER Unique Switch that will only run tests who’s name exactly matches those listed in Tests. .PARAMETER ResultsDirectoryPath The directory path to create the test result file (.trx) By Defaults the test file will be written to the users temp directory (e.g. C:\Users\[User Name]\AppData\Local\Temp). .PARAMETER Verbose Switch will print test information to the PowerShell console. .PARAMETER ShowTestWindow Switch will show the cmd window running MsTest.exe .PARAMETER PromptToCloseTestWindow Switch that gives users the option of keeping the cmd window running MsTest.exe open once all the tests are complete. .PARAMETER NoResults Switch will block the results object from returning (Cannot be used with IsAllPassed). .PARAMETER IsAllPassed Switch will return a boolean true if all test pass else a false (Cannot be used with NoResults). .PARAMETER MsTestParameters Additionally MsTest Parameters that the user may wish to include (http://msdn.microsoft.com/en-US/library/ms182489(v=vs.80).aspx) Note: This script uses /testcontainer to run tests and will not support /testmetadata parameters. .OUTPUTS If NoResults is not selected then the script will pass an object with Outcomes, Test names (or project/solution names), Storage (container dll), and Comments. If all tests passed, there will be no output. .EXAMPLE Import-Module "C:\Path To Module\Inoke-MsTest.psm1" $TestResults = Invoke-MsTest -Path "C:\Some Folder\MySolution.sln" Foreach ($TestResult in $TestResults) { if ($TestResult.Outcome -eq "Failed") { Write-Host $TestResult.Name -ForgroundColor Red } } The default test will run all test under a given Visual Studio solution or project and return a list of failed tests, project, or solutions. The above example will print the name of all test failures to the console. .EXAMPLE $TestFailures = Invoke-MsTest -Path "C:\Some Folder\MySolution.sln" -Tests MyTest1,MyTest2,MyTest3 Listing tests will only run tests that match the test name given. Note: MyTest1 will match with MyTest10, MyTest11, MyTest100 to use only exact matching test names use the 'Unique' switch .EXAMPLE Invoke-MsTest -Path "C:\Some Folder\MyProject.csproj" -ResultsDirectoryPath "C:\Some Folder" Runs test and drops the test results file in the "C:\Some Folder" directory .EXAMPLE Invoke-MsTest -Path "C:\Some Folder\MyProject.csproj" -Verbose -NoResults Runs test and print test projess/results to the console. This will not pass back the results object. .EXAMPLE Invoke-MsTest -Path "C:\Some Folder\MySolution.sln" -PromptToCloseTestWindow Runs test and keep the cmd MsTest.exe window open awaiting user input. .LINK Project home: https://invokemstest.codeplex.com/ .NOTES Name: Invoke-MsTest Author: Paul Selles [https://paulselles.wordpress.com/] Version: 1.0 Inspired by my colleague, Daniel Schroeder's Invoke-MsBuild [https://invokemsbuild.codeplex.com] #> [CmdletBinding(DefaultParameterSetName="Default")] param ( # List the root path for your projects or solutions # this will find all test containers within and run them [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)] [ValidateScript({(Test-Path $_ -PathType Leaf) -and (((Dir $_).Extension -eq ".sln") -or ((Dir $_).Extension -match '.*..*proj'))})] [String]$Path, # List of names of the specific test to run, if nothing is # specified then run all tests in the test containers [parameter(Mandatory=$false)] [ValidateNotNullOrEmpty()] [String[]]$Tests=$null, # Will only run tests that exactly match the test name 'Tests' parameter [Parameter(Mandatory=$false)] [Alias("U")] [Switch]$Unique, # Option to select the location of the result files [parameter(Mandatory=$false)] [ValidateScript({Test-Path $_ -PathType Container})] [Alias("Results")] [Alias("R")] [string]$ResultsDirectoryPath=$env:Temp, # Show the tests running in a command window [parameter(Mandatory=$false)] [Alias("Show")] [Alias("S")] [switch] $ShowTestWindow, [parameter(Mandatory=$false)] [Alias("Prompt")] [switch] $PromptToCloseTestWindow, # Will not return test results [parameter(Mandatory=$false,ParameterSetName="NoResults")] [Switch]$NoResults, # Will return a boolean result, $true if all tests pass otherwise $false [parameter(Mandatory=$false,ParameterSetName="IsAllPassed")] [Alias("BoolResults")] [Switch]$IsAllPassed, # Desired test run parameters [parameter(Mandatory=$false)] [ValidateNotNullOrEmpty()] [Alias("Params")] [Alias("P")] [string]$MsTestParameters ) BEGIN { } END { } PROCESS { Set-StrictMode -Version Latest # Array for our test results $TestResults = @() # Sets the verbose Parameter if ($PSBoundParameters['Verbose']) { $Verbose = $true } else { $Verbose = $false } # Get the path to the MsTest executable # Exit if MsTest path is invalid $MsTest = Get-MsTest if (-not (Test-Path $MsTest)) { return } # Array for out container and projects $TestContainers = @() $Projects = @() # Get a list of project under the supplied solution file if (((Dir $Path).Extension -eq ".sln")) { Get-Content -Path $Path | % { if ($_ -match '\s*Project.+=\s*.*,\s*\"\s*(.*proj)\s*\"\s*,\s*') { $Projects += Join-Path -Path (Dir $Path).DirectoryName $matches[1] } } } # If a project file we don't need to so much elseif ((Dir $Path).Extension -match '.*..*proj') { $Projects += $Path } # Loop through all project files and Foreach ($Project in $Projects) { $ProjectXml = [Xml](Get-Content -Path $Project) $ns = New-Object Xml.XmlNamespaceManager $ProjectXml.NameTable $ns.AddNamespace('dns','http://schemas.microsoft.com/developer/msbuild/2003') # Test is the project includes the unit test class if ([Bool]($ProjectXml.SelectNodes('//dns:Project/dns:ItemGroup/dns:Reference', $ns) | Where {$_.Include -match 'Microsoft.VisualStudio.QualityTools.UnitTestFramework'})) { # Collect all possible configurations $Configurations = $ProjectXml.SelectNodes('//dns:Project/dns:PropertyGroup[@Condition]', $ns).Condition # Collect all PropertyGroup Nodes referencing IntermediateOutputPath and BaseIntermediateOutputPath elements $IntermediateOutputPathNodes = $ProjectXml.SelectNodes('//dns:Project/dns:PropertyGroup[dns:IntermediateOutputPath]', $ns) $BaseIntermediateOutputPathNodes = $ProjectXml.SelectNodes('//dns:Project/dns:PropertyGroup[dns:BaseIntermediateOutputPath]', $ns) # Container for the IntermediateOutputPath relative to the proj directory $IntermediateOutputPaths = @() # IntermediateOutputPath take precedence over BaseIntermediateOutputPath # Remove all BaseIntermediateOutputPaths that matches with an IntermediateOutputPath Configuration # Register the IntermediateOutputPath # Remove Configuration from Configurations list Foreach ($IntermediateOutputPathNode in $IntermediateOutputPathNodes) { $Configuration = $IntermediateOutputPathNode.Condition $BaseIntermediateOutputPathNodes = $BaseIntermediateOutputPathNodes | ? {$_.GetAttribute('Condition') -ne $Configuration} $IntermediateOutputPaths += $IntermediateOutputPathNode.IntermediateOutputPath $Configurations = $Configurations | ? {$_ -ne $Configuration} } # Register the remaining BaseIntermediateOutputPath Foreach ($BaseIntermediateOutputPathNode in $BaseIntermediateOutputPathNodes) { $Configuration = $IntermediateOutputPathNode.Condition $IntermediateOutputPaths += $BaseIntermediateOutputPathNode.BaseIntermediateOutputPath $Configurations = $Configurations | ? {$_ -ne $Configuration} } $IntermediateOutputPaths = $IntermediateOutputPaths | % {Join-Path (Dir $Project) $_} | Where {Test-Path "$_\*FileListAbsolute.txt"} if ($Configurations) { $IntermediateOutputPaths += (Get-ChildItem -Path (Join-Path (Dir $Project).DirectoryName obj) -Directory).FullName | Where {Test-Path "$_\*FileListAbsolute.txt"} } # If all IntermediateOutputPaths are empty alert user if (-not $IntermediateOutputPaths) { if ($Verbose) { Write-Host "Error :"(Dir $Project).Name": Could not find IntermediateOutputPath contents. Please rebuild the solution/project and try again." -ForegroundColor Red } $TestResults += New-Object PSObject -Property @{Outcome="projerror";Name=(Dir $Project).Name;Storage="";Comments="Could not find IntermediateOutputPath contents. Please rebuild the solution/project and try again."} break } # Get the latest FileListAbsolute this is cumbersome because if it is an array of one element the string will return the first char $FileListAbsolute = (($IntermediateOutputPaths | % {(Get-ChildItem -Path $_ -File *.FileListAbsolute.txt)} | Sort -Descending {(Dir $_.FullName).LastWriteTime})[0]).FullName # Find all test containers from the DLL files submitted $ProjectTestContainers = @() # Get a list of all the DLL in the project and find the test containers Get-Content -Path $FileListAbsolute | % { if ($_ -match '.*\\bin\\.*.dll' -and $_ -notmatch '.*nunit.core.dll') { if (-not (Test-Path $_)) { if ($Verbose) { Write-Host "Error :"(Dir $Project).Name": Could not find library $_. Please rebuild the solution/project and try again." -ForegroundColor Red } $TestResults += New-Object PSObject -Property @{Outcome="projerror";Name=(Dir $Project).Name;Storage="$_";Comments="Could not find library $_. Please rebuild the solution/project and try again."} } elseif ([IO.File]::ReadAllText($_) -match 'TestClass') { $ProjectTestContainers += $_ } }} # If there are not test containers, let the user know if (-not $ProjectTestContainers) { if ($Verbose) { Write-Host "Warning :"(Dir $Project).Name": Could not find test containers." -ForegroundColor Yellow } $TestResults += New-Object PSObject -Property @{Outcome="projwarning";Name=(Dir $Project).Name;Storage="";Comments="Could not find test containers."} } else { if ($Verbose) { Write-Host "Added :"(Dir $Project).Name": Found" $ProjectTestContainers.Count "test container(s)" -ForegroundColor Green } } $TestContainers += $ProjectTestContainers } } if (-not $TestContainers) { # By returning the unique TestResults we prevent spamming the same problem (ie. Multiple assemblies cannot be found with only report once) if ($Verbose) { Write-Host "Error :"(Dir $Path).Name": Could not find any test containers." -ForegroundColor Red } $TestResults += New-Object PSObject -Property @{Outcome="testerror";Name=(Dir $Path).Name;Storage="";Comments="Could not find any test containers."} if ($IsAllPassed) {(!$TestResults)} elseif (!$NoResults) {$TestResults} return } # Add Parameters to test arguments $TestArguments = "${MsTestParameters}" # Add test names the arguments and unique, if chosen if ($Tests) { $TestArguments = "${TestArguments} /test:" foreach ($Test in $Tests) {$TestArguments = "${TestArguments}${Test},"} $TestArguments = $($TestArguments.Substring(0,$TestArguments.Length-1)) if ($Unique) { $TestArguments = "${TestArguments} /unique" } } foreach ($TestContainer in $TestContainers) { if ($Verbose) { Write-Host "Loading Test: " (Dir $TestContainer).BaseName -ForegroundColor Cyan } $TestArguments = "${TestArguments} /testcontainer:${TestContainer}" } # Select our window style $WindowStyle = if ($ShowTestWindow -or $PromptToCloseTestWindow) { "Normal" } else { "Hidden" } # Time Stamp will be added to our test files $TimeStamp = (Get-Date -Format "yyyy-MM-dd hh_mm_ss") # Add the test results file to the directory $TestResultsFile = "InvokeMsTestResults ${TimeStamp}.trx" $TestResultsFile = Join-Path $ResultsDirectoryPath $TestResultsFile $TestArguments = "${TestArguments} /resultsfile:`"${TestResultsFile}`"" # Construct the test test cmd argument $PauseForInput = if ($PromptToCloseTestWindow) { "Pause & " } else { "" } $TestCmdArgument = "/k "" ""${MsTest}"" ${TestArguments} & ${PauseForInput} Exit"" " # Starts the MsTests Start-Process cmd.exe -ArgumentList $TestCmdArgument -WindowStyle $windowStyle -Wait if (Test-Path $TestResultsFile) { $TestResultsFileXml = [Xml](Get-Content -Path $TestResultsFile) $ns = New-Object Xml.XmlNamespaceManager $TestResultsFileXml.NameTable $ns.AddNamespace('dns','http://microsoft.com/schemas/VisualStudio/TeamTest/2010') $UnitTests = $TestResultsFileXml.SelectNodes('//dns:TestRun/dns:TestDefinitions/dns:UnitTest',$ns) $UnitTestResults = $TestResultsFileXml.SelectNodes('//dns:TestRun/dns:Results/dns:UnitTestResult',$ns) if ($Verbose) { Write-Host "--------------------------------------------------------------------------------`r`nResults`r`n--------------------------------------------------------------------------------" } Foreach ($UnitTestResult in $UnitTestResults) { $Storage = ($UnitTests | Where {$_.id -eq $UnitTestResult.testId}).storage $TestResults += New-Object PSObject -Property @{Outcome=$UnitTestResult.outcome;Name=$UnitTestResult.testName;Storage=$Storage;Comments=""} if ($Verbose) { if ($UnitTestResult.outcome -eq "passed") { $Color = 'Green' } elseif ($UnitTestResult.outcome -eq "warning") { $Color = 'Yellow' } elseif ($UnitTestResult.outcome -eq "failed") { $Color = 'Red' } else { $Color = 'Gray' } Write-Host $UnitTestResult.outcome"`t"$UnitTestResult.testName"`t"$Storage -ForegroundColor $Color } } } else { if ($Verbose) { Write-Host "Error: Could not find or open test results file." } $TestResults += New-Object PSObject -Property @{Outcome="testerror";Name=(Dir $Path).Name;Storage="";Comments="Could not find or open test results file."} } if ($IsAllPassed) {(($TestResults.outcome | Where {$_ -eq "Passed"}).Count -eq $TestResults.Count)} elseif (!$NoResults) {$TestResults} } } ################################################################################ function Get-MsTest { <# .SYNOPSIS Gets path for latest version of MsTest.exe .DESCRIPTION Gets path for latest version of MsTest.exe #> $MsTest = "$(Get-VsCommonTools)..\IDE\MsTest.exe" if (Test-Path $MsTest) {$MsTest} else {Write-Error "Unable to find MsTest.exe"} } ################################################################################ function Get-VsCommonTools { <# .SYNOPSIS Gets path to the current VS common tools .DESCRIPTION Gets path to the current VS common tools. Current list supports VS11 and VS10, you may need to add to this list to satisfy your needs. #> $VsCommonToolsPaths = @(@($env:VS110COMNTOOLS,$env:VS100COMNTOOLS) | Where-Object {$_ -ne $null}) if ($VsCommonToolsPaths.Count -ne 0) {$VsCommonToolsPaths[0]} else {Write-Error "Unable to find Visual Studio Common Tool Path."} } ################################################################################ Export-ModuleMember -Function Invoke-MsTest
Take the module for a spin and let me know how well it works. Comments and suggestions are more than welcome.
Edit: Added a DefaultParameterSet.
Powershell Tip #1: Strict Mode XML Parsing Gotcha
July 25, 2013
Posted by on I love how easy it is to parse XML with Powershell, but then I started scripting in Strict Mode and got hung-up on a little problem dealing with attributes.
I will revisit a revised cats.xml to use as an example xml file:
<?xml version="1.0" encoding="utf-8"?> <Cats> <Cat Name="Wilson" Type="Tabby"> <Property Name="Fur" Value="Coarse"/> <Property Name="Color" Value="Orange" /> <Part Name="Paws"> <Property Name="Claws" Value="Very sharp" /> </Part> <Part Name="Nose"> <Property Name="Cute" Value="true" /> </Part> </Cat> <Cat Name="Winnie" Type="Short hair"> <Property Name="Fur" Value="Soft"/> <Property Name="Color" Value="Black" /> <Part Name="Paws"> <Property Name="Polydactyl" Value="true" /> <Property Name="Claws" Value="Sharp" /> </Part> <Part Name="Nose"> <Property Name="Cute" Value="true" /> </Part> </Cat> <Cat Name="Luna"> <Property Name="Fur" Value="Soft"/> <Property Name="Color" Value="Black" /> <Part Name="Paws"> <Property Name="Claws" Value="Trimmed" /> </Part> <Part Name="Nose"> <Property Name="Cute" Value="true" /> </Part> </Cat> </Cats>
Lets make a script that will retrieve the Name of a cat by their type:
# Get cat name by type Param ( [ValidateNotNullOrEmpty()] [String]$Type ) [Xml]$Cats = (Get-Content -Path C:\temp\cats.xml) ($Cats.Cats.Cat | Where {$_.Type -match $Type}).Name
The script is nice and short and does what we expect. If the Type matches, it returns the Name of the cat (otherwise, we get nothing):
Now let’s try the script in Strict Mode:
# Get cat name by type Param ( [ValidateNotNullOrEmpty()] [String]$Type ) Set-StrictMode -Version Latest [Xml]$Cats = (Get-Content -Path C:\temp\cats.xml) ($Cats.Cats.Cat | Where {$_.Type -match $Type}).Name
If we try to run the script again we run into problems:
What did we do wrong? As it turns out there are two problems. The first is that the entry for Cat Luna does not have a Type attribute (if we re-ran the tests removing Luna then the script will pass). Secondly, we are expecting Powershell to interpret what we mean by the property Type. We want Type as an attribute name, but how does Powershell know that? This is sloppy scripting, but it works until we switch into Strict Mode.
In order to move forward, we need a better idea of the objects that we are playing with. So let’s get the members of $Cats.Cats.
We can see that this is an System.Xml.XmlElement[1]. We could look the class up or we can intuitively see what method on from the list above will help us clean up our code and test for the attribute Type:
# Get cat name by type Param ( [ValidateNotNullOrEmpty()] [String]$Type ) [Xml]$Cats = (Get-Content -Path C:\temp\cats.xml) $Cat = $Cats.Cats.Cat | Where {$_.GetAttribute('Type') -match $Type} if ($Cat) { $Cat.GetAttribute('Name') }
Using GetAttribute on attribute Type will solve the first error. We solve our second error (when a not matching type is entered) by making sure that the object is not null before reading the attribute Name.
Paul
References
[1] XmlXElement Class. MSDN Library
Recent Comments