Conversation
…vider in a test constructor
This addresses a code review feedback point from dotnet#25558.
|
Yes, ready and working aside from #25691
It's not much different from the last one, and if you break one of the new rules it will typically tell you what the specific problem was. Most of the changes to how tests are created are listed in #25560.
It shouldn't. |
| using (var disposableView = workspace.ExportProvider.GetExportedValue<ITextEditorFactoryService>().CreateDisposableTextView(extraBuffer)) | ||
| { | ||
| var listenerProvider = new AsynchronousOperationListenerProvider(); | ||
| var listenerProvider = workspace.ExportProvider.GetExportedValue<IAsynchronousOperationListenerProvider>(); |
There was a problem hiding this comment.
tagging @dotnet/roslyn-ide @jinujoseph @DustinCampbell @Pilchie @jasonmalinowski
I think if we go this route, we should change our code to use [Import] property/fields rather than doing [ImportingConstructor]
we choose ImportingConstructor at the time with completely opposite mindset. we wanted to give in Mock to dependencies. and make test more API level unit tests.
we start to move to more behavior test kind of unit tests. start to use MEF to provide dependencies (since our dependency was too complex, almost impossible to provide all those mocks at right layer).
now with this change, where we use MEF provider itself as isolation point, I think ImportingConstructor actually work against what we want. since user can provide one at the constructor, and deep down other service getting same dependency from MEF export and messing up everything.
we should ban using ImportingConstructor from our code, and use [Import] instead. to force, all dependencies are coming from MEF export.
There was a problem hiding this comment.
rather than doing [ImportingConstructor]
We need to keep [ImportingConstructor] so we can block direct construction of the object (otherwise the default constructor would be public and accessible). The placement of dependencies as parameters vs. properties at that point makes little difference.
Also note that this strategy does not prevent the use of mocks. To the contrary, it provides a way for a mock to be uniformly introduced for the duration of a single test, and have it disposed at the end of the test.
There was a problem hiding this comment.
hmm.. I dont understand what you mean by blocking direct construction? you can give in null to all dependencies. or mock to it. isn't one of point of what you are doing here is to make sure all dependencies get same services rather than different one?
why use code review to make sure it gets same dependency when you can make sure in does by the system?
having [Import] is same as I create object giving null to all dependencies?
There was a problem hiding this comment.
The goal of the change is for the following to be a compilation error:
new AsynchronousOperationListenerProvider(...)Alternatives to what I have implemented here are fine as long as that characteristic is preserved.
There was a problem hiding this comment.
that's interesting. so planning to mark all importing constructor as obsolete? that should work as well.
so, when someone wants to test service itself like
http://source.roslyn.io/#Roslyn.Services.Editor2.UnitTests/GoToDefinition/GoToDefinitionCommandHandlerTests.vb,103
they should get CSharpGotoDefinitionService from MEF from the beginning? ya. that should work as well rather than changing all code to [Import] from [ImportingConstructor]
so I guess that work is not done yet? I see only Async Listener Provider is marked obsolete. that can fix the problem where 2 different instance of IAsynchornounsListenerProvider is used. one directly given and one from MEF. but what about all other services?
There was a problem hiding this comment.
Correct, I have only made the change for two services to date (one in #25558, and one here).
| Using workspace = TestWorkspace.Create(definition, exportProvider:=GoToTestHelpers.ExportProviderFactory.CreateExportProvider()) | ||
| Dim cursorDocument = workspace.Documents.First(Function(d) d.CursorPosition.HasValue) | ||
| Dim cursorPosition = cursorDocument.CursorPosition.Value | ||
|
|
There was a problem hiding this comment.
this will be the example. here it uses mock IDocumentNavigationService and IStreamFindUsagespresenter mock.
but one of them is given as dependency and the other uses MEF. one is probably given as MEF since it is not exposed through constructor of CSharpGotoDefinitionService but want to easily inject that dependency under several layers.
and the other is happen to be exposed through CSharpGoToDefinitionService. unless I know exactly how these dependencies are used, I can't be sure whether IStreamingFindUsagesPresernter I gave in here is the one used internally.
removing all possibility to provide dependency except through MEF removes your concern that some tests uses 2 different instances of same service (one from constructor and one from MEF) and making test flaky.
especially, when one can add dependency through MEF to new service without realizing one had to change test like this as well since one just introduced possiblity of tests having 2 different instance of same service.
There was a problem hiding this comment.
by the way, I just searched Mock in roslyn solution. there are quite more mocks we used in our tests. I didn't even search for Testxxx. (I believe we used Mockxxx or Testxxx naming for our mock dependencies for unit tests)
There was a problem hiding this comment.
Correct, fixing the mocks is left as a follow-up item. 😄
jasonmalinowski
left a comment
There was a problem hiding this comment.
Round one of reviewing complete. To give context, I was reviewing commit-by-commit, and tried my best to revise previous comments if a later commit cleared up an earlier question. My apologies if I missed something.
I would like more comments in ExportProviderCache and UseExportProviderAttribute. Perhaps more comments than code. Half way through I wasn't able to figure out what was going on there anymore (perhaps a side effect of commit-by-commit, I admit) and so further commits only got me more confused. There's some complicated threading bits where both the "what" and the "why" aren't clear from the code, which I suspect can be simplified or at least explained well. If we can at least get comments there, I'm happy to re-review and (presuming I actually understand it and think it's right 😄) can sign off
I did very much appreciate the change to using IExportProviderFactory more. Wish we had done that sooner.
Many ProjectReferences to EditorFeatures* stuff got added, and I believe those are layering violations we'd like to avoid. I recognize that if we're already consuming those via MEF the violation is there, but let's not perpetuate it. 😄 By the end of this UseExportProviderAttribute seems to have become MakeSureThingsGetCleanedUpAttribute, and I wonder if that hints to a bit better abstraction. Spitballing here, if we just had a UseDeferredTestCleanupAttribute that just held a List that various pieces of code could register into (which would of course fail if somebody didn't have the attribute, preserving your guard), it means the ExportProviderCache could just create an ExportProvider and then register a cleanup action. Ditto for all the other things happening in the finally {} block there, complete with all the various waiting logic. I could imagine the ExportProvider cleanup also just doing a ExportProvider.GetExportValues() and where that's just an interface we define if you want other ways to inject. It's adding a layer of indirection, sure, but I think this could become a very useful tool long term. We have a lot of test stuff where we often stick logic in finalizers to ensure that test stuff was Disposed -- this would mean we could easily add "assert this was done with this helper by the time we're done" which would be great.
| } | ||
|
|
||
| public void CloseTextView() | ||
| public void DisposeAfterTest() |
There was a problem hiding this comment.
What's the point of CSharpTestWorkspaceFixture at all then? And isn't this still going to fail to flag any of the tests that are sharing it for the xunit-constructed fixture?
There was a problem hiding this comment.
➡️ No purpose. Filed #25804 to address this in a follow-up PR.
| { | ||
| get | ||
| { | ||
| return _workspaceAndCodeModel ?? (_workspaceAndCodeModel = CreateWorkspaceAndFileCodeModelAsync(_contents)); |
There was a problem hiding this comment.
Just to confirm understanding, is this an isolation fix, or just avoiding eagerly creating things when we're not using them?
There was a problem hiding this comment.
➡️ Constructors are called before UseExportProviderAttribute, so eager initialization of these properties will throw an exception on the following line (the entire gap from the end of a test to the initialization of the export provider for the next test is attributed to the former test in the exception message):
There was a problem hiding this comment.
Constructors are called before UseExportProviderAttribute
Interesting. That feels like an xunit bug/gotcha -- is that something we can fix or at least make more diagnosable? Otherwise the obvious sequence of events of:
- Write a new test from scratch, get error that you don't have attribute.
- Apply attribute.
- Run test.
- Still broken.
seems quite undebuggable.
There was a problem hiding this comment.
➡️ Extracted a method which throws the InvalidOperationException, and added a comment to help users who hit it.
| "test.dll", | ||
| LanguageNames.CSharp, | ||
| metadataReferences: new[] { TestReferences.NetFx.v4_0_30319.mscorlib })); | ||
|
|
| private readonly NonReentrantLock _serializationLock = new NonReentrantLock(); | ||
|
|
||
| public TestWorkspace(HostServices hostServices = null) | ||
| : base(hostServices ?? new AdhocWorkspace().Services.HostServices, "Test") |
There was a problem hiding this comment.
By switching the WorkspaceKind we're potentially changing the services under test here. We validated this is maintaining the existing test intentions?
There was a problem hiding this comment.
➡️ It was only used in three places, all in FormattingTests.cs. These tests do not appear to be impacted by the change.
|
|
||
| namespace Microsoft.CodeAnalysis.UnitTests | ||
| { | ||
| internal class TestWorkspace : Workspace |
There was a problem hiding this comment.
I always hated having two TestWorkspaces, thank you.
| @@ -111,9 +118,9 @@ private static Type[] GetNeutralAndCSharpAndVisualBasicTypes() | |||
| /// Create fresh ExportProvider that doesn't share anything with others. Tests can use this | |||
| { | ||
| return s_defaultHostCatalog; | ||
| } | ||
| else if (s_desktopHostCatalog != null && assembliesArray == DesktopMefHostServices.DefaultAssemblies) |
There was a problem hiding this comment.
Oof this is magic: so the static initializer calls the method that then uses the field inside of it? That seems like it's ripe for breaking.
There was a problem hiding this comment.
➡️ Refactoring to avoid magic
| var expectedCatalog = Interlocked.CompareExchange(ref _expectedCatalog, _catalog, null) ?? _catalog; | ||
| if (expectedCatalog != _catalog) | ||
| { | ||
| throw new InvalidOperationException($"Only one {nameof(ExportProvider)} can be created for a single test."); |
There was a problem hiding this comment.
I admit I'm still not getting the one-export-provider restriction. If it's for cleanup, why can't we just clean all of the ones that were made up? It seems like it'd simplify the code.
I can imagine you had cases through this work where we were creating one that was previously a static and by re-creating multiple instances you caused a bug because state that was a singleton in a test now isn't. But it seems that was a point-in-time offense, where now just allowing multiple to be created and cleaning up all of them would be easier code to maintain going forward.
Put another way: this rule might have been great for your work to do this, but seems like it needs justification (and commenting) if it's going to stay.
There was a problem hiding this comment.
➡️ Documenting condition code at the site where the exception is thrown
| if (expected == null) | ||
| { | ||
| expected = _exportProviderFactory.CreateExportProvider(); | ||
| expected = Interlocked.CompareExchange(ref _expectedProviderForCatalog, expected, null) ?? expected; |
There was a problem hiding this comment.
Why the Interlocked* here? Is this trying to support a single test consuming multiple threads? It's clearly not multiple tests running in parallel (since this mechanism of using statics wouldn't work)...
| <ProjectReference Include="..\..\Compilers\Core\Portable\CodeAnalysis.csproj" /> | ||
| <ProjectReference Include="..\..\Compilers\CSharp\Portable\CSharpCodeAnalysis.csproj" /> | ||
| <ProjectReference Include="..\..\Compilers\Test\Resources\Core\CompilerTestResources.csproj" /> | ||
| <ProjectReference Include="..\..\EditorFeatures\Core\EditorFeatures.csproj" /> |
There was a problem hiding this comment.
This is a layering violation I believe.
|
And I should clarify: the ProjectReference concerns I wouldn't consider blocking for this PR, but if they get cleaned up while trying to simplify the attribute and ExportProviderCache, I won't complain... |
|
@jasonmalinowski I filed follow-up issues (with links to/from comments) and pushed a new commit with documentation and minor refactoring. |
jasonmalinowski
left a comment
There was a problem hiding this comment.
With #25806 I'm at least convinced we kinda know what this code is doing and how to make it better, which is better than prior to it being filed.
| CreateExportProviderFactory(s_remoteHostCatalog); | ||
| CreateExportProviderFactoryNoCache(s_remoteHostCatalog); | ||
|
|
||
| private static bool _enabled; |
There was a problem hiding this comment.
Can't stick a comment on line 47 so doing it here: can we rename the setting property to something more like "Enabled_OnlyCallFromUseExportProvider"? I read the property as "enabled via the attribute" which implies there are other ways you can enable that are also valid. I didn't read it as "only use from there".
There was a problem hiding this comment.
➡️ Name is improved now
| } | ||
| } | ||
|
|
||
| return CreateAssemblyCatalogNoCache(assemblies, resolver); |
There was a problem hiding this comment.
You can remove the null checks now in CreateAssemblyCatalog, since they're always non-null since this method is always running after the static field initialization ran.
(I do like the cleanup though, thanks.)
There was a problem hiding this comment.
Also random thought/naming nit: should CreateAssemblyCatalogNoCache just be called "CreateAssemblyCatalog" and the caching one called GetOrCreate? I think that pattern is seen elsewhere and clarifies that "Create" doesn't always create something new.
There was a problem hiding this comment.
➡️ Modified to use Create and GetOrCreate consistently aside from the remaining issue in #25863
| // * A test attempted to perform multiple test sequences in the context of a single test method, | ||
| // rather than break up the test into distict tests for each case. | ||
| // * A test referenced different predefined ExportProvider instances within the context of a test. | ||
| // Each test is expected to use the same ExportProvider throughout the test. |
There was a problem hiding this comment.
This comment is great, and motivates the reasoning well. Thanks.
|
@Pilchie Can I get ask mode approval for the production code changes in this PR? They are:
|
|
Yes, I approve those changes. |
The getter is 'Enabled' and the setter is 'SetEnabled_OnlyUseExportProviderAttributeCanCall'.
|
Fun, so error code COR_E_EXECUTIONENGINE is a new one for me. 😆 |
This change ensures that all work added to the work queue after disconnection is immediately cancelled.
|
@Pilchie There is one more change to approve in commit a4eb409. I kept hitting cases where a test didn't cancel it's work, resulting in a failed test. After quite a bit of debugging locally, I found that in some cases (average once per execution of the full test suite), the Edit: 8ede7e4 further improves test cleanup reliability by avoiding scheduling new work items for which cancellation is requested in advance. |
While it doesn't make sense to still be requesting foreground actions after cancellation is requested, tracking down all callers of IForegroundNotificationService to ensure the proper checks are in place can be tedious. This change treats IForegroundNotificationService as the gatekeeper for scheduling asynchronous operations, and proactively cancels operations if requested before adding them to the work queue.
This change implements MEF container isolation during tests.
Closes #25560
Fixes #25119
Fixes #25315
It is further expected to close a number of open flaky test issues where xunit was crashing in a
FailFast.This is almost strictly a test change. The only change that could impact a user is if code with IVTs to Roslyn uses
new AsynchronousOperationListenerProvider(), as opposed to going to the MEF container.