Skip to content
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

Multi-Targeting of engine extensions (.NET Framework only) #452

Merged
merged 8 commits into from
Aug 25, 2018

Conversation

ChrisMaddock
Copy link
Member

This is some unfinished work from a few months back - excuse it being a bit random! Fixes #394 as far as is currently possible - if we're considering implementing extension functionality for the .NET Standard engine as a whole separate issue!

The engine will now prioritise the extension with the highest target framework - when two of the same type are made available. Preference order in determining which extension version to load is now:

Assembly Version -> Target Framework -> Found via wildcard/path directly specified

In the short term, this unblocks nunit/vs-project-loader#25 - something I might have a look at soon. In the longer-term - this is hopefully the foundations for resolving extensions made available for different platforms.

Again, I recommend review commit-by-commit - I'll add in a few line-comments shortly on some bits worth highlighting. The changes are less significant then they look - I did an amount of refactoring so we could test this better, as I imagine this functionality may become more complex over time!

using System.Reflection;
using System.Text;
using NUnit.Engine.Extensibility;
using NUnit.Framework;

Copy link
Member Author

Choose a reason for hiding this comment

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

Significant changes in this file are mainly due to tests here split out into ExtensionSelectorTests

@@ -0,0 +1,77 @@
// ***********************************************************************
Copy link
Member Author

Choose a reason for hiding this comment

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

This file has been moved and isn't entirely new - I guess GitHub hasn't noticed as there's also some changes.

@@ -0,0 +1,75 @@
// ***********************************************************************
Copy link
Member Author

Choose a reason for hiding this comment

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

Functionality in this new class mostly extracted from ExtensionAssembly.cs - some changes to consider targetframework. Refactor was for test purposes.

{
if (!fromWildCard)
throw;
}
Copy link
Member Author

Choose a reason for hiding this comment

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

Additional check as we now attempt to read target frameworks in this section, which can throw.

@ChrisMaddock
Copy link
Member Author

Tests are failing on mono-only, I'm going to guess some differences in assembly name reflection. Looking into it...

@ChrisMaddock ChrisMaddock changed the title Multi-Targeting of engine extensions (.NET Framework only) [WIP] Multi-Targeting of engine extensions (.NET Framework only) Aug 18, 2018
@ChrisMaddock
Copy link
Member Author

Issue is that our RuntimeFramework code currently believes Mono 5.10.0.160 can't support .NET 2.0 targeted things. I think I might have to deal with this as a separate issue...

@ChrisMaddock
Copy link
Member Author

ChrisMaddock commented Aug 18, 2018

Cool - an actual bug! 😄

Turns out RuntimeFramework.Supports() wasn't doing what I expected - I've added in a RuntimeFramework.CanLoad() instead.

Ready for review now!

@ChrisMaddock ChrisMaddock changed the title [WIP] Multi-Targeting of engine extensions (.NET Framework only) Multi-Targeting of engine extensions (.NET Framework only) Aug 18, 2018
@ChrisMaddock
Copy link
Member Author

@rprouse - this is a non-trivial change, and could probably be merged post-release, if you'd prefer? I don't mind either way. 🙂

@@ -432,7 +446,7 @@ public void FindExtensionsInAssembly(ExtensionAssembly assembly)
if (versionArg != null && new Version((string)versionArg) > ENGINE_VERSION)
continue;

var node = new ExtensionNode(assembly.FilePath, type.FullName);
var node = new ExtensionNode(assembly.FilePath, type.FullName, assemblyTargetFramework);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Once we decide that we can load the assembly in the current runtime, why is the assemblyTargetFramework still needed in the extension node? Is it only so that the console runner can display it?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, exactly. Info is only stored in ExtensionNode to be accessible to runners.

"48cd57a6")]

