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