Skip to content

Commit

Permalink
Merge pull request #42 from AArnott/fix41
Browse files Browse the repository at this point in the history
Execute theory data-generating methods on UI thread
  • Loading branch information
AArnott authored May 6, 2020
2 parents 5bc829f + 0198c32 commit 68790c2
Show file tree
Hide file tree
Showing 23 changed files with 449 additions and 226 deletions.
1 change: 1 addition & 0 deletions azure-pipelines/publish-codecoverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ steps:
displayName: Merge coverage
- task: PublishCodeCoverageResults@1
displayName: Publish code coverage results to Azure DevOps
continueOnError: true # https://developercommunity.visualstudio.com/content/problem/1018392/publish-code-coverage-results-task-fails-to-conver.html
inputs:
codeCoverageTool: cobertura
summaryFileLocation: 'coveragereport/Cobertura.xml'
Expand Down
14 changes: 14 additions & 0 deletions src/Xunit.StaFact.Tests/NonSerializableObject.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) Andrew Arnott. All rights reserved.
// Licensed under the Ms-PL license. See LICENSE.txt file in the project root for full license information.

using System;
using System.Diagnostics;

public class NonSerializableObject
{
public static object[][] Data => new object[][] { new object[] { new NonSerializableObject() } };

public int ProcessId { get; } = Process.GetCurrentProcess().Id;

public int ThreadId { get; } = Environment.CurrentManagedThreadId;
}
3 changes: 3 additions & 0 deletions src/Xunit.StaFact.Tests/StaFactTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,7 @@ public async void AsyncVoid_IsNotSupported()
// this test should be rejected by test discovery because
// async void tests aren't supportable when you have no SynchronizationContext.
}

[StaFact, Trait("TestCategory", "FailureExpected")]
public void JustFailVoid() => throw new InvalidOperationException("Expected failure.");
}
14 changes: 13 additions & 1 deletion src/Xunit.StaFact.Tests/StaTheoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
using System.Threading.Tasks;
using Xunit;

public class StaTheoryTests
public partial class StaTheoryTests
{
public static object[][] MemberDataSource => new object[][]
{
Expand Down Expand Up @@ -62,4 +62,16 @@ public async Task OperationCanceledException_Thrown(int a)
await Task.Yield();
throw new OperationCanceledException();
}

[StaTheory]
[MemberData(nameof(NonSerializableObject.Data), MemberType = typeof(NonSerializableObject))]
public void ThreadAffinitizedDataObject(NonSerializableObject o)
{
Assert.Equal(System.Diagnostics.Process.GetCurrentProcess().Id, o.ProcessId);
Assert.Equal(Environment.CurrentManagedThreadId, o.ThreadId);
}

[StaTheory, Trait("TestCategory", "FailureExpected")]
[InlineData(0)]
public void JustFailVoid(int a) => throw new InvalidOperationException("Expected failure " + a);
}
3 changes: 3 additions & 0 deletions src/Xunit.StaFact.Tests/UIFactTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -146,4 +146,7 @@ await Task.Run(delegate
5));
});
}

[UIFact, Trait("TestCategory", "FailureExpected")]
public void JustFailVoid() => throw new InvalidOperationException("Expected failure.");
}
13 changes: 12 additions & 1 deletion src/Xunit.StaFact.Tests/UITheoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,6 @@ public async Task UITheory_OnSingleThreadedSyncContext(int arg)

[Trait("TestCategory", "FailureExpected")]
[UITheory]
[InlineData(0)]
[InlineData(1)]
public async Task UITheoryFails(int arg)
{
Expand All @@ -139,4 +138,16 @@ public async Task UITheoryFails(int arg)
Assert.Same(this.ctorSyncContext, SynchronizationContext.Current);
Assert.False(arg == 0 || arg == 1);
}

[UITheory]
[MemberData(nameof(NonSerializableObject.Data), MemberType = typeof(NonSerializableObject))]
public void ThreadAffinitizedDataObject(NonSerializableObject o)
{
Assert.Equal(System.Diagnostics.Process.GetCurrentProcess().Id, o.ProcessId);
Assert.Equal(Environment.CurrentManagedThreadId, o.ThreadId);
}

[UITheory, Trait("TestCategory", "FailureExpected")]
[InlineData(0)]
public void JustFailVoid(int a) => throw new InvalidOperationException("Expected failure " + a);
}
3 changes: 3 additions & 0 deletions src/Xunit.StaFact.Tests/desktop/WinFormsFactTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ public async Task OperationCanceledException_Thrown()
throw new OperationCanceledException();
}

[DesktopFact, Trait("TestCategory", "FailureExpected")]
public void JustFailVoid() => throw new InvalidOperationException("Expected failure.");

private void AssertThreadCharacteristics()
{
Assert.Same(this.ctorSyncContext, SynchronizationContext.Current);
Expand Down
12 changes: 12 additions & 0 deletions src/Xunit.StaFact.Tests/desktop/WinFormsTheoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,16 @@ public async Task WinFormsTheoryFails(int arg)
Assert.IsType<WindowsFormsSynchronizationContext>(SynchronizationContext.Current);
Assert.False(arg == 0 || arg == 1);
}

[WinFormsTheory]
[MemberData(nameof(NonSerializableObject.Data), MemberType = typeof(NonSerializableObject))]
public void ThreadAffinitizedDataObject(NonSerializableObject o)
{
Assert.Equal(System.Diagnostics.Process.GetCurrentProcess().Id, o.ProcessId);
Assert.Equal(Environment.CurrentManagedThreadId, o.ThreadId);
}

[WinFormsTheory, Trait("TestCategory", "FailureExpected")]
[InlineData(0)]
public void JustFailVoid(int a) => throw new InvalidOperationException("Expected failure " + a);
}
3 changes: 3 additions & 0 deletions src/Xunit.StaFact.Tests/desktop/WpfFactTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ public void ShouldShowWindow()
Assert.True(window.IsVisible);
}

[DesktopFact, Trait("TestCategory", "FailureExpected")]
public void JustFailVoid() => throw new InvalidOperationException("Expected failure.");

private void AssertThreadCharacteristics()
{
Assert.IsType<DesktopSyncContext>(SynchronizationContext.Current);
Expand Down
16 changes: 14 additions & 2 deletions src/Xunit.StaFact.Tests/desktop/WpfTheoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,31 @@ public async Task WpfTheoryFails(int arg)
Assert.False(arg == 0 || arg == 1);
}

[StaTheory]
[WpfTheory]
[MemberData(nameof(MemberDataSource))]
public void MemberBasedTheory(int a, int b)
{
Assert.Equal(b, a + 1);
}

[StaTheory, Trait("TestCategory", "FailureExpected")]
[WpfTheory, Trait("TestCategory", "FailureExpected")]
[InlineData(1)]
public async Task OperationCanceledException_Thrown(int a)
{
Assert.Equal(1, a);
await Task.Yield();
throw new OperationCanceledException();
}

[WpfTheory]
[MemberData(nameof(NonSerializableObject.Data), MemberType = typeof(NonSerializableObject))]
public void ThreadAffinitizedDataObject(NonSerializableObject o)
{
Assert.Equal(System.Diagnostics.Process.GetCurrentProcess().Id, o.ProcessId);
Assert.Equal(Environment.CurrentManagedThreadId, o.ThreadId);
}

[WpfTheory, Trait("TestCategory", "FailureExpected")]
[InlineData(0)]
public void JustFailVoid(int a) => throw new InvalidOperationException("Expected failure " + a);
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ private DispatcherSynchronizationContextAdapter()

internal override SynchronizationContext Create() => new DispatcherSynchronizationContext();

internal override void CompleteOperations()
internal override Task WaitForOperationCompletionAsync(SynchronizationContext syncContext)
{
throw new NotSupportedException("Async void test methods are not supported by the WPF dispatcher. Use Async Task instead.");
}

internal override void PumpTill(Task task)
internal override void PumpTill(SynchronizationContext synchronizationContext, Task task)
{
if (!task.IsCompleted)
{
Expand All @@ -35,13 +35,6 @@ internal override void PumpTill(Task task)
}
}

internal override void Run(Func<Task> work)
{
var task = work();
this.PumpTill(task);
task.GetAwaiter().GetResult();
}