[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2,PublicKey=002400000480000094" +
Copy link
Collaborator

Choose a reason for hiding this comment

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

Did I miss where this is explained? Maybe a comment here would help.

Copy link
Member Author

Choose a reason for hiding this comment

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

Sorry - this is to allow using NSubstitute with internal types. It should have a comment, yes!

Copy link
Member Author

Choose a reason for hiding this comment

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

Done!

@CharliePoole
Copy link
Collaborator

I had to go back and re-read all the discussion, including my own comments, to try to remember why we were doing this! Restating what I think you are trying to accomplish in my own terms so you can tell me if I get it correctly... 😄

  1. Currently, we can write extensions that target .NET 2.0. We ask that extension-writers do so in order to make them available to all users.

  2. You'd like to write extensions that target higher framework versions and eventually .NET Core. This change takes care of the first and prepares for doing the second later on.

Where I am a bit confused is that I thought we already handled the first case. If you write an extension that targets say 4.5, then we don't load it unless we are running under 4.5 or higher. The existing extension service is supposed to take care of that and I'm pretty sure it once did.

So, are we doing anything new here? Or are we just preparing for eventual .NET Core extensions? IOW, what is the use case that currently fails but will succeed under this PR?

If that case is, for example, loading a 4.6 extension when running under 3.5, I'd like to figure out when it stopped working correctly - that is, ignoring the extension. At least I think it used to work that way!

@ChrisMaddock
Copy link
Member Author

ChrisMaddock commented Aug 18, 2018

@CharliePoole - your understanding is correct. (Specifically, in my case right now, I want to write a net46 version of the vs-project-loader extension)

What's definitely new, is that the engine will now purposefully choose the highest target framework version of an extension - previously, with two equal assembly-version extensions, it would have just taken the first it encountered. That allows us to package multiple versions of the same extension in the same NuGet package, and ensure the user always gets the 'best' version available.

I don't believe there was any previous functionality at all to check if the current framework supports the target framework of the extension. As far as I can see, it would have crashed at the point it tries to load the assembly. I'd have thought that we previously haven't encountered that problem as all the extensions we look after are .NET 2.0. Whether any other extension authors have, I don't know.

Of course, if we start distributing none .NET 2.0 extensions, then that situation becomes much more likely, so I added in the check. 🙂

@CharliePoole
Copy link
Collaborator

My memory (which could easily be wrong) was that we caught the exception (from 4.6 for example) logged the error and ignored the assembly without crashing. I had to deal with it even though we didn't have extensions > 2.0 because there could still be assemblies in the same directory that targeted a higher version. In fact, at one point, we threw if there was a non-assembly in the same directory! However, this all may have been before we started using Mono.Cecil and you obviously have to deal with what we now do, not with what we used to do.

In any case, selecting the highest loadable version is a new thing that I hadn't thought about.

BTW, I assume you are dealing with the identical assembly being found twice? It's possible to set up connected .addins files in a way that the same directory is scanned twice and it's also possible to have two copies of the same assembly in different directories. In fact, that's what the original "duplicate" logic was about.

@ChrisMaddock
Copy link
Member Author

ChrisMaddock commented Aug 18, 2018

Ah. So if there's an exception at the point of examining an assembly - then yes, we would catch and continue (or error, circumstance depending) - adding that was my first ever contribution to NUnit, in fact! 😄 I had thought however that it would be possible to examine an assembly targeting a higher framework without crashing...but I'm not sure. The particular case I fixed was an exception on examining an unmanaged dll.

BTW, I assume you are dealing with the identical assembly being found twice?

I didn't change existing functionality here - if there's no differentiating factors two assemblies, the engine chooses the first it encounters.

@CharliePoole
Copy link
Collaborator

Then it all makes sense to me. Nice work!

Copy link
Collaborator

@CharliePoole CharliePoole left a comment

Choose a reason for hiding this comment

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

Looks Great now!

Copy link
Member

@rprouse rprouse left a comment

Choose a reason for hiding this comment

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

Looks good...

@rprouse rprouse merged commit e355877 into master Aug 25, 2018
@rprouse rprouse deleted the issue-394b branch August 25, 2018 15:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants