Skip to content

Isolate tests#25681

Merged
sharwell merged 30 commits intodotnet:dev15.7.xfrom
sharwell:isolate-tests
Mar 31, 2018
Merged

Isolate tests#25681
sharwell merged 30 commits intodotnet:dev15.7.xfrom
sharwell:isolate-tests

Conversation

@sharwell
Copy link
Copy Markdown
Contributor

@sharwell sharwell commented Mar 23, 2018

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.

@sharwell sharwell requested a review from a team as a code owner March 23, 2018 17:30
@CyrusNajmabadi
Copy link
Copy Markdown
Contributor

  1. is this ready for review?
  2. is there any writup that explains the new system and how new test fixtures should be created?
  3. Does this affect how people write features?

@sharwell
Copy link
Copy Markdown
Contributor Author

is this ready for review?

Yes, ready and working aside from #25691

is there any writup that explains the new system and how new test fixtures should be created?

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.

Does this affect how people write features?

It shouldn't.

using (var disposableView = workspace.ExportProvider.GetExportedValue<ITextEditorFactoryService>().CreateDisposableTextView(extraBuffer))
{
var listenerProvider = new AsynchronousOperationListenerProvider();
var listenerProvider = workspace.ExportProvider.GetExportedValue<IAsynchronousOperationListenerProvider>();
Copy link
Copy Markdown
Contributor

@heejaechang heejaechang Mar 26, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

@sharwell sharwell Mar 26, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

@heejaechang heejaechang Mar 27, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

@heejaechang heejaechang Mar 27, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, fixing the mocks is left as a follow-up item. 😄

Copy link
Copy Markdown
Member

@jasonmalinowski jasonmalinowski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

➡️ No purpose. Filed #25804 to address this in a follow-up PR.

{
get
{
return _workspaceAndCodeModel ?? (_workspaceAndCodeModel = CreateWorkspaceAndFileCodeModelAsync(_contents));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to confirm understanding, is this an isolation fix, or just avoiding eagerly creating things when we're not using them?

Copy link
Copy Markdown
Contributor Author

@sharwell sharwell Mar 29, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

➡️ 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):

MefHostServices.HookServiceCreation((_, __) => throw new InvalidOperationException("Cannot create host services after test tear down."));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Write a new test from scratch, get error that you don't have attribute.
  2. Apply attribute.
  3. Run test.
  4. Still broken.

seems quite undebuggable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

➡️ 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 }));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra blank line.

private readonly NonReentrantLock _serializationLock = new NonReentrantLock();

public TestWorkspace(HostServices hostServices = null)
: base(hostServices ?? new AdhocWorkspace().Services.HostServices, "Test")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By switching the WorkspaceKind we're potentially changing the services under test here. We validated this is maintaining the existing test intentions?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

➡️ 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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment now stale.

{
return s_defaultHostCatalog;
}
else if (s_desktopHostCatalog != null && assembliesArray == DesktopMefHostServices.DefaultAssemblies)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

➡️ 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.");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

@sharwell sharwell Mar 29, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

➡️ 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;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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" />
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a layering violation I believe.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

➡️ This should be covered by #25805

@jasonmalinowski
Copy link
Copy Markdown
Member

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...

@sharwell
Copy link
Copy Markdown
Contributor Author

@jasonmalinowski I filed follow-up issues (with links to/from comments) and pushed a new commit with documentation and minor refactoring.

Copy link
Copy Markdown
Member

@jasonmalinowski jasonmalinowski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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".

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

➡️ Name is improved now

}
}

return CreateAssemblyCatalogNoCache(assemblies, resolver);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

➡️ 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.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is great, and motivates the reasoning well. Thanks.

@sharwell
Copy link
Copy Markdown
Contributor Author

@Pilchie Can I get ask mode approval for the production code changes in this PR? They are:

  1. A few IVTs from production to local test assemblies (should not need special approval?)

  2. A change to IAsynchronousOperationListenerProvider.cs here, which prevents outside code from using the constructor of AsynchronousOperationListenerProvider. We already verified it is not used in this manner downstream.

    sharwell@33a1a3b#diff-ce3a65ac7e5f22f0b2333d00b6c89556

  3. The addition of one test hook method:

    internal static void ResetHostServicesTestOnly()
    {
    s_defaultServices = null;
    }

@Pilchie
Copy link
Copy Markdown
Member

Pilchie commented Mar 30, 2018

Yes, I approve those changes.

@sharwell
Copy link
Copy Markdown
Contributor Author

sharwell commented Mar 31, 2018

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.
@sharwell
Copy link
Copy Markdown
Contributor Author

sharwell commented Mar 31, 2018

@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 CancellationTokenSource was getting reset to non-cancelled after TagSource.Dispose was called, and then a work item queued with that token.

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.
@sharwell sharwell merged commit 0e76c45 into dotnet:dev15.7.x Mar 31, 2018
@sharwell sharwell deleted the isolate-tests branch March 31, 2018 23:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants