-
Notifications
You must be signed in to change notification settings - Fork 733
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implemented the ability to await in STA tests 🎉 #2818
Conversation
…synchronization context check (saving thread check for later when STA synchronization context is added, plus it overlaps with the next commit's tests)
0c002f0
to
2e5cb68
Compare
@jnm2 I am not neglecting this, I just sold a house AND I'm swamped at work right now. I will review when my head is clearer. |
@rprouse I appreciate that. Don't worry about it! I also have my hands full. I'd rather have your clear-headed review. 😜 And I don't mind having an excuse to do better documentation or write this one up. (I'm thinking: I've been programming lazily and hoping people will just tell me where my code is hard to understand so that I can improve it. But that may not be fair on you...) |
…lResetEventSlim polyfills for net35 performance
I'm adding a simple ManualResetEventSlim polyfill in this one, but I built a commit on top of it with full-blown lock-free CountdownEvent and spinning/lazy ManualResetEventSlim to see if that improves the throughput and gets rid of all the performance issues on net35 CI. After this PR is merged I'll rebase it and start a separate one. |
…lResetEventSlim polyfills for net35 performance
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me although it is a lot to digest. I've asked a couple of questions, so I am not approving yet. @ChrisMaddock or @mikkelbu are either of you up to a second review? This is a large change, so the more eyes the better.
} | ||
else | ||
#endif | ||
Guard.ArgumentNotAsyncVoid(code, nameof(code)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't we be guarding that code shouldn't be async
period?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
code
is TestDelegate
which is declared as delegate void
, so if it's ever async, it's async void. I added a comment to make that clear.
{ | ||
if (type.GetTypeInfo().IsGenericType | ||
? type.GetGenericTypeDefinition().FullName == "System.Threading.Tasks.Task`1" | ||
: type.FullName == "System.Threading.Tasks.Task") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about ValueTask
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm thinking of this as the second async PR, and had planned ValueTask along with general awaitable support in the third PR.
2e5cb68
to
4be14d6
Compare
.NET 3.5 tests hung on AppVeyor, I've kicked off a new build. |
I would like a second one. Rob, from your review, do you think it would benefit from me commenting the code? |
I can try and have a look next week - but I know very little about .NET async, so am probably the wrong person for it! |
@ChrisMaddock Thanks for being willing to look! I can think of two-way benefits that can come from reviewing even so. You might catch a typo or think of an NUnit async constraint I didn't cover, and this way you have a general sense of where code is and what it's called. And if you find yourself curious about async, I enjoy explaining things like that. (Whether I'm good at explaining it might be another question. 😄) |
@ChrisMaddock I wrote it up for you anyway! 😄 All you need to know about async for this PR:
And all you need to know about SynchronizationContext:
The rest is plumbing: AsyncToSyncAdapter (formerly AsyncInvocationRegion) gets a SingleThreadedTestMessagePumpStrategy and calls WaitForCompletion which is implemented exactly like the Windows Forms and WPF strategies. And to get the test method to |
@rprouse One of these days I'm going to write it up properly in the PR body. 😆 |
Thanks Joseph, that looks great! I'll try and take a look at this next week. 😄 |
Hi folks. I thought this might be of interest/relevant: This uses distinct The examples given for these attributes are: [WpfFact]
public async Task WpfFact_OnSTAThread()
{
Assert.Equal(ApartmentState.STA, Thread.CurrentThread.GetApartmentState());
await Task.Yield();
Assert.Equal(ApartmentState.STA, Thread.CurrentThread.GetApartmentState()); // still there
} [UIFact]
public async Task UIFact_OnSTAThread()
{
int initialThread = Environment.CurrentManagedThreadId;
await Task.Yield();
Assert.Equal(initialThread, Environment.CurrentManagedThreadId);
} |
I see this PR solves the same problem UIFact does, while #2776 solves what WinFormsFact and WpfFact do. I took a more hands-off approach. E.g. rather than installing a WindowsFormsSynchronizationContext, it just reacts compatibly to the one you have. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the write-up! I don't think I'm familiar enough with this stuff to notice if there were any glaring errors - but I was able to follow it through. 🙂
@@ -0,0 +1,15 @@ | |||
using NUnit.Framework.Constraints; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a header missing here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 Fixed.
private const string TaskResultProperty = "Result"; | ||
private const string VoidTaskResultType = "VoidTaskResult"; | ||
private const string SystemAggregateException = "System.AggregateException"; | ||
private const string InnerExceptionsProperty = "InnerExceptions"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are these strings mostly just used the once? Is there a reason not to inline them?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Spot on. I'm going to leave it since this code was moved without changing it, and in the next PR when I add support for general C# awaitables (and maybe F# too), I'll be pulling this stuff out anyway.
new ThrowsConstraintAdapter(), | ||
new ThrowsExceptionConstraintAdapter(), | ||
new NormalConstraintAdapter() | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like these! 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, isn't this better than what I tried to do the last time? 😆
Oops, I just realized I forgot to cover setups and teardowns!
…synchronization context check (saving thread check for later when STA synchronization context is added, plus it overlaps with the next commit's tests)
4be14d6
to
728b6cb
Compare
@rprouse, @ChrisMaddock I rebased to fix the missing header Chris found, and then I added the last four commits containing some new simple tests and refactors. As soon as any team member reapproves, I'm happy for this to be merged. |
I was getting some strange results when using NUnit to test some related functionality in TerstDriven.Net (see jcansdale/TestDriven.Net-Issues#110 for the gory details). It turns out the following test fails in NUnit [Test]
[Apartment(ApartmentState.STA)]
public async Task ApartmentStateAfterYield()
{
await Task.Yield();
Assert.That(Thread.CurrentThread.GetApartmentState(), Is.EqualTo(ApartmentState.STA));
} This test passes when using the This PR would get a ✔️ from me. Thanks for your work on this @jnm2. I still find the task architecture a little mind bending and your write-ups have been appreciated. 👍 BTW, I think this would make a great blog post and something for the NUnit twitter. What do you reckon @jnm2? 😄 |
Thanks! I see you're a member here, which is awesome... I don't know but I don't think there would be a problem with you approving the PR if you wanted?
Haha, I had just offered to help with this. There are at least three more fundamental changes beyond these two which I want to get in for 3.11:
And a sweep of a dozen potentially-related existing issues. |
However, @rprouse, don't wait for my my slow pace with these if you want to release 3.11 sooner. Master has been continuously deliverable as far as I know. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tested this build with an async test I was having problems with. This PR makes STA tests behave is a much less surprising way.
Haven't had a chance to review the code itself.
I've approved from an API consumer PoV. I'd love to review the code itself, but work, family etc. 😕 |
@jcansdale Understandable, no worries. Thanks for your feedback! |
LGTM, let's get this merged 😄 |
If you'd like to take advantage of this early, look for version <?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="NUnit MyGet" value="https://www.myget.org/F/nunit/api/v3/index.json" />
</packageSources>
</configuration> |
Hi @jnm2 I tried package 05600 with your changes. It would be really amazing to make a new configurable behavior that executes all delegates in SynchronizaitonContext that are left. This will make testing of WDYT ? |
Also i've noticed that
is 3-5 times slower than just
|
@Belorus Since there's only one STA, your first example is single-threaded for all practical purposes. If you had some non-STA fixtures, they would run on parallel. Are you sure all your tests need the STA? |
n@CharliePoole Well, what my example needs is a SynchronizationContext that will run all delegates in its queue before letting test finish. This means that all async void methods have finished their work. This sounds for me as a good behavior for most cases, if you expect people testing async void methods. From my POV its absolutely legal to create 8 threads each with its own SynchronizationContext. I don't see reason to execute such tests sequentially. |
Because STA is an old COM concept it may not be what you want. @jnm2 did all the recent work on synchronization contexts and will probably have to help us out here. |
@Belorus Thanks for the questions! I replied about async void over where you initially asked: #2851 (comment).
I would expect NUnit to be creating eight STA threads for you. So long as nothing COM-sensitive crosses from one thread to another, that's the ideal situation. |
Also, Charlie is right about STA being an old COM thing. You should only use it if you need to use COM, such as certain Windows UI. If what you really want is for your async test to stay on the same thread, I think the thing that aligns 1:1 with your goal is actually a SingleThreadedSynchronizationContext like the one I posted at #2851 (comment) or my original one at #1200 (comment). |
Fixes #1200, re: #2774
Whew! This was fun and the collateral was worth doing, but this was a longer time coming than I expected!
The heart and soul of this change is the 8th (last) commit. Its motivating tests are the 6th commit.
The 7th commit took a lot of thought to get right while keeping the complexity down. It refactors AsyncInvocationRegion—which as I've noticed many times since I arrived at this project, had become neither async, an invocation, nor a region 😆—due to the necessity for it to control the invocation itself rather than being handed the return value from the invocation. I did my best to come up with a descriptive name,
AsyncToSyncAdapter
, since it adapts async execution to synchronous execution.The preceding commits were things which were all in the critical path, so for simplicity they are in the same PR. They should each be viewed separately.
@rprouse Would it be a good idea for me to do a walkthrough explanation of the 8th commit for posterity, or do #1200 and #2774 explain it well enough?
/cc @sharwell