diff --git a/src/NUnitCommon/nunit.extensibility.api/IExtensionNode.cs b/src/NUnitCommon/nunit.extensibility.api/IExtensionNode.cs index 54f4f0f48..ed86420e7 100644 --- a/src/NUnitCommon/nunit.extensibility.api/IExtensionNode.cs +++ b/src/NUnitCommon/nunit.extensibility.api/IExtensionNode.cs @@ -9,6 +9,18 @@ namespace NUnit.Extensibility /// The IExtensionNode interface is implemented by a class that represents a /// single extension being installed on a particular extension point. /// + public enum ExtensionStatus + { + /// Extension is not yet loaded + Unloaded, + /// Extension has been loaded + Loaded, + /// An error occurred trying to load the extension + Error, + /// Extension without a corresponding path./summary> + Unknown, + } + public interface IExtensionNode { /// @@ -22,6 +34,16 @@ public interface IExtensionNode /// true if enabled; otherwise, false. bool Enabled { get; } + /// + /// Status of this extension. + /// + ExtensionStatus Status { get; } + + /// + /// Exception thrown in creating the ExtensionObject, if Status is error, otherwise null. + /// + Exception? Exception { get; } + /// /// Gets the unique string identifying the ExtensionPoint for which /// this Extension is intended. This identifier may be supplied by the attribute @@ -39,14 +61,6 @@ public interface IExtensionNode /// IEnumerable PropertyNames { get; } - /// - /// Gets a collection of the values of a particular named property - /// If none are present, returns an empty enumerator. - /// - /// The property name - /// A collection of values - IEnumerable GetValues(string name); - /// /// The path to the assembly implementing this extension. /// @@ -56,5 +70,13 @@ public interface IExtensionNode /// The version of the assembly implementing this extension. /// Version AssemblyVersion { get; } + + /// + /// Gets a collection of the values of a particular named property. + /// If none are present, returns an empty enumerator. + /// + /// The property name + /// A collection of values + IEnumerable GetValues(string name); } } diff --git a/src/NUnitCommon/nunit.extensibility.tests/ExtensionManagerTests.cs b/src/NUnitCommon/nunit.extensibility.tests/ExtensionManagerTests.cs index ada0f5c4e..f9aec928a 100644 --- a/src/NUnitCommon/nunit.extensibility.tests/ExtensionManagerTests.cs +++ b/src/NUnitCommon/nunit.extensibility.tests/ExtensionManagerTests.cs @@ -8,6 +8,7 @@ using System.IO; using System.Reflection; using System.Collections.Generic; +using System.Net; namespace NUnit.Extensibility { @@ -18,9 +19,11 @@ public class ExtensionManagerTests private static readonly string THIS_ASSEMBLY_DIRECTORY = Path.GetDirectoryName(THIS_ASSEMBLY.Location)!; private const string FAKE_EXTENSIONS_FILENAME = "FakeExtensions.dll"; private static readonly string FAKE_EXTENSIONS_PARENT_DIRECTORY = - Path.Combine(new DirectoryInfo(THIS_ASSEMBLY_DIRECTORY).Parent!.FullName, "fakesv2"); - private static readonly string FAKE_EXTENSIONS_SOURCE_DIRECTORY = - Path.Combine(new DirectoryInfo(THIS_ASSEMBLY_DIRECTORY).Parent!.Parent!.Parent!.FullName, "src/TestData/FakeExtensions"); +#if NETFRAMEWORK + Path.Combine(new DirectoryInfo(THIS_ASSEMBLY_DIRECTORY).Parent!.FullName, "fakesv2/net462"); +#else + Path.Combine(new DirectoryInfo(THIS_ASSEMBLY_DIRECTORY).Parent!.FullName, "fakesv2/netstandard2.0"); +#endif private const string FAKE_AGENT_LAUNCHER_EXTENSION = "NUnit.Engine.Fakes.FakeAgentLauncherExtension"; private const string FAKE_FRAMEWORK_DRIVER_EXTENSION = "NUnit.Engine.Fakes.FakeFrameworkDriverExtension"; @@ -30,6 +33,7 @@ public class ExtensionManagerTests private const string FAKE_SERVICE_EXTENSION = "NUnit.Engine.Fakes.FakeServiceExtension"; private const string FAKE_DISABLED_EXTENSION = "NUnit.Engine.Fakes.FakeDisabledExtension"; private const string FAKE_NUNIT_V2_DRIVER_EXTENSION = "NUnit.Engine.Fakes.V2DriverExtension"; + private const string FAKE_EXTENSION_WITH_NO_EXTENSION_POINT = "NUnit.Engine.Fakes.FakeExtension_NoExtensionPointFound"; private readonly string[] KnownExtensions = { @@ -39,8 +43,9 @@ public class ExtensionManagerTests FAKE_RESULT_WRITER_EXTENSION, FAKE_EVENT_LISTENER_EXTENSION, FAKE_SERVICE_EXTENSION, - FAKE_DISABLED_EXTENSION - //FAKE_NUNIT_V2_DRIVER_EXTENSION + FAKE_DISABLED_EXTENSION, + //FAKE_NUNIT_V2_DRIVER_EXTENSION, + FAKE_EXTENSION_WITH_NO_EXTENSION_POINT }; private ExtensionManager _extensionManager; @@ -94,7 +99,7 @@ public void CreateExtensionManager() _extensionManager.FindExtensionPoints(typeof(ITestEngine).Assembly); // Find Fake Extensions using alternate start directory - _extensionManager.FindExtensionAssemblies(FAKE_EXTENSIONS_SOURCE_DIRECTORY); + _extensionManager.FindExtensionAssemblies(FAKE_EXTENSIONS_PARENT_DIRECTORY); _extensionManager.LoadExtensions(); } @@ -121,6 +126,18 @@ public void AllExtensionsUseTheLatestVersion() Assert.That(node.AssemblyVersion.ToString(), Is.EqualTo("2.0.0.0")); } + [Test] + public void AllExtensionsHaveCorrectStatus() + { + foreach (var node in _extensionManager.Extensions) + { + var expectedStatus = node.TypeName == FAKE_EXTENSION_WITH_NO_EXTENSION_POINT + ? ExtensionStatus.Unknown + : ExtensionStatus.Unloaded; + Assert.That(node.Status, Is.EqualTo(expectedStatus)); + } + } + [Test] public void AllKnownExtensionsAreEnabledAsRequired() { @@ -216,20 +233,20 @@ public void SkipsGracefullyLoadingOtherFrameworkExtensionAssembly() #if NETCOREAPP [TestCase("netstandard2.0", ExpectedResult = true)] - [TestCase("net462", ExpectedResult = false)] + //[TestCase("net462", ExpectedResult = false)] //[TestCase("net20", ExpectedResult = false)] #elif NET40_OR_GREATER - [TestCase("netstandard2.0", ExpectedResult = false)] + //[TestCase("netstandard2.0", ExpectedResult = false)] [TestCase("net462", ExpectedResult = true)] //[TestCase("net20", ExpectedResult = true)] #else - [TestCase("netstandard2.0", ExpectedResult = false)] - [TestCase("net462", ExpectedResult = false)] + //[TestCase("netstandard2.0", ExpectedResult = false)] + //[TestCase("net462", ExpectedResult = false)] //[TestCase("net20", ExpectedResult = true)] #endif public bool LoadTargetFramework(string tfm) { - return _extensionManager.CanLoadTargetFramework(THIS_ASSEMBLY, FakeExtensions(tfm)); + return _extensionManager.CanLoadTargetFramework(THIS_ASSEMBLY, FakeExtensions()); } //[TestCaseSource(nameof(ValidCombos))] @@ -351,10 +368,10 @@ private static string GetSiblingDirectory(string dir) /// assembly based on the argument provided. /// /// A test framework moniker. Must be one for which the fake extensions are built. - private static ExtensionAssembly FakeExtensions(string tfm) + private static ExtensionAssembly FakeExtensions() { return new ExtensionAssembly( - Path.Combine(FAKE_EXTENSIONS_PARENT_DIRECTORY, Path.Combine(tfm, FAKE_EXTENSIONS_FILENAME)), false); + Path.Combine(FAKE_EXTENSIONS_PARENT_DIRECTORY, FAKE_EXTENSIONS_FILENAME), false); } } } diff --git a/src/NUnitCommon/nunit.extensibility/ExtensionManager.cs b/src/NUnitCommon/nunit.extensibility/ExtensionManager.cs index d5ce0789d..0e46d4f9d 100644 --- a/src/NUnitCommon/nunit.extensibility/ExtensionManager.cs +++ b/src/NUnitCommon/nunit.extensibility/ExtensionManager.cs @@ -547,25 +547,19 @@ public void FindExtensionsInAssembly(ExtensionAssembly extensionAssembly) _extensions.Add(node); - ExtensionPoint? ep; - if (extensionAttrPath is null) - { - ep = DeduceExtensionPointFromType(extensionType); - if (ep is null) - throw new ExtensibilityException($"Unable to deduce ExtensionPoint for Type {extensionType.FullName}. Specify Path on ExtensionAttribute to resolve."); + ExtensionPoint? ep = extensionAttrPath is not null + ? GetExtensionPoint(extensionAttrPath) + : DeduceExtensionPointFromType(extensionType); - node.Path = ep.Path; - } - else + if (ep is null) { - node.Path = extensionAttrPath; - - // TODO: Remove need for the cast - ep = GetExtensionPoint(node.Path) as ExtensionPoint; - if (ep is null) - throw new ExtensibilityException($"Unable to locate ExtensionPoint for Type {extensionType.FullName}. The Path {node.Path} cannot be found."); + log.Warning($"Extension ignored - Unable to deduce ExtensionPoint."); + node.Status = ExtensionStatus.Unknown; + node.Exception = new Exception("Unable to deduce ExtensionPoint"); + continue; } + node.Path = ep.Path; ep.Install(node); } } @@ -619,28 +613,6 @@ public bool CanLoadTargetFramework(Assembly? runnerAsm, ExtensionAssembly extens return false; } } - - //string extensionFrameworkName = AssemblyDefinition.ReadAssembly(extensionAsm.FilePath).GetFrameworkName(); - //string runnerFrameworkName = AssemblyDefinition.ReadAssembly(runnerAsm.Location).GetFrameworkName(); - //if (runnerFrameworkName?.StartsWith(".NETStandard") == true) - //{ - // throw new NUnitEngineException($"{runnerAsm.FullName} test runner must target .NET Core or .NET Framework, not .NET Standard"); - //} - //else if (runnerFrameworkName?.StartsWith(".NETCoreApp") == true) - //{ - // if (extensionFrameworkName?.StartsWith(".NETStandard") != true && extensionFrameworkName?.StartsWith(".NETCoreApp") != true) - // { - // log.Info($".NET Core runners require .NET Core or .NET Standard extension for {extensionAsm.FilePath}"); - // return false; - // } - //} - //else if (extensionFrameworkName?.StartsWith(".NETCoreApp") == true) - //{ - // log.Info($".NET Framework runners cannot load .NET Core extension {extensionAsm.FilePath}"); - // return false; - //} - - //return true; } private static System.Runtime.Versioning.FrameworkName GetTargetRuntime(string filePath) diff --git a/src/NUnitCommon/nunit.extensibility/ExtensionNode.cs b/src/NUnitCommon/nunit.extensibility/ExtensionNode.cs index 1fe3ab5be..579a8aeee 100644 --- a/src/NUnitCommon/nunit.extensibility/ExtensionNode.cs +++ b/src/NUnitCommon/nunit.extensibility/ExtensionNode.cs @@ -31,6 +31,7 @@ public ExtensionNode(ExtensionAssembly extensionAssembly, TypeDefinition extensi AssemblyPath = extensionAssembly.FilePath; AssemblyVersion = extensionAssembly.AssemblyVersion; TypeName = extensionType.FullName; + Status = ExtensionStatus.Unloaded; Enabled = true; // By default } @@ -62,6 +63,16 @@ public ExtensionNode(ExtensionAssembly extensionAssembly, TypeDefinition extensi /// true if enabled; otherwise, false. public bool Enabled { get; set; } + /// + /// Status of this extension + /// + public ExtensionStatus Status { get; set; } + + /// + /// Exception thrown in creating the ExtensionObject, if Status is error, otherwise null. + /// + public Exception? Exception { get; set; } + /// /// Gets and sets the unique string identifying the ExtensionPoint for which /// this Extension is intended. This identifier may be supplied by the attribute @@ -127,6 +138,8 @@ public object CreateExtensionObject(params object[] args) object obj = Activator.CreateInstance(type, args)!; #endif + // TODO: Determine whether to continue support for V3 extensions here, + // defer it to the engine or eliminate it entirely. return IsV3Extension ? ExtensionWrapper.Wrap(obj, Path) : obj; @@ -134,6 +147,9 @@ public object CreateExtensionObject(params object[] args) #endregion + /// + /// Used by ExtensionManger to add a value to the node's properties collection. + /// public void AddProperty(string name, string val) { if (_properties.TryGetValue(name, out List? list)) @@ -145,6 +161,9 @@ public void AddProperty(string name, string val) } } + /// + /// Gets the string representation of this node. + /// public override string ToString() { return $"{TypeName} - {Path}"; diff --git a/src/NUnitConsole/nunit4-console/ConsoleRunner.cs b/src/NUnitConsole/nunit4-console/ConsoleRunner.cs index 4b6cba074..c16ad7ee9 100644 --- a/src/NUnitConsole/nunit4-console/ConsoleRunner.cs +++ b/src/NUnitConsole/nunit4-console/ConsoleRunner.cs @@ -1,17 +1,20 @@ // Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt -using System; -using System.Collections.Generic; -using System.IO; -using System.Xml; using NUnit.Common; -using NUnit.ConsoleRunner.Utilities; using NUnit.ConsoleRunner.Options; +using NUnit.ConsoleRunner.Utilities; using NUnit.Engine; using NUnit.Engine.Extensibility; +using NUnit.Extensibility; using NUnit.TextDisplay; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Runtime.InteropServices; using System.Text; +using System.Xml; +using System.Xml.Linq; namespace NUnit.ConsoleRunner { @@ -21,7 +24,7 @@ namespace NUnit.ConsoleRunner /// public class ConsoleRunner { - private static Logger log = InternalTrace.GetLogger(typeof(ConsoleRunner)); + private static readonly Logger log = InternalTrace.GetLogger(typeof(ConsoleRunner)); private static readonly char[] PathSeparator = [Path.PathSeparator]; @@ -36,6 +39,7 @@ public class ConsoleRunner private const string INDENT4 = " "; private const string INDENT6 = " "; private const string INDENT8 = " "; + private const string INDENT10 = " "; private const string NUNIT_EXTENSION_DIRECTORIES = "NUNIT_EXTENSION_DIRECTORIES"; @@ -330,9 +334,12 @@ private static void DisplayRuntimeEnvironment(ExtendedTextWriter OutWriter) OutWriter.WriteLine(); } +#if NETFRAMEWORK + [DllImport("libc")] + private static extern int uname(IntPtr buf); + private static string GetOSVersion() { -#if NETFRAMEWORK OperatingSystem os = Environment.OSVersion; string osString = os.ToString(); if (os.Platform == PlatformID.Unix) @@ -349,13 +356,13 @@ private static string GetOSVersion() Marshal.FreeHGlobal(buf); } return osString; + } #else + private static string GetOSVersion() + { return RuntimeInformation.OSDescription; -#endif } - - [DllImport("libc")] - private static extern int uname(IntPtr buf); +#endif private void DisplayExtensionList() { @@ -369,35 +376,59 @@ private void DisplayExtensionList() _outWriter.WriteLine(ColorStyle.SectionHeader, "Installed Extensions"); - if (_extensionService?.ExtensionPoints is not null) + if (_extensionService.ExtensionPoints is not null) foreach (var ep in _extensionService.ExtensionPoints) { _outWriter.WriteLabelLine(INDENT4 + "Extension Point: ", ep.Path); foreach (var node in ep.Extensions) - { - _outWriter.Write(INDENT6 + "Extension: "); - _outWriter.Write(ColorStyle.Value, $"{node.TypeName}"); - _outWriter.WriteLine(node.Enabled ? string.Empty : " (Disabled)"); - - _outWriter.Write(INDENT8 + "Version: "); - _outWriter.WriteLine(ColorStyle.Value, node.AssemblyVersion.ToString()); - - _outWriter.Write(INDENT8 + "Path: "); - _outWriter.WriteLine(ColorStyle.Value, node.AssemblyPath); - - foreach (var prop in node.PropertyNames) - { - _outWriter.Write(INDENT8 + prop + ":"); - foreach (var val in node.GetValues(prop)) - _outWriter.Write(ColorStyle.Value, " " + val); - _outWriter.WriteLine(); - } - } + DisplayExtension(node); } + var unknownExtensions = _extensionService.Extensions.Where(n => n.Status == ExtensionStatus.Unknown); + if (unknownExtensions.Any()) + { + _outWriter.WriteLine(); + _outWriter.WriteLine(ColorStyle.Label, "Unknown Extensions"); + _outWriter.WriteLine(INDENT4 + "Extensions not matching any known ExtensionPoint"); + foreach (var node in unknownExtensions) + DisplayExtension(node); + } + _outWriter.WriteLine(); } + private void DisplayExtension(IExtensionNode node) + { + _outWriter.Write(INDENT6 + "Extension: "); + _outWriter.Write(ColorStyle.Value, $"{node.TypeName}"); + _outWriter.WriteLine(node.Enabled ? string.Empty : " (Disabled)"); + + _outWriter.Write(INDENT8 + "Version: "); + _outWriter.WriteLine(ColorStyle.Value, node.AssemblyVersion.ToString()); + + _outWriter.Write(INDENT8 + "Status: "); + _outWriter.WriteLine(ColorStyle.Value, node.Status.ToString()); + + _outWriter.Write(INDENT8 + "Enabled: "); + _outWriter.WriteLine(ColorStyle.Value, node.Enabled.ToString()); + + _outWriter.Write(INDENT8 + "Path: "); + _outWriter.WriteLine(ColorStyle.Value, node.AssemblyPath); + + if (node.PropertyNames.Any()) + { + _outWriter.WriteLine(INDENT8 + "Properties -"); + + foreach (var prop in node.PropertyNames) + { + _outWriter.Write(INDENT10 + prop + ":"); + foreach (var val in node.GetValues(prop)) + _outWriter.Write(ColorStyle.Value, " " + val); + _outWriter.WriteLine(); + } + } + } + private void DisplayTestFilters() { if (_options.TestList.Count > 0 || _options.WhereClauseSpecified) diff --git a/src/TestData/FakeExtensions/2.0/FakeExtensions.cs b/src/TestData/FakeExtensions/2.0/FakeExtensions.cs index 437340fe9..14f59d691 100644 --- a/src/TestData/FakeExtensions/2.0/FakeExtensions.cs +++ b/src/TestData/FakeExtensions/2.0/FakeExtensions.cs @@ -132,4 +132,10 @@ public class FakeAgentLauncherExtension : IAgentLauncher public Process CreateAgent(Guid agentId, string agencyUrl, TestPackage package) => throw new NotImplementedException(); } + + [Extension] + public class FakeExtension_NoExtensionPointFound + { + public void SomeMethod() => throw new NotImplementedException(); + } } diff --git a/src/TestData/FakeExtensions/2.0/FakeExtensionsV2.csproj b/src/TestData/FakeExtensions/2.0/FakeExtensionsV2.csproj index a3c45360a..1ab28d1bb 100644 --- a/src/TestData/FakeExtensions/2.0/FakeExtensionsV2.csproj +++ b/src/TestData/FakeExtensions/2.0/FakeExtensionsV2.csproj @@ -14,4 +14,10 @@ + + + PreserveNewest + + + diff --git a/src/TestData/FakeExtensions/2.0/fake-extensions.addins b/src/TestData/FakeExtensions/2.0/fake-extensions.addins new file mode 100644 index 000000000..3af4f2bde --- /dev/null +++ b/src/TestData/FakeExtensions/2.0/fake-extensions.addins @@ -0,0 +1,3 @@ +# This file is copied to the binary output directory for use +# when running unit tests. +FakeExtensions.dll