internal override void Cleanup()
{
Dispatcher.CurrentDispatcher.InvokeShutdown();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ private WinFormsSynchronizationContextAdapter()

internal override SynchronizationContext Create() => new WindowsFormsSynchronizationContext();

internal override void CompleteOperations()
internal override Task WaitForOperationCompletionAsync(SynchronizationContext syncContext)
{
throw new NotSupportedException("Async void test methods are not supported by the WinForms dispatcher. Use Async Task instead.");
}

internal override void PumpTill(Task task)
internal override void PumpTill(SynchronizationContext synchronizationContext, Task task)
{
while (!task.IsCompleted)
{
Expand All @@ -35,12 +35,5 @@ internal override void PumpTill(Task task)
}

internal override void InitializeThread() => Application.OleRequired();

internal override void Run(Func<Task> work)
{
var task = work();
this.PumpTill(task);
task.GetAwaiter().GetResult();
}
}
}
75 changes: 75 additions & 0 deletions src/Xunit.StaFact/Sdk/AsyncAutoResetEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright (c) Andrew Arnott. All rights reserved.
// Licensed under the Ms-PL license. See LICENSE.txt file in the project root for full license information.

#nullable enable

namespace Xunit.Sdk
{
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;

/// <summary>
/// An asynchronous implementation of an AutoResetEvent.
/// </summary>
[DebuggerDisplay("Signaled: {signaled}")]
internal class AsyncAutoResetEvent
{
/// <summary>
/// A queue of folks awaiting signals.
/// </summary>
private readonly Queue<TaskCompletionSource<object?>> signalAwaiters = new Queue<TaskCompletionSource<object?>>();

/// <summary>
/// A value indicating whether this event is already in a signaled state.
/// </summary>
/// <devremarks>
/// This should not need the volatile modifier because it is
/// always accessed within a lock.
/// </devremarks>
private bool signaled;

/// <summary>
/// Returns an awaitable that may be used to asynchronously acquire the next signal.
/// </summary>
/// <returns>An awaitable.</returns>
public Task WaitAsync()
{
lock (this.signalAwaiters)
{
if (this.signaled)
{
this.signaled = false;
return Task.CompletedTask;
}
else
{
var waiter = new TaskCompletionSource<object?>(TaskCreationOptions.RunContinuationsAsynchronously);
this.signalAwaiters.Enqueue(waiter);
return waiter.Task;
}
}
}

/// <summary>
/// Sets the signal if it has not already been set, allowing one awaiter to handle the signal if one is already waiting.
/// </summary>
public void Set()
{
TaskCompletionSource<object?>? toRelease = null;
lock (this.signalAwaiters)
{
if (this.signalAwaiters.Count > 0)
{
toRelease = this.signalAwaiters.Dequeue();
}
else if (!this.signaled)
{
this.signaled = true;
}
}

toRelease?.TrySetResult(null);
}
}
}
20 changes: 8 additions & 12 deletions src/Xunit.StaFact/Sdk/SyncContextAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ internal abstract class SyncContextAdapter
/// <summary>
/// Gets a value indicating whether async void methods are supported.
/// </summary>
/// <value><c>true</c> if <see cref="CompleteOperations()"/> can be invoked.</value>
/// <value><c>true</c> if <see cref="WaitForOperationCompletionAsync(SynchronizationContext)"/> can be invoked.</value>
internal virtual bool CanCompleteOperations => true;

internal virtual bool ShouldSetAsCurrent => true;

/// <summary>
/// Creates a new <see cref="SynchronizationContext"/> of the derived type.
/// </summary>
Expand All @@ -32,24 +34,18 @@ internal virtual void InitializeThread()
{
}

/// <summary>
/// Executes an async delegate while synchronously blocking the calling thread,
/// but without deadlocking.
/// </summary>
/// <param name="work">The async delegate.</param>
internal abstract void Run(Func<Task> work);

/// <summary>
/// Pumps messages until a task completes.
/// </summary>
/// <param name="syncContext">The <see cref="SynchronizationContext"/> returned from <see cref="Create"/>.</param>
/// <param name="task">The task to wait on.</param>
internal abstract void PumpTill(Task task);
internal abstract void PumpTill(SynchronizationContext syncContext, Task task);

/// <summary>
/// Pump messages until all pending operations have completed
/// and the message queue is empty.
/// Returns a <see cref="Task"/> that completes when all pending operations have completed.
/// </summary>
internal abstract void CompleteOperations();
/// <returns>A <see cref="Task"/> that completes when all pending operations have completed.</returns>
internal abstract Task WaitForOperationCompletionAsync(SynchronizationContext syncContext);

/// <summary>
/// Clean up this instance.
Expand Down
Loading

0 comments on commit 68790c2

Please sign in to comment.