21 Commits

Author SHA1 Message Date
Kevin Pilch-Bisson fd72605d9d Update to released xunit 2.1 2015-10-12 12:13:26 -07:00
Kevin Pilch-Bisson cb39c7af29 Merge pull request #27 from Pilchie/ui-tweaks
A few UI tweaks
2015-10-12 09:23:59 -07:00
Kevin Pilch-Bisson 6aa5a6c0cd Merge pull request #23 from Pilchie/oneproc
Couple of fixes
2015-10-12 09:21:31 -07:00
Jared Parsons 90643fbf8f Worker can process tasks in parallel 2015-10-12 09:20:44 -07:00
Jared Parsons 0d96b29a7a Allow opening of multiple assemblies at a time 2015-10-12 09:20:42 -07:00
Jared Parsons d861f4a6ee Made connection async
The connection to the worker process was sync on the UI thread which
could lead to hangs.  Made it an async operation instead.

This did require me to upgrade to the 4.6 framework to use the
NamedPipeClientStream::ConnectAsync method.  If that is a blocker I can
downgrade back to 4.5.2 and use Thread tricks to get a similar effect.
2015-10-12 09:20:41 -07:00
Jared Parsons 3f894e0e7c Correct debugging workflow
There is now a single worker process model for all requests.  Debugging
workflow is to attach to this process and debug there.

closes #20
2015-10-12 09:19:44 -07:00
Kevin Pilch-Bisson 4371a4e837 Merge pull request #26 from Pilchie/issue-25
Fix issue where running a specific test was treated as running all of them
2015-10-12 09:11:57 -07:00
Dustin Campbell 65adde358c A few UI tweaks
Notably, the images for passed, failed and skipped have been replaced with images from the Visual Studio Image Library.
2015-10-12 08:58:11 -07:00
Dustin Campbell ae5e46d772 Fix issue where running a specific test was treated as running all of them 2015-10-12 06:28:42 -07:00
Jared Parsons 9934a70f52 Merge pull request #22 from Pilchie/reload
Implement Assembly reload and remove support
2015-08-29 10:10:55 -07:00
Jared Parsons 13afb6eea5 Fixed the race condition in the run and discover tasks 2015-08-27 17:13:55 -07:00
Jared Parsons e3a17c5308 Respond to PR feedback
Handled everything but the race condition.  Going to fix that in a
separate commit.
2015-08-27 16:41:14 -07:00
Jared Parsons 89be98bebc Naming consistency 2015-08-23 23:09:23 -07:00
Jared Parsons 9e5ac70234 Fix reload bugs
Fixes a couple of bugs in the Reload / Remove logic.
2015-08-23 23:08:00 -07:00
Jared Parsons b35da545d6 Added remove all assemblies menu item 2015-08-23 23:01:54 -07:00
Jared Parsons 3a7d01b87e Add Assembly reload support
Can now reload individual or all currently loaded assemblies.

closes #2
2015-08-23 21:25:21 -07:00
Jared Parsons 0e24227de0 Ability to clear trait selection
This should fulfill issue #7
2015-08-23 21:00:28 -07:00
Jared Parsons 65f3fc970e Test run can be filtered by traits 2015-08-23 20:50:59 -07:00
Jared Parsons 06f1c8c703 Traits displaying in the UI 2015-08-23 19:20:34 -07:00
Jared Parsons 29aa127230 Merge pull request #19 from Pilchie/remote
Move discover and execution to a remote process
2015-08-22 12:02:28 -07:00
39 changed files with 829 additions and 540 deletions
+8 -11
View File
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\packages\xunit.core.2.1.0-beta4-build3109\build\portable-net45+netcore45+wp8+wpa81\xunit.core.props" Condition="Exists('..\packages\xunit.core.2.1.0-beta4-build3109\build\portable-net45+netcore45+wp8+wpa81\xunit.core.props')" />
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
@@ -45,12 +44,16 @@
<HintPath>..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="xunit.assert, Version=2.1.0.3109, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL">
<HintPath>..\packages\xunit.assert.2.1.0-beta4-build3109\lib\dotnet\xunit.assert.dll</HintPath>
<Reference Include="xunit.assert, Version=2.1.0.3179, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL">
<HintPath>..\packages\xunit.assert.2.1.0\lib\dotnet\xunit.assert.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="xunit.core, Version=2.1.0.3109, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL">
<HintPath>..\packages\xunit.extensibility.core.2.1.0-beta4-build3109\lib\dotnet\xunit.core.dll</HintPath>
<Reference Include="xunit.core, Version=2.1.0.3179, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL">
<HintPath>..\packages\xunit.extensibility.core.2.1.0\lib\dotnet\xunit.core.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="xunit.execution.desktop, Version=2.1.0.3179, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL">
<HintPath>..\packages\xunit.extensibility.execution.2.1.0\lib\net45\xunit.execution.desktop.dll</HintPath>
<Private>True</Private>
</Reference>
</ItemGroup>
@@ -62,12 +65,6 @@
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\packages\xunit.core.2.1.0-beta4-build3109\build\portable-net45+netcore45+wp8+wpa81\xunit.core.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\xunit.core.2.1.0-beta4-build3109\build\portable-net45+netcore45+wp8+wpa81\xunit.core.props'))" />
</Target>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
+5 -4
View File
@@ -1,8 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="xunit" version="2.1.0-beta4-build3109" targetFramework="net452" />
<package id="xunit" version="2.1.0" targetFramework="net452" />
<package id="xunit.abstractions" version="2.0.0" targetFramework="net452" />
<package id="xunit.assert" version="2.1.0-beta4-build3109" targetFramework="net452" />
<package id="xunit.core" version="2.1.0-beta4-build3109" targetFramework="net452" />
<package id="xunit.extensibility.core" version="2.1.0-beta4-build3109" targetFramework="net452" />
<package id="xunit.assert" version="2.1.0" targetFramework="net452" />
<package id="xunit.core" version="2.1.0" targetFramework="net452" />
<package id="xunit.extensibility.core" version="2.1.0" targetFramework="net452" />
<package id="xunit.extensibility.execution" version="2.1.0" targetFramework="net452" />
</packages>
+9 -2
View File
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using System.Threading.Tasks;
@@ -12,27 +13,33 @@ namespace xunit.runner.data
public string SerializedForm { get; set; }
public string DisplayName { get; set; }
public string AssemblyPath { get; set; }
public Dictionary<string, List<string>> TraitMap { get; set; }
public TestCaseData(string serializedForm, string displayName, string assemblyPath)
public TestCaseData(string serializedForm, string displayName, string assemblyPath, Dictionary<string, List<string>> traitMap)
{
SerializedForm = serializedForm;
DisplayName = displayName;
AssemblyPath = assemblyPath;
TraitMap = traitMap;
}
public static TestCaseData ReadFrom(BinaryReader reader)
{
var formatter = new BinaryFormatter();
var serializedForm = reader.ReadString();
var displayName = reader.ReadString();
var assemblyPath = reader.ReadString();
return new TestCaseData(serializedForm, displayName, assemblyPath);
var traitMap = (Dictionary<string, List<string>>)formatter.Deserialize(reader.BaseStream);
return new TestCaseData(serializedForm, displayName, assemblyPath, traitMap);
}
public void WriteTo(BinaryWriter writer)
{
var formatter = new BinaryFormatter();
writer.Write(SerializedForm);
writer.Write(DisplayName);
writer.Write(AssemblyPath);
formatter.Serialize(writer.BaseStream, TraitMap);
}
}
}
+3 -3
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" />
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6"/>
</startup>
</configuration>
</configuration>
+5 -2
View File
@@ -16,11 +16,13 @@ namespace xunit.runner.worker
{
private readonly ITestFrameworkDiscoverer _discoverer;
private readonly ClientWriter _writer;
private readonly Dictionary<string, List<string>> _traitMap;
internal Impl(ITestFrameworkDiscoverer discoverer, ClientWriter writer)
{
_discoverer = discoverer;
_writer = writer;
_traitMap = new Dictionary<string, List<string>>(StringComparer.Ordinal);
}
protected override bool Visit(ITestCaseDiscoveryMessage testCaseDiscovered)
@@ -29,7 +31,8 @@ namespace xunit.runner.worker
var testCaseData = new TestCaseData(
_discoverer.Serialize(testCase),
testCase.DisplayName,
testCaseDiscovered.TestAssembly.Assembly.AssemblyPath);
testCaseDiscovered.TestAssembly.Assembly.AssemblyPath,
testCase.Traits);
Console.WriteLine(testCase.DisplayName);
_writer.Write(TestDataKind.Value);
@@ -43,7 +46,7 @@ namespace xunit.runner.worker
{
using (AssemblyHelper.SubscribeResolve())
using (var xunit = new XunitFrontController(
useAppDomain: true,
AppDomainSupport.IfAvailable,
assemblyFileName: fileName,
diagnosticMessageSink: new MessageVisitor(),
shadowCopy: false))
+117
View File
@@ -0,0 +1,117 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Pipes;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using xunit.runner.data;
namespace xunit.runner.worker
{
internal sealed class Listener
{
private readonly string _pipeName;
private readonly List<Task> _taskList = new List<Task>();
internal Listener(string pipeName)
{
_pipeName = pipeName;
}
internal void Go()
{
bool success;
do
{
_taskList.RemoveAll(x => x.IsCompleted);
success = GoOne();
} while (success);
// Wait for the existing tasks to complete before stopping the listener
Task.WaitAll(_taskList.ToArray());
}
private bool GoOne()
{
try
{
var namedPipe = new NamedPipeServerStream(_pipeName, PipeDirection.InOut, maxNumberOfServerInstances: NamedPipeServerStream.MaxAllowedServerInstances);
namedPipe.WaitForConnection();
_taskList.Add(Task.Run(() => ProcessConnection(namedPipe)));
return true;
}
catch (Exception ex)
{
Console.WriteLine($"Error creating named pipe {ex.Message}");
return false;
}
}
private static void ProcessConnection(NamedPipeServerStream stream)
{
Console.WriteLine("Connection established processing");
ProcessConnectionCore(stream);
Console.WriteLine("Connection completed");
}
private static void ProcessConnectionCore(NamedPipeServerStream stream)
{
Debug.Assert(stream.IsConnected);
try
{
var reader = new ClientReader(stream);
var action = reader.ReadString();
var argument = reader.ReadString();
switch (action)
{
case Constants.ActionDiscover:
Discover(stream, argument);
break;
case Constants.ActionRunAll:
RunAll(stream, argument);
break;
case Constants.ActionRunSpecific:
RunSpecific(stream, argument);
break;
default:
Debug.Fail($"Invalid action {action}");
break;
}
}
catch (Exception ex)
{
// Happens during a rude disconnect by the client
Console.WriteLine(ex.Message);
}
finally
{
stream.Dispose();
}
}
private static void Discover(Stream stream, string assemblyPath)
{
Console.WriteLine($"discover started: {assemblyPath}");
DiscoverUtil.Go(assemblyPath, stream);
Console.WriteLine("discover ended");
}
private static void RunAll(Stream stream, string assemblyPath)
{
Console.WriteLine($"run all started: {assemblyPath}");
RunUtil.RunAll(assemblyPath, stream);
Console.WriteLine("run all ended");
}
private static void RunSpecific(Stream stream, string assemblyPath)
{
Console.WriteLine($"run specific started: {assemblyPath}");
RunUtil.RunSpecific(assemblyPath, stream);
Console.WriteLine("run specific ended");
}
}
}
+14 -64
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Pipes;
using System.Linq;
@@ -16,83 +17,32 @@ namespace xunit.runner.worker
public static int Main(string[] args)
{
if (args.Length < 3)
if (args.Length < 2)
{
Usage();
return ExitError;
}
string pipeName = args[0];
string action = args[1];
string argument = args[2];
try
var pipeName = args[0];
var parentPid = Int32.Parse(args[1]);
var process = Process.GetProcessById(parentPid);
if (process == null)
{
using (var connection = CreateConnection(pipeName))
{
connection.WaitForClientConnect();
var stream = connection.Stream;
switch (action)
{
case Constants.ActionDiscover:
Discover(stream, argument);
break;
case Constants.ActionRunAll:
RunAll(stream, argument);
break;
case Constants.ActionRunSpecific:
RunSpecific(stream, argument);
break;
default:
Usage();
return ExitError;
}
connection.WaitForClientDone();
}
}
catch (Exception ex)
{
// Errors will happen during a rude shut down from the client. Print out to the screen
// for diagnostics and continue on.
Console.Error.WriteLine(ex.Message);
Console.WriteLine($"Invalid parent pid {parentPid}");
return ExitError;
}
Task.Run(() => WaitForParentExit(process));
var listener = new Listener(pipeName);
listener.Go();
return ExitSuccess;
}
private static Connection CreateConnection(string pipeName)
private static void WaitForParentExit(Process process)
{
if (pipeName == "test")
{
return new TestConnection();
}
return new NamedPipeConnection(pipeName);
}
private static void Discover(Stream stream, string assemblyPath)
{
Console.WriteLine($"discover started: {assemblyPath}");
DiscoverUtil.Go(assemblyPath, stream);
Console.WriteLine("discover ended");
}
private static void RunAll(Stream stream, string assemblyPath)
{
Console.WriteLine($"run all started: {assemblyPath}");
RunUtil.RunAll(assemblyPath, stream);
Console.WriteLine("run all ended");
}
private static void RunSpecific(Stream stream, string assemblyPath)
{
Console.WriteLine($"run specific started: {assemblyPath}");
RunUtil.RunSpecific(assemblyPath, stream);
Console.WriteLine("run specific ended");
process.WaitForExit();
Environment.Exit(ExitSuccess);
}
private static void Usage()
+2 -2
View File
@@ -122,8 +122,8 @@ namespace xunit.runner.worker
{
using (AssemblyHelper.SubscribeResolve())
using (var xunit = new XunitFrontController(
AppDomainSupport.IfAvailable,
assemblyFileName: assemblyPath,
useAppDomain: true,
shadowCopy: false,
diagnosticMessageSink: new MessageVisitor()))
using (var writer = new ClientWriter(stream))
@@ -139,8 +139,8 @@ namespace xunit.runner.worker
{
using (AssemblyHelper.SubscribeResolve())
using (var xunit = new XunitFrontController(
AppDomainSupport.IfAvailable,
assemblyFileName: assemblyPath,
useAppDomain: true,
shadowCopy: false,
diagnosticMessageSink: new MessageVisitor()))
using (var writer = new ClientWriter(stream))
+1 -1
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="xunit.abstractions" version="2.0.0" targetFramework="net452" />
<package id="xunit.runner.utility" version="2.1.0-beta4-build3109" targetFramework="net452" />
<package id="xunit.runner.utility" version="2.1.0" targetFramework="net46" />
</packages>
@@ -9,9 +9,10 @@
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>xunit.runner.worker</RootNamespace>
<AssemblyName>xunit.runner.worker</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<TargetFrameworkVersion>v4.6</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<TargetFrameworkProfile />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
@@ -44,13 +45,15 @@
<Reference Include="xunit.abstractions">
<HintPath>..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll</HintPath>
</Reference>
<Reference Include="xunit.runner.utility.desktop">
<HintPath>..\packages\xunit.runner.utility.2.1.0-beta4-build3109\lib\net35\xunit.runner.utility.desktop.dll</HintPath>
<Reference Include="xunit.runner.utility.desktop, Version=2.1.0.3179, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL">
<HintPath>..\packages\xunit.runner.utility.2.1.0\lib\net35\xunit.runner.utility.desktop.dll</HintPath>
<Private>True</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="Connection.cs" />
<Compile Include="DiscoverUtil.cs" />
<Compile Include="Listener.cs" />
<Compile Include="MessageVisitor.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
+3 -3
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" />
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6"/>
</startup>
</configuration>
</configuration>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 703 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 659 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 B

@@ -1,27 +1,23 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Data;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using xunit.runner.data;
using xunit.runner.wpf.ViewModel;
namespace xunit.runner.wpf.Converters
{
public class TestStateConverter : IValueConverter
{
private static ImageSource passedSource;
private static ImageSource failedSource;
private static ImageSource passedSource;
private static ImageSource skippedSource;
static TestStateConverter()
{
passedSource = LoadResourceImage("Passed.ico");
failedSource = LoadResourceImage("Failed.ico");
skippedSource = LoadResourceImage("Skipped.ico");
failedSource = LoadResourceImage("Failed_small.png");
passedSource = LoadResourceImage("Passed_small.png");
skippedSource = LoadResourceImage("Skipped_small.png");
}
private static BitmapImage LoadResourceImage(string resourceName)
@@ -42,10 +38,10 @@ namespace xunit.runner.wpf.Converters
{
case TestState.Failed:
return Brushes.Red;
case TestState.Skipped:
return Brushes.Yellow;
case TestState.Passed:
return Brushes.Green;
case TestState.Skipped:
return Brushes.Yellow;
default:
return Brushes.Gray;
}
@@ -56,10 +52,10 @@ namespace xunit.runner.wpf.Converters
{
case TestState.Failed:
return failedSource;
case TestState.Skipped:
return skippedSource;
case TestState.Passed:
return passedSource;
case TestState.Skipped:
return skippedSource;
default:
return null;
}
+3 -62
View File
@@ -17,75 +17,16 @@ namespace xunit.runner.wpf
/// <summary>
/// Discover the list of test cases which are available in the specified assembly.
/// </summary>
ITestDiscoverSession Discover(string assemblyPath, CancellationToken cancellationToken = default(CancellationToken));
Task Discover(string assemblyPath, Action<TestCaseData> callback, CancellationToken cancellationToken = default(CancellationToken));
/// <summary>
/// Begin a run of all unit tests for the given assembly.
/// </summary>
ITestRunSession RunAll(string assemblyPath, CancellationToken cancellationToken = default(CancellationToken));
Task RunAll(string assemblyPath, Action<TestResultData> callback, CancellationToken cancellationToken = default(CancellationToken));
/// <summary>
/// Begin a run of specific unit tests for the given assembly.
/// </summary>
ITestRunSession RunSpecific(string assemblyPath, ImmutableArray<string> testCaseDisplayNames, CancellationToken cancellationToken = default(CancellationToken));
Task RunSpecific(string assemblyPath, ImmutableArray<string> testCaseDisplayNames, Action<TestResultData> callback, CancellationToken cancellationToken = default(CancellationToken));
}
internal interface ITestSession
{
/// <summary>
/// Task which will be completed when the session is finished.
/// </summary>
Task Task { get; }
}
internal interface ITestRunSession : ITestSession
{
/// <summary>
/// Raised when an individual test is finished running.
/// </summary>
event EventHandler<TestResultDataEventArgs> TestFinished;
/// <summary>
/// Raised when the session has finished executing all of the specified tests.
/// </summary>
event EventHandler SessionFinished;
}
internal interface ITestDiscoverSession : ITestSession
{
/// <summary>
/// Raised when an individual test is finished running.
/// </summary>
event EventHandler<TestCaseDataEventArgs> TestDiscovered;
/// <summary>
/// Raised when the session has finished executing all of the specified tests.
/// </summary>
event EventHandler SessionFinished;
}
internal sealed class TestCaseDataEventArgs : EventArgs
{
internal readonly TestCaseData TestCaseData;
internal TestCaseDataEventArgs(TestCaseData data)
{
TestCaseData = data;
}
}
internal sealed class TestResultDataEventArgs : EventArgs
{
internal readonly TestResultData TestResultData;
internal string TestCaseDisplayName => TestResultData.TestCaseDisplayName;
internal TestState TestState => TestResultData.TestState;
internal string Output => TestResultData.Output;
internal TestResultDataEventArgs(TestResultData testResultData)
{
TestResultData = testResultData;
}
}
}
@@ -54,26 +54,29 @@ namespace xunit.runner.wpf.Impl
}
}
/// <summary>
/// Utility for reading a collection of <see cref="{T}"/> values from the given
/// <see cref="ClientReader"/> value.
/// </summary>
/// <typeparam name="T"></typeparam>
private sealed class BackgroundReader<T> where T : class
{
private readonly ConcurrentQueue<T> _queue;
private readonly ClientReader _reader;
private readonly Func<ClientReader, T> _readValue;
private readonly CancellationToken _cancellationToken;
internal ClientReader Reader => _reader;
internal BackgroundReader(ConcurrentQueue<T> queue, ClientReader reader, Func<ClientReader, T> readValue, CancellationToken cancellationToken)
internal BackgroundReader(ConcurrentQueue<T> queue, ClientReader reader, Func<ClientReader, T> readValue)
{
_queue = queue;
_reader = reader;
_readValue = readValue;
_cancellationToken = cancellationToken;
}
internal Task ReadAsync()
internal Task ReadAsync(CancellationToken cancellationToken = default(CancellationToken))
{
return Task.Run(() => GoOnBackground(), _cancellationToken);
return Task.Run(() => GoOnBackground(cancellationToken), cancellationToken);
}
/// <summary>
@@ -81,9 +84,9 @@ namespace xunit.runner.wpf.Impl
/// named pipe client stream.
/// </summary>
/// <returns></returns>
private void GoOnBackground()
private void GoOnBackground(CancellationToken cancellationToken)
{
while (!_cancellationToken.IsCancellationRequested)
while (!cancellationToken.IsCancellationRequested)
{
try
{
@@ -116,7 +119,7 @@ namespace xunit.runner.wpf.Impl
private readonly Connection _connection;
private readonly ConcurrentQueue<T> _queue;
private readonly DispatcherTimer _timer;
private readonly Action<List<T>> _callback;
private readonly Action<T> _callback;
private readonly int _maxPerTick;
private readonly TaskCompletionSource<bool> _taskCompletionSource;
@@ -126,7 +129,7 @@ namespace xunit.runner.wpf.Impl
Connection connection,
Dispatcher dispatcher,
ConcurrentQueue<T> queue,
Action<List<T>> callback,
Action<T> callback,
int maxResultPerTick = MaxResultPerTick,
TimeSpan? interval = null)
{
@@ -159,16 +162,15 @@ namespace xunit.runner.wpf.Impl
list.Add(value);
}
if (list.Count > 0)
{
_callback(list);
foreach (var cur in list)
{
_callback(cur);
}
if (isDone)
{
try
{
_callback(null);
_timer.Stop();
_connection.Dispose();
}
@@ -19,47 +19,37 @@ namespace xunit.runner.wpf.Impl
private sealed class Connection : IDisposable
{
private NamedPipeClientStream _stream;
private Process _process;
private ClientReader _reader;
internal NamedPipeClientStream Stream => _stream;
internal ClientReader Reader => _reader;
internal Connection(NamedPipeClientStream stream, Process process)
internal Connection(NamedPipeClientStream stream)
{
_stream = stream;
_process = process;
_reader = new ClientReader(stream);
}
internal void Dispose()
{
if (_process != null)
if (_stream == null)
{
Debug.Assert(_stream != null);
try
{
_stream.WriteAsync(new byte[] { 0 }, 0, 1);
}
catch
{
// Signal to server we are done with the connection. Okay to fail because
// it means the server isn't listening anymore.
}
_stream.Close();
try
{
_process.Kill();
}
catch
{
// Inherent race condition shutting down the process.
}
return;
}
try
{
_stream.WriteAsync(new byte[] { 0 }, 0, 1);
}
catch
{
// Signal to server we are done with the connection. Okay to fail because
// it means the server isn't listening anymore.
}
_stream.Close();
_stream = null;
}
void IDisposable.Dispose()
@@ -1,70 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Pipes;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Threading;
using xunit.runner.data;
using xunit.runner.wpf.ViewModel;
namespace xunit.runner.wpf.Impl
{
internal partial class RemoteTestUtil
{
private sealed class DiscoverSession : ITestDiscoverSession
{
private readonly Task _task;
private event EventHandler<TestCaseDataEventArgs> _testDiscovered;
private event EventHandler _sessionFinished;
internal DiscoverSession(Connection connection, Dispatcher dispatcher, CancellationToken cancellationToken)
{
var queue = new ConcurrentQueue<TestCaseData>();
var backgroundReader = new BackgroundReader<TestCaseData>(queue, new ClientReader(connection.Stream), r => r.ReadTestCaseData(), cancellationToken);
backgroundReader.ReadAsync();
var backgroundProducer = new BackgroundProducer<TestCaseData>(connection, dispatcher, queue, OnDiscovered);
_task = backgroundProducer.Task;
}
private void OnDiscovered(List<TestCaseData> list)
{
Debug.Assert(!_task.IsCompleted);
if (list == null)
{
_sessionFinished?.Invoke(this, EventArgs.Empty);
return;
}
foreach (var cur in list)
{
_testDiscovered?.Invoke(this, new TestCaseDataEventArgs(cur));
}
}
#region ITestRunSession
Task ITestSession.Task => _task;
event EventHandler<TestCaseDataEventArgs> ITestDiscoverSession.TestDiscovered
{
add { _testDiscovered += value; }
remove { _testDiscovered -= value; }
}
event EventHandler ITestDiscoverSession.SessionFinished
{
add { _sessionFinished += value; }
remove { _sessionFinished -= value; }
}
#endregion
}
}
}
@@ -1,100 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.IO.Pipes;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Threading;
using xunit.runner.data;
using xunit.runner.wpf.ViewModel;
namespace xunit.runner.wpf.Impl
{
internal partial class RemoteTestUtil
{
private sealed class RunSession : ITestRunSession
{
private readonly Task _task;
private event EventHandler<TestResultDataEventArgs> _testFinished;
private event EventHandler _sessionFinished;
internal RunSession(Connection connection, Dispatcher dispatcher, ImmutableArray<string> testCaseDisplayNames, CancellationToken cancellationToken)
{
var queue = CreateQueue(connection, testCaseDisplayNames, cancellationToken);
var backgroundProducer = new BackgroundProducer<TestResultData>(connection, dispatcher, queue, OnDataProduced);
_task = backgroundProducer.Task;
}
/// <summary>
/// Create the <see cref="ConcurrentQueue{T}"/> which will be populated with the <see cref="TestResultData"/>
/// as it arrives from the worker.
/// </summary>
private static ConcurrentQueue<TestResultData> CreateQueue(Connection connection, ImmutableArray<string> testCaseDisplayNames, CancellationToken cancellationToken)
{
var queue = new ConcurrentQueue<TestResultData>();
var unused = CreateQueueCore(queue, connection, testCaseDisplayNames, cancellationToken);
return queue;
}
private static async Task CreateQueueCore(ConcurrentQueue<TestResultData> queue, Connection connection, ImmutableArray<string> testCaseDisplayNames, CancellationToken cancellationToken)
{
try
{
if (!testCaseDisplayNames.IsDefaultOrEmpty)
{
var backgroundWriter = new BackgroundWriter<string>(new ClientWriter(connection.Stream), testCaseDisplayNames, (w, s) => w.Write(s), cancellationToken);
await backgroundWriter.WriteAsync();
}
var backgroundReader = new BackgroundReader<TestResultData>(queue, new ClientReader(connection.Stream), r => r.ReadTestResultData(), cancellationToken);
await backgroundReader.ReadAsync();
}
catch (Exception ex)
{
Debug.Fail(ex.Message);
// Signal data completed
queue.Enqueue(null);
}
}
private void OnDataProduced(List<TestResultData> list)
{
Debug.Assert(!_task.IsCompleted);
if (list == null)
{
_sessionFinished?.Invoke(this, EventArgs.Empty);
return;
}
foreach (var cur in list)
{
_testFinished?.Invoke(this, new wpf.TestResultDataEventArgs(cur));
}
}
#region ITestRunSession
Task ITestSession.Task => _task;
event EventHandler<TestResultDataEventArgs> ITestRunSession.TestFinished
{
add { _testFinished += value; }
remove { _testFinished -= value; }
}
event EventHandler ITestRunSession.SessionFinished
{
add { _sessionFinished += value; }
remove { _sessionFinished -= value; }
}
#endregion
}
}
}
+83 -25
View File
@@ -17,68 +17,126 @@ namespace xunit.runner.wpf.Impl
{
internal sealed partial class RemoteTestUtil : ITestUtil
{
private struct ProcessInfo
{
internal readonly string PipeName;
internal readonly Process Process;
internal ProcessInfo(string pipeName, Process process)
{
PipeName = pipeName;
Process = process;
}
}
private readonly Dispatcher _dispatcher;
private ProcessInfo? _processInfo;
internal RemoteTestUtil(Dispatcher dispatcher)
{
_dispatcher = dispatcher;
_processInfo = StartWorkerProcess();
}
private static Connection StartWorkerProcess(string action, string argument)
private async Task<Connection> CreateConnection(string action, string argument)
{
var pipeName = $"xunit.runner.wpf.pipe.{Guid.NewGuid()}";
var processStartInfo = new ProcessStartInfo();
processStartInfo.FileName = typeof(xunit.runner.worker.Program).Assembly.Location;
processStartInfo.Arguments = $"{pipeName} {action} {argument}";
processStartInfo.WindowStyle = ProcessWindowStyle.Hidden;
var pipeName = GetPipeName();
var process = Process.Start(processStartInfo);
try
{
var stream = new NamedPipeClientStream(pipeName);
stream.Connect();
return new Connection(stream, process);
await stream.ConnectAsync();
var writer = new ClientWriter(stream);
writer.Write(action);
writer.Write(argument);
return new Connection(stream);
}
catch
{
process.Kill();
try
{
_processInfo?.Process.Kill();
}
catch
{
// Inherent race condition here. Just need to make sure the process is
// dead as it can't even handle new connections.
}
throw;
}
}
private DiscoverSession Discover(string assemblyPath, CancellationToken cancellationToken)
private string GetPipeName()
{
var connection = StartWorkerProcess(Constants.ActionDiscover, assemblyPath);
return new DiscoverSession(connection, _dispatcher, cancellationToken);
var process = _processInfo?.Process;
if (process != null && !process.HasExited)
{
return _processInfo.Value.PipeName;
}
_processInfo = StartWorkerProcess();
return _processInfo.Value.PipeName;
}
private RunSession RunAll(string assemblyPath, CancellationToken cancellationToken)
private static ProcessInfo StartWorkerProcess()
{
var connection = StartWorkerProcess(Constants.ActionRunAll, assemblyPath);
return new RunSession(connection, _dispatcher, ImmutableArray<string>.Empty, cancellationToken);
var pipeName = $"xunit.runner.wpf.pipe.{Guid.NewGuid()}";
var processStartInfo = new ProcessStartInfo();
processStartInfo.FileName = typeof(xunit.runner.worker.Program).Assembly.Location;
processStartInfo.Arguments = $"{pipeName} {Process.GetCurrentProcess().Id}";
processStartInfo.WindowStyle = ProcessWindowStyle.Hidden;
var process = Process.Start(processStartInfo);
return new ProcessInfo(pipeName, process);
}
private RunSession RunSpecific(string assemblyPath, ImmutableArray<string> testCaseDisplayNames, CancellationToken cancellationToken)
private async Task Discover(string assemblyPath, Action<TestCaseData> callback, CancellationToken cancellationToken)
{
var connection = StartWorkerProcess(Constants.ActionRunSpecific, assemblyPath);
return new RunSession(connection, _dispatcher, testCaseDisplayNames, cancellationToken);
var connection = await CreateConnection(Constants.ActionDiscover, assemblyPath);
await ProcessResultsCore(connection, r => r.ReadTestCaseData(), callback, cancellationToken);
}
private async Task RunCore(string actionName, string assemblyPath, ImmutableArray<string> testCaseDisplayNames, Action<TestResultData> callback, CancellationToken cancellationToken)
{
var connection = await CreateConnection(actionName, assemblyPath);
if (!testCaseDisplayNames.IsDefaultOrEmpty)
{
var backgroundWriter = new BackgroundWriter<string>(new ClientWriter(connection.Stream), testCaseDisplayNames, (w, s) => w.Write(s), cancellationToken);
await backgroundWriter.WriteAsync();
}
await ProcessResultsCore(connection, r => r.ReadTestResultData(), callback, cancellationToken);
}
private async Task ProcessResultsCore<T>(Connection connection, Func<ClientReader, T> readValue, Action<T> callback, CancellationToken cancellationToken)
where T : class
{
var queue = new ConcurrentQueue<T>();
var backgroundReader = new BackgroundReader<T>(queue, new ClientReader(connection.Stream), readValue);
var backgroundProducer = new BackgroundProducer<T>(connection, _dispatcher, queue, callback);
await backgroundReader.ReadAsync(cancellationToken);
await backgroundProducer.Task;
}
#region ITestUtil
ITestDiscoverSession ITestUtil.Discover(string assemblyPath, CancellationToken cancellationToken)
Task ITestUtil.Discover(string assemblyPath, Action<TestCaseData> callback, CancellationToken cancellationToken)
{
return Discover(assemblyPath, cancellationToken);
return Discover(assemblyPath, callback, cancellationToken);
}
ITestRunSession ITestUtil.RunAll(string assemblyPath, CancellationToken cancellationToken)
Task ITestUtil.RunAll(string assemblyPath, Action<TestResultData> callback, CancellationToken cancellationToken)
{
return RunAll(assemblyPath, cancellationToken);
return RunCore(Constants.ActionRunAll, assemblyPath, ImmutableArray<string>.Empty, callback, cancellationToken);
}
ITestRunSession ITestUtil.RunSpecific(string assemblyPath, ImmutableArray<string> testCaseDisplayNames, CancellationToken cancellationToken)
Task ITestUtil.RunSpecific(string assemblyPath, ImmutableArray<string> testCaseDisplayNames, Action<TestResultData> callback, CancellationToken cancellationToken)
{
return RunSpecific(assemblyPath, testCaseDisplayNames, cancellationToken);
return RunCore(Constants.ActionRunSpecific, assemblyPath, testCaseDisplayNames, callback, cancellationToken);
}
#endregion
+134 -41
View File
@@ -8,6 +8,7 @@
xmlns:vm="clr-namespace:xunit.runner.wpf.ViewModel"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:cmd="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Platform"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
mc:Ignorable="d"
DataContext="{Binding Main, Source={StaticResource Locator}}"
Title="xUnit.net Test Runner"
@@ -62,6 +63,7 @@
<ColumnDefinition Width="*" />
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="auto" />
@@ -80,6 +82,7 @@
<RowDefinition Height="auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Label Content="Search:"
Grid.Row="0" />
<TextBox Grid.Row="1"
@@ -95,10 +98,47 @@
<TextBlock Text="{Binding DisplayName}" />
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="IsSelected" Value="{Binding Mode=TwoWay, Path=IsSelected}"/>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ContextMenu>
<ContextMenu>
<MenuItem Header="Reload" Command="{Binding AssemblyReloadCommand}" />
<MenuItem Header="Reload All" Command="{Binding AssemblyReloadAllCommand}" />
<Separator />
<MenuItem Header="Remove" Command="{Binding AssemblyRemoveCommand}" />
<MenuItem Header="Remove All" Command="{Binding AssemblyRemoveAllCommand}" />
</ContextMenu>
</ListBox.ContextMenu>
</ListBox>
<Label Content="Traits:"
Grid.Row="4" />
<ListBox Grid.Row="5" />
<ListBox Grid.Row="5"
ItemsSource="{Binding Traits}">
<ListBox.ItemTemplate>
<DataTemplate DataType="vm:TraitViewModel">
<TextBlock Text="{Binding DisplayName}" />
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="IsSelected" Value="{Binding Mode=TwoWay, Path=IsSelected}"/>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ContextMenu>
<ContextMenu>
<MenuItem Header="Clear" Command="{Binding TraitsClearCommand}" />
</ContextMenu>
</ListBox.ContextMenu>
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<cmd:EventToCommand Command="{Binding TraitSelectionChangedCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</ListBox>
</Grid>
</GroupBox>
@@ -127,7 +167,7 @@
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<GroupBox Header="{Binding MethodsCaption}"
<GroupBox Header="{Binding TestCasesCaption}"
Margin="3"
Grid.Row="0">
<Grid>
@@ -136,42 +176,57 @@
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ToggleButton IsChecked="{Binding IncludePassedTests}"
Margin="3">
<StackPanel Orientation="Horizontal">
<Image Source="Artwork\Passed.ico"
Height="16" />
<TextBlock Margin="4,0,0,0"
Text="{Binding TestsPassed}" />
</StackPanel>
</ToggleButton>
<ToggleButton IsChecked="{Binding IncludeFailedTests}"
Margin="3"
Grid.Column="1">
<StackPanel Orientation="Horizontal">
<Image Source="Artwork\Failed.ico"
Height="16" />
<TextBlock Margin="4,0,0,0"
Text="{Binding TestsFailed}" />
</StackPanel>
</ToggleButton>
<ToggleButton IsChecked="{Binding IncludeSkippedTests}"
Margin="3"
Grid.Column="2">
<StackPanel Orientation="Horizontal">
<Image Source="Artwork\Skipped.ico"
Height="16" />
<TextBlock Margin="4,0,0,0"
Text="{Binding TestsSkipped}" />
</StackPanel>
</ToggleButton>
</Grid>
<!--<ToolBarTray Grid.Row="0"
ToolBarTray.IsLocked="True"
Background="Transparent"
RenderOptions.BitmapScalingMode="NearestNeighbor">
<ToolBar>-->
<StackPanel Orientation="Horizontal">
<ToggleButton IsChecked="{Binding IncludePassedTests}"
BorderThickness="0"
Background="Transparent"
Margin="0,4,2,4"
Grid.Column="0">
<StackPanel Orientation="Horizontal">
<Image Source="Artwork\Passed_large.png" />
<TextBlock Margin="4,0"
FontSize="16"
Text="{Binding TestsPassed, StringFormat={}{0:#\,0}}"
VerticalAlignment="Center" />
</StackPanel>
</ToggleButton>
<ToggleButton IsChecked="{Binding IncludeFailedTests}"
BorderThickness="0"
Background="Transparent"
Margin="2,4"
Grid.Column="1">
<StackPanel Orientation="Horizontal">
<Image Source="Artwork\Failed_large.png" />
<TextBlock Margin="4,0"
FontSize="16"
Text="{Binding TestsFailed, StringFormat={}{0:#\,0}}"
VerticalAlignment="Center" />
</StackPanel>
</ToggleButton>
<ToggleButton IsChecked="{Binding IncludeSkippedTests}"
BorderThickness="0"
Background="Transparent"
Margin="2,4,0,4"
Grid.Column="2">
<StackPanel Orientation="Horizontal">
<Image Source="Artwork\Skipped_large.png" />
<TextBlock Margin="4,0"
FontSize="16"
Text="{Binding TestsSkipped, StringFormat={}{0:#\,0}}"
VerticalAlignment="Center" />
</StackPanel>
</ToggleButton>
</StackPanel>
<!--</ToolBar>
</ToolBarTray>-->
<ListBox ItemsSource="{Binding TestCases}"
SelectionMode="Extended"
Grid.Row="1">
@@ -184,10 +239,11 @@
</Grid.ColumnDefinitions>
<Image Width="16"
Margin="2"
Margin="0,0,2,0"
Source="{Binding Path=State, Mode=OneWay, Converter={StaticResource TestStateConverter}}"
Grid.Column="0" />
<TextBlock Text="{Binding DisplayName}"
VerticalAlignment="Center"
Grid.Column="1" />
</Grid>
</DataTemplate>
@@ -220,8 +276,45 @@
BorderThickness="1"
Margin="3" />
<StatusBar>
<StatusBarItem>
<Label />
<StatusBar.ItemsPanel>
<ItemsPanelTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="auto" />
<ColumnDefinition Width="auto" />
<ColumnDefinition Width="auto" />
<ColumnDefinition Width="16"/>
</Grid.ColumnDefinitions>
</Grid>
</ItemsPanelTemplate>
</StatusBar.ItemsPanel>
<StatusBarItem Grid.Column="1">
<StackPanel Orientation="Horizontal">
<TextBlock Text="Tests passed: "
Foreground="DarkGreen"/>
<TextBlock Text="{Binding TestsPassed, StringFormat={}{0:#\,0}}"/>
</StackPanel>
</StatusBarItem>
<StatusBarItem Grid.Column="2">
<StackPanel Orientation="Horizontal">
<TextBlock Text="Tests failed: "
Foreground="DarkRed"/>
<TextBlock Text="{Binding TestsFailed, StringFormat={}{0:#\,0}}"/>
</StackPanel>
</StatusBarItem>
<StatusBarItem Grid.Column="3">
<StackPanel Orientation="Horizontal">
<TextBlock Text="Tests skipped: "
Foreground="DarkGoldenrod"/>
<TextBlock Text="{Binding TestsSkipped, StringFormat={}{0:#\,0}}"/>
</StackPanel>
</StatusBarItem>
</StatusBar>
</StackPanel>
+17 -25
View File
@@ -8,10 +8,10 @@
// </auto-generated>
//------------------------------------------------------------------------------
namespace xunit.runner.wpf.Properties
{
namespace xunit.runner.wpf.Properties {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
@@ -22,48 +22,40 @@ namespace xunit.runner.wpf.Properties
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources
{
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources()
{
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager
{
get
{
if ((resourceMan == null))
{
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("xunit.runner.wpf.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture
{
get
{
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set
{
set {
resourceCulture = value;
}
}
+9 -13
View File
@@ -8,21 +8,17 @@
// </auto-generated>
//------------------------------------------------------------------------------
namespace xunit.runner.wpf.Properties
{
namespace xunit.runner.wpf.Properties {
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
{
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "14.0.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
public static Settings Default
{
get
{
public static Settings Default {
get {
return defaultInstance;
}
}
+186 -39
View File
@@ -12,9 +12,6 @@ using System.Collections.Specialized;
using System.Threading;
using System.Threading.Tasks;
using System.Diagnostics;
using System.IO.Pipes;
using System.IO;
using System.Text;
using xunit.runner.data;
using System.Windows.Threading;
@@ -24,6 +21,7 @@ namespace xunit.runner.wpf.ViewModel
{
private readonly ITestUtil testUtil;
private readonly ObservableCollection<TestCaseViewModel> allTestCases = new ObservableCollection<TestCaseViewModel>();
private readonly TraitCollectionView traitCollectionView = new TraitCollectionView();
private CancellationTokenSource filterCancellationTokenSource = new CancellationTokenSource();
private CancellationTokenSource cancellationTokenSource;
@@ -39,7 +37,7 @@ namespace xunit.runner.wpf.ViewModel
CommandBindings = CreateCommandBindings();
this.testUtil = new xunit.runner.wpf.Impl.RemoteTestUtil(Dispatcher.CurrentDispatcher);
this.MethodsCaption = "Methods (0)";
this.TestCasesCaption = "Test Cases (0)";
TestCases = new FilteredCollectionView<TestCaseViewModel, SearchQuery>(
allTestCases, TestCaseMatches, searchQuery, TestComparer.Instance);
@@ -48,6 +46,12 @@ namespace xunit.runner.wpf.ViewModel
this.WindowLoadedCommand = new RelayCommand(OnExecuteWindowLoaded);
this.RunCommand = new RelayCommand(OnExecuteRun, CanExecuteRun);
this.CancelCommand = new RelayCommand(OnExecuteCancel, CanExecuteCancel);
this.TraitSelectionChangedCommand = new RelayCommand(OnExecuteTraitSelectionChanged);
this.TraitsClearCommand = new RelayCommand(OnExecuteTraitsClear);
this.AssemblyReloadCommand = new RelayCommand(OnExecuteAssemblyReload, CanExecuteAssemblyReload);
this.AssemblyReloadAllCommand = new RelayCommand(OnExecuteAssemblyReloadAll);
this.AssemblyRemoveCommand = new RelayCommand(OnExecuteAssemblyRemove, CanExecuteAssemblyRemove);
this.AssemblyRemoveAllCommand = new RelayCommand(OnExecuteAssemblyRemoveAll);
}
private static bool TestCaseMatches(TestCaseViewModel testCase, SearchQuery searchQuery)
@@ -57,6 +61,24 @@ namespace xunit.runner.wpf.ViewModel
return false;
}
if (searchQuery.TraitSet.Count > 0)
{
var anyMatch = false;
foreach (var cur in testCase.Traits)
{
if (searchQuery.TraitSet.Contains(cur))
{
anyMatch = true;
break;
}
}
if (!anyMatch)
{
return false;
}
}
switch (testCase.State)
{
case TestState.Passed:
@@ -79,7 +101,7 @@ namespace xunit.runner.wpf.ViewModel
private void TestCases_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
MethodsCaption = $"Methods ({TestCases.Count})";
TestCasesCaption = $"Test Cases ({TestCases.Count:#,0})";
MaximumProgress = TestCases.Count;
}
@@ -87,14 +109,25 @@ namespace xunit.runner.wpf.ViewModel
public ICommand WindowLoadedCommand { get; }
public RelayCommand RunCommand { get; }
public RelayCommand CancelCommand { get; }
public ICommand TraitSelectionChangedCommand { get; }
public ICommand TraitsClearCommand { get; }
public ICommand AssemblyReloadCommand { get; }
public ICommand AssemblyReloadAllCommand { get; }
public ICommand AssemblyRemoveCommand { get; }
public ICommand AssemblyRemoveAllCommand { get; }
public CommandBindingCollection CommandBindings { get; }
private string methodsCaption;
public string MethodsCaption
public List<TestAssemblyViewModel> SelectedAssemblies
{
get { return methodsCaption; }
private set { Set(ref methodsCaption, value); }
get { return Assemblies.Where(x => x.IsSelected).ToList(); }
}
private string testCasesCaption;
public string TestCasesCaption
{
get { return testCasesCaption; }
private set { Set(ref testCasesCaption, value); }
}
private int testsCompleted = 0;
@@ -189,12 +222,14 @@ namespace xunit.runner.wpf.ViewModel
public ObservableCollection<TestAssemblyViewModel> Assemblies { get; } = new ObservableCollection<TestAssemblyViewModel>();
public FilteredCollectionView<TestCaseViewModel, SearchQuery> TestCases { get; }
public ObservableCollection<TraitViewModel> Traits => this.traitCollectionView.Collection;
private async void OnExecuteOpen(object sender, ExecutedRoutedEventArgs e)
{
var fileDialog = new OpenFileDialog
{
Filter = "Unit Test Assemblies|*.dll",
Multiselect = true
};
if (fileDialog.ShowDialog(Application.Current.MainWindow) != true)
@@ -202,8 +237,8 @@ namespace xunit.runner.wpf.ViewModel
return;
}
var fileName = fileDialog.FileName;
await AddAssemblies(new[] { new AssemblyAndConfigFile(fileName, configFileName: null) });
var assemblies = fileDialog.FileNames.Select(x => new AssemblyAndConfigFile(x, configFileName: null));
await AddAssemblies(assemblies);
}
private async Task AddAssemblies(IEnumerable<AssemblyAndConfigFile> assemblies)
@@ -218,18 +253,15 @@ namespace xunit.runner.wpf.ViewModel
{
await ExecuteTestSessionOperation(() =>
{
var testSessionList = new List<ITestSession>();
var taskList = new List<Task>();
foreach (var assembly in assemblies)
{
var assemblyPath = assembly.AssemblyFileName;
var session = this.testUtil.Discover(assemblyPath, cancellationTokenSource.Token);
session.TestDiscovered += OnTestDiscovered;
testSessionList.Add(session);
taskList.Add(this.testUtil.Discover(assemblyPath, OnTestDiscovered, cancellationTokenSource.Token));
Assemblies.Add(new TestAssemblyViewModel(assembly));
}
return testSessionList;
return taskList;
});
}
finally
@@ -238,6 +270,75 @@ namespace xunit.runner.wpf.ViewModel
}
}
private async Task ReloadAssemblies(IEnumerable<TestAssemblyViewModel> assemblies)
{
var loadingDialog = new LoadingDialog { Owner = MainWindow.Instance };
try
{
await ExecuteTestSessionOperation(() =>
{
var taskList = new List<Task>();
foreach (var assembly in assemblies)
{
var assemblyPath = assembly.FileName;
RemoveAssemblyTestCases(assemblyPath);
taskList.Add(this.testUtil.Discover(assemblyPath, OnTestDiscovered, cancellationTokenSource.Token));
}
return taskList;
});
RebuildTraits();
}
finally
{
loadingDialog.Close();
}
}
private void RemoveAssemblies(IEnumerable<TestAssemblyViewModel> assemblies)
{
foreach (var assembly in assemblies.ToList())
{
RemoveAssemblyTestCases(assembly.FileName);
Assemblies.Remove(assembly);
}
RebuildTraits();
}
private void RemoveAssemblyTestCases(string assemblyPath)
{
var i = 0;
while (i < this.allTestCases.Count)
{
if (this.allTestCases[i].AssemblyFileName == assemblyPath)
{
this.allTestCases.RemoveAt(i);
}
else
{
i++;
}
}
}
/// <summary>
/// Reloading an assembly could have changed the traits. There is no easy way
/// to selectively edit this list (traits can cross assembly boundaries). Just
/// do a full reload instead.
/// way to
/// </summary>
private void RebuildTraits()
{
this.traitCollectionView.Collection.Clear();
foreach (var testCase in this.allTestCases)
{
this.traitCollectionView.Add(testCase.Traits);
}
}
private bool IsBusy
{
get { return isBusy; }
@@ -289,7 +390,7 @@ namespace xunit.runner.wpf.ViewModel
await ExecuteTestSessionOperation(RunTests);
}
private List<ITestSession> RunTests()
private List<Task> RunTests()
{
Debug.Assert(this.isBusy);
Debug.Assert(this.cancellationTokenSource != null);
@@ -306,17 +407,15 @@ namespace xunit.runner.wpf.ViewModel
tc.State = TestState.NotRun;
}
// TODO: Need a way to filter based on traits
var runAll = TestCases.Count == this.allTestCases.Count;
var testSessionList = new List<ITestSession>();
var testSessionList = new List<Task>();
foreach (var assemblyPath in TestCases.Select(x => x.AssemblyFileName).Distinct())
{
ITestRunSession session;
Task task;
if (runAll)
{
session = this.testUtil.RunAll(assemblyPath, this.cancellationTokenSource.Token);
task = this.testUtil.RunAll(assemblyPath, OnTestFinished, this.cancellationTokenSource.Token);
}
else
{
@@ -324,17 +423,16 @@ namespace xunit.runner.wpf.ViewModel
.Where(x => x.AssemblyFileName == assemblyPath)
.Select(x => x.DisplayName)
.ToImmutableArray();
session = this.testUtil.RunSpecific(assemblyPath, testCaseDisplayNames, this.cancellationTokenSource.Token);
task = this.testUtil.RunSpecific(assemblyPath, testCaseDisplayNames, OnTestFinished, this.cancellationTokenSource.Token);
}
session.TestFinished += OnTestFinished;
testSessionList.Add(session);
testSessionList.Add(task);
}
return testSessionList;
}
private async Task ExecuteTestSessionOperation(Func<List<ITestSession>> operation)
private async Task ExecuteTestSessionOperation(Func<List<Task>> operation)
{
Debug.Assert(!this.IsBusy);
Debug.Assert(this.cancellationTokenSource == null);
@@ -344,8 +442,8 @@ namespace xunit.runner.wpf.ViewModel
this.IsBusy = true;
this.cancellationTokenSource = new CancellationTokenSource();
var testSessionList = operation();
await Task.WhenAll(testSessionList.Select(x => x.Task));
var taskList = operation();
await Task.WhenAll(taskList);
}
catch (Exception ex)
{
@@ -359,35 +457,38 @@ namespace xunit.runner.wpf.ViewModel
}
}
private void OnTestDiscovered(object sender, TestCaseDataEventArgs e)
private void OnTestDiscovered(TestCaseData testCaseData)
{
var t = e.TestCaseData;
allTestCases.Add(new TestCaseViewModel(t.SerializedForm, t.DisplayName, t.AssemblyPath));
var traitList = testCaseData.TraitMap.Count == 0
? ImmutableArray<TraitViewModel>.Empty
: testCaseData.TraitMap.SelectMany(pair => pair.Value.Select(value => new TraitViewModel(pair.Key, value))).ToImmutableArray();
this.allTestCases.Add(new TestCaseViewModel(testCaseData.SerializedForm, testCaseData.DisplayName, testCaseData.AssemblyPath, traitList));
this.traitCollectionView.Add(traitList);
}
private void OnTestFinished(object sender, TestResultDataEventArgs e)
private void OnTestFinished(TestResultData testResultData)
{
var testCase = TestCases.Single(x => x.DisplayName == e.TestCaseDisplayName);
testCase.State = e.TestState;
var testCase = TestCases.Single(x => x.DisplayName == testResultData.TestCaseDisplayName);
testCase.State = testResultData.TestState;
TestsCompleted++;
switch (e.TestState)
switch (testResultData.TestState)
{
case TestState.Passed:
TestsPassed++;
break;
case TestState.Failed:
TestsFailed++;
Output = Output + e.Output;
Output = Output + testResultData.Output;
break;
case TestState.Skipped:
TestsSkipped++;
break;
}
if (e.TestState > CurrentRunState)
if (testResultData.TestState > CurrentRunState)
{
CurrentRunState = e.TestState;
CurrentRunState = testResultData.TestState;
}
}
@@ -402,6 +503,52 @@ namespace xunit.runner.wpf.ViewModel
this.cancellationTokenSource.Cancel();
}
private void OnExecuteTraitSelectionChanged()
{
this.searchQuery.TraitSet = new HashSet<TraitViewModel>(
this.traitCollectionView.Collection.Where(x => x.IsSelected),
TraitViewModelComparer.Instance);
FilterAfterDelay();
}
private void OnExecuteTraitsClear()
{
foreach (var cur in this.traitCollectionView.Collection)
{
cur.IsSelected = false;
}
}
private bool CanExecuteAssemblyReload()
{
return SelectedAssemblies.Count > 0;
}
private async void OnExecuteAssemblyReload()
{
await ReloadAssemblies(SelectedAssemblies);
}
private async void OnExecuteAssemblyReloadAll()
{
await ReloadAssemblies(Assemblies);
}
private bool CanExecuteAssemblyRemove()
{
return SelectedAssemblies.Count > 0;
}
private void OnExecuteAssemblyRemove()
{
RemoveAssemblies(SelectedAssemblies);
}
private void OnExecuteAssemblyRemoveAll()
{
RemoveAssemblies(Assemblies.ToArray());
}
public bool IncludePassedTests
{
get { return searchQuery.IncludePassedTests; }
+1 -1
View File
@@ -11,7 +11,7 @@ namespace xunit.runner.wpf.ViewModel
public bool IncludeFailedTests = true;
public bool IncludePassedTests = true;
public bool IncludeSkippedTests = true;
public string SearchString = string.Empty;
public HashSet<TraitViewModel> TraitSet = new HashSet<TraitViewModel>(TraitViewModelComparer.Instance);
}
}
@@ -10,15 +10,22 @@ namespace xunit.runner.wpf.ViewModel
{
public class TestAssemblyViewModel : ViewModelBase
{
private readonly AssemblyAndConfigFile assembly;
private readonly AssemblyAndConfigFile _assembly;
private bool _isSelected;
public TestAssemblyViewModel(AssemblyAndConfigFile assembly)
{
this.assembly = assembly;
_assembly = assembly;
}
public string FileName => assembly.AssemblyFileName;
public string ConfigFileName => Path.GetFileNameWithoutExtension(assembly.ConfigFileName);
public string DisplayName => Path.GetFileNameWithoutExtension(assembly.AssemblyFileName);
public string FileName => _assembly.AssemblyFileName;
public string ConfigFileName => Path.GetFileNameWithoutExtension(_assembly.ConfigFileName);
public string DisplayName => Path.GetFileNameWithoutExtension(_assembly.AssemblyFileName);
public bool IsSelected
{
get { return _isSelected; }
set { Set(ref _isSelected, value, nameof(IsSelected)); }
}
}
}
@@ -1,6 +1,7 @@
using GalaSoft.MvvmLight;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Text;
@@ -11,24 +12,28 @@ namespace xunit.runner.wpf.ViewModel
{
public class TestCaseViewModel : ViewModelBase
{
public TestCaseViewModel(string testCase, string displayName, string assemblyFileName)
private TestState _state = TestState.NotRun;
public TestCaseViewModel(string testCase, string displayName, string assemblyFileName, ImmutableArray<TraitViewModel> traits)
{
this.TestCase = testCase;
this.DisplayName = displayName;
this.AssemblyFileName = assemblyFileName;
this.Traits = traits;
}
public string DisplayName { get; }
private TestState state = TestState.NotRun;
public TestState State
{
get { return state; }
set { Set(ref state, value); }
get { return _state; }
set { Set(ref _state, value); }
}
public string AssemblyFileName { get; }
public string TestCase { get; }
public ImmutableArray<TraitViewModel> Traits { get; }
}
}
@@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace xunit.runner.wpf.ViewModel
{
public sealed partial class TraitCollectionView
{
private readonly TraitViewModelComparer _comparer = TraitViewModelComparer.Instance;
private readonly ObservableCollection<TraitViewModel> _collection = new ObservableCollection<TraitViewModel>();
public ObservableCollection<TraitViewModel> Collection => _collection;
public TraitCollectionView()
{
}
public void Add(ImmutableArray<TraitViewModel> traitList)
{
if (traitList.IsDefaultOrEmpty)
{
return;
}
foreach (var traitViewModel in traitList)
{
InsertIfNotPresent(traitViewModel);
}
}
private void InsertIfNotPresent(TraitViewModel trait)
{
// TODO: make it a binary search
for (int i = 0; i < _collection.Count; i++)
{
var current = _collection[i];
var result = _comparer.Compare(trait, current);
if (result < 0)
{
_collection.Insert(i, trait);
return;
}
if (result == 0)
{
return;
}
}
_collection.Add(trait);
}
}
}
@@ -0,0 +1,31 @@
using GalaSoft.MvvmLight;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace xunit.runner.wpf.ViewModel
{
public sealed class TraitViewModel : ViewModelBase
{
private bool _isSelected;
public string Name { get; }
public string Value { get; }
public string DisplayName { get; }
public bool IsSelected
{
get { return _isSelected; }
set { Set(ref _isSelected, value); }
}
public TraitViewModel(string name, string value)
{
Name = name;
Value = value;
DisplayName = $"{Name}={Value}";
}
}
}
@@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace xunit.runner.wpf.ViewModel
{
internal sealed class TraitViewModelComparer : IEqualityComparer<TraitViewModel>, IComparer<TraitViewModel>
{
internal static readonly TraitViewModelComparer Instance = new TraitViewModelComparer();
private readonly StringComparer _comparer = StringComparer.Ordinal;
public int Compare(TraitViewModel x, TraitViewModel y)
{
var result = _comparer.Compare(x.Name, y.Name);
if (result != 0)
{
return result;
}
return _comparer.Compare(x.Value, y.Value);
}
public bool Equals(TraitViewModel x, TraitViewModel y)
{
return _comparer.Equals(x.Name, y.Name)
&& _comparer.Equals(x.Value, y.Value);
}
public int GetHashCode(TraitViewModel obj)
{
return obj.Name.GetHashCode();
}
}
}
+9 -1
View File
@@ -3,7 +3,15 @@
<package id="CommonServiceLocator" version="1.3" targetFramework="net452" />
<package id="MvvmLight" version="5.1.1.0" targetFramework="net452" />
<package id="MvvmLightLibs" version="5.1.1.0" targetFramework="net452" />
<package id="System.Collections" version="4.0.0" targetFramework="net46" />
<package id="System.Collections.Immutable" version="1.1.37" targetFramework="net452" />
<package id="System.Diagnostics.Debug" version="4.0.0" targetFramework="net46" />
<package id="System.Globalization" version="4.0.0" targetFramework="net46" />
<package id="System.Linq" version="4.0.0" targetFramework="net46" />
<package id="System.Resources.ResourceManager" version="4.0.0" targetFramework="net46" />
<package id="System.Runtime" version="4.0.0" targetFramework="net46" />
<package id="System.Runtime.Extensions" version="4.0.0" targetFramework="net46" />
<package id="System.Threading" version="4.0.0" targetFramework="net46" />
<package id="xunit.abstractions" version="2.0.0" targetFramework="net452" />
<package id="xunit.runner.utility" version="2.1.0-beta4-build3109" targetFramework="net452" />
<package id="xunit.runner.utility" version="2.1.0" targetFramework="net46" />
</packages>
+25 -7
View File
@@ -9,13 +9,14 @@
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>xunit.runner.wpf</RootNamespace>
<AssemblyName>xunit.runner.wpf</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<TargetFrameworkVersion>v4.6</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<WarningLevel>4</WarningLevel>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
<TargetFrameworkProfile />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
@@ -75,6 +76,10 @@
<Reference Include="WindowsBase" />
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
<Reference Include="xunit.runner.utility.desktop, Version=2.1.0.3179, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL">
<HintPath>..\packages\xunit.runner.utility.2.1.0\lib\net35\xunit.runner.utility.desktop.dll</HintPath>
<Private>True</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<ApplicationDefinition Include="App.xaml">
@@ -86,19 +91,20 @@
<Compile Include="Extensions.cs" />
<Compile Include="FilteredCollectionView.cs" />
<Compile Include="Impl\RemoteTestUtil.Connection.cs" />
<Compile Include="Impl\RemoteTestUtil.DiscoverSession.cs" />
<Compile Include="Impl\RemoteTestUtil.RunSession.cs" />
<Compile Include="Impl\RemoteTestUtil.BackgroundRunner.cs" />
<Compile Include="Impl\RemoteTestUtil.cs" />
<Compile Include="ITestUtil.cs" />
<Compile Include="LoadingDialog.xaml.cs">
<DependentUpon>LoadingDialog.xaml</DependentUpon>
</Compile>
<Compile Include="ViewModel\TraitCollectionView.cs" />
<Compile Include="ViewModel\AssemblyAndConfigFile.cs" />
<Compile Include="ViewModel\MainViewModel.cs" />
<Compile Include="ViewModel\SearchQuery.cs" />
<Compile Include="ViewModel\TestCaseViewModel.cs" />
<Compile Include="ViewModel\TestAssemblyViewModel.cs" />
<Compile Include="ViewModel\TraitViewModel.cs" />
<Compile Include="ViewModel\TraitViewModelComparer.cs" />
<Compile Include="ViewModel\ViewModelLocator.cs" />
<Page Include="LoadingDialog.xaml">
<SubType>Designer</SubType>
@@ -147,9 +153,6 @@
</ItemGroup>
<ItemGroup>
<Resource Include="Artwork\xunit-dot-net-small-logo.png" />
<Resource Include="Artwork\Passed.ico" />
<Resource Include="Artwork\Skipped.ico" />
<Resource Include="Artwork\Failed.ico" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SampleTestAssembly\SampleTestAssembly.csproj">
@@ -167,7 +170,22 @@
</ProjectReference>
</ItemGroup>
<ItemGroup>
<WCFMetadata Include="Service References\" />
<Resource Include="Artwork\Passed_small.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="Artwork\Passed_large.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="Artwork\Failed_small.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="Artwork\Failed_large.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="Artwork\Skipped_small.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="Artwork\Skipped_large.png" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.