diff --git a/src/NUnitConsole/nunit3-console.tests/CommandLineTests.cs b/src/NUnitConsole/nunit3-console.tests/CommandLineTests.cs index cf69e7c5e..f2fda3363 100644 --- a/src/NUnitConsole/nunit3-console.tests/CommandLineTests.cs +++ b/src/NUnitConsole/nunit3-console.tests/CommandLineTests.cs @@ -144,6 +144,7 @@ public void NoInputFiles() [TestCase("DisposeRunners", "dispose-runners")] [TestCase("TeamCity", "teamcity")] [TestCase("SkipNonTestAssemblies", "skipnontestassemblies")] + [TestCase("NoResult", "noresult")] #if NETFRAMEWORK [TestCase("RunAsX86", "x86")] [TestCase("ShadowCopyFiles", "shadowcopy")] @@ -505,20 +506,6 @@ public void DefaultResultSpecification() Assert.That(spec.Transform, Is.Null); } - [Test] - public void NoResultSuppressesDefaultResultSpecification() - { - var options = ConsoleMocks.Options("test.dll", "-noresult"); - Assert.That(options.ResultOutputSpecifications.Count, Is.EqualTo(0)); - } - - [Test] - public void NoResultSuppressesAllResultSpecifications() - { - var options = ConsoleMocks.Options("test.dll", "-result:results.xml", "-noresult", "-result:nunit2results.xml;format=nunit2"); - Assert.That(options.ResultOutputSpecifications.Count, Is.EqualTo(0)); - } - [Test] public void InvalidResultSpecRecordsError() { @@ -859,6 +846,13 @@ public void DeprecatedLabelsOptionsAreReplacedCorrectly(string oldOption, string Assert.That(options.DisplayTestLabels, Is.EqualTo(newOption)); } + public void UserExtensionDirectoryTest() + { + ConsoleOptions options = ConsoleMocks.Options("--extensionDirectory=/a/b/c"); + Assert.That(options.Validate); + Assert.That(options.ExtensionDirectories.Contains("/a/b/c")); + } + private static IFileSystem GetFileSystemContainingFile(string fileName) { var fileSystem = new VirtualFileSystem(); diff --git a/src/NUnitConsole/nunit3-console.tests/ConsoleRunnerTests.cs b/src/NUnitConsole/nunit3-console.tests/ConsoleRunnerTests.cs index 991322567..17f038de4 100644 --- a/src/NUnitConsole/nunit3-console.tests/ConsoleRunnerTests.cs +++ b/src/NUnitConsole/nunit3-console.tests/ConsoleRunnerTests.cs @@ -39,9 +39,9 @@ public void ThrowsNUnitEngineExceptionWhenTestResultsAreNotWriteable() } [Test] - public void ThrowsNUnitExceptionWhenTeamcityOptionIsSpecifiedButNotAvailable() + public void ThrowsRequiredExtensionExceptionWhenTeamcityOptionIsSpecifiedButNotAvailable() { - var ex = Assert.Throws( + var ex = Assert.Throws( () => new ConsoleRunner(_testEngine, ConsoleMocks.Options("mock-assembly.dll", "--teamcity"), new ColorConsoleWriter())); Assert.That(ex, Has.Message.Contains("teamcity")); diff --git a/src/NUnitConsole/nunit3-console.tests/MakeTestPackageTests.cs b/src/NUnitConsole/nunit3-console.tests/MakeTestPackageTests.cs index 7f8978a66..3f3173f64 100644 --- a/src/NUnitConsole/nunit3-console.tests/MakeTestPackageTests.cs +++ b/src/NUnitConsole/nunit3-console.tests/MakeTestPackageTests.cs @@ -65,7 +65,7 @@ public void WhenOptionIsSpecified_PackageIncludesSetting(string option, string k var options = ConsoleMocks.Options("test.dll", option); var package = ConsoleRunner.MakeTestPackage(options); - Assert.That(package.Settings.ContainsKey(key), "Setting not included for {0}", option); + Assert.That(package.Settings.ContainsKey(key), $"Setting not included for {option}"); Assert.That(package.Settings[key], Is.EqualTo(val), "NumberOfTestWorkers not set correctly for {0}", option); } diff --git a/src/NUnitConsole/nunit3-console/ConsoleOptions.cs b/src/NUnitConsole/nunit3-console/ConsoleOptions.cs index 5eba632c4..2939dc804 100644 --- a/src/NUnitConsole/nunit3-console/ConsoleOptions.cs +++ b/src/NUnitConsole/nunit3-console/ConsoleOptions.cs @@ -21,8 +21,7 @@ public class ConsoleOptions : OptionSet { private static readonly string CURRENT_DIRECTORY_ON_ENTRY = Directory.GetCurrentDirectory(); - private bool validated; - private bool noresult; + private bool _validated; /// /// An abstraction of the file system @@ -41,7 +40,7 @@ internal ConsoleOptions(IDefaultOptionsProvider defaultOptionsProvider, IFileSys Parse(args); } - // Action to Perform + // Action to Perform ( Default is to run the tests ) public bool Explore { get; private set; } @@ -49,11 +48,16 @@ internal ConsoleOptions(IDefaultOptionsProvider defaultOptionsProvider, IFileSys public bool ShowVersion { get; private set; } + public bool ListExtensions { get; private set; } + + // Additional directories to be used to search for user extensions + public List ExtensionDirectories { get; } = new List(); + // Select tests - public IList InputFiles { get; } = new List(); + public List InputFiles { get; } = new List(); - public IList TestList { get; } = new List(); + public List TestList { get; } = new List(); public IDictionary TestParameters { get; } = new Dictionary(); @@ -99,14 +103,13 @@ public string WorkDirectory public string InternalTraceLevel { get; private set; } public bool InternalTraceLevelSpecified { get { return InternalTraceLevel != null; } } + public bool NoResult { get; private set; } + private readonly List resultOutputSpecifications = new List(); - public IList ResultOutputSpecifications + public List ResultOutputSpecifications { get { - if (noresult) - return new OutputSpecification[0]; - if (resultOutputSpecifications.Count == 0) resultOutputSpecifications.Add( new OutputSpecification("TestResult.xml", CURRENT_DIRECTORY_ON_ENTRY)); @@ -115,7 +118,7 @@ public IList ResultOutputSpecifications } } - public IList ExploreOutputSpecifications { get; } = new List(); + public List ExploreOutputSpecifications { get; } = new List(); public string ActiveConfig { get; private set; } public bool ActiveConfigSpecified { get { return ActiveConfig != null; } } @@ -153,15 +156,13 @@ public IList ResultOutputSpecifications public bool DebugAgent { get; private set; } - public bool ListExtensions { get; private set; } - public bool PauseBeforeRun { get; private set; } public string PrincipalPolicy { get; private set; } - public IList WarningMessages { get; } = new List(); + public List WarningMessages { get; } = new List(); - public IList ErrorMessages { get; } = new List(); + public List ErrorMessages { get; } = new List(); private void ConfigureOptions() { @@ -274,7 +275,7 @@ private void ConfigureOptions() }); this.Add("noresult", "Don't save any test results.", - v => noresult = v != null); + v => NoResult = v != null); this.Add("labels=", "Specify whether to write test case names to the output. Values: Off, OnOutputOnly, Before, After, BeforeAndAfter", v => { @@ -383,6 +384,9 @@ private void ConfigureOptions() this.Add("list-extensions", "List all extension points and the extensions for each.", v => ListExtensions = v != null); + this.Add("extensionDirectory=", "Specifies an additional directory to be examined for extensions. May be repeated.", + v => { ExtensionDirectories.Add(Path.GetFullPath(v)); }); + this.AddNetFxOnlyOption("set-principal-policy=", "Set PrincipalPolicy for the test domain.", NetFxOnlyOption("set-principal-policy=", v => PrincipalPolicy = parser.RequiredValue(v, "--set-principal-policy", "UnauthenticatedPrincipal", "NoPrincipal", "WindowsPrincipal"))); @@ -413,11 +417,11 @@ private Action NetFxOnlyOption(string optionName, Action action) public bool Validate() { - if (!validated) + if (!_validated) { CheckOptionCombinations(); - validated = true; + _validated = true; } return ErrorMessages.Count == 0; diff --git a/src/NUnitConsole/nunit3-console/ConsoleRunner.cs b/src/NUnitConsole/nunit3-console/ConsoleRunner.cs index f06503cec..606fec6d0 100644 --- a/src/NUnitConsole/nunit3-console/ConsoleRunner.cs +++ b/src/NUnitConsole/nunit3-console/ConsoleRunner.cs @@ -11,6 +11,7 @@ using NUnit.Engine.Extensibility; using System.Runtime.InteropServices; using System.Text; +using System.Linq; namespace NUnit.ConsoleRunner { @@ -26,7 +27,10 @@ public class ConsoleRunner private const int MAXIMUM_RETURN_CODE_ALLOWED = 100; // In case we are running on Unix private const string EVENT_LISTENER_EXTENSION_PATH = "/NUnit/Engine/TypeExtensions/ITestEventListener"; - private const string TEAMCITY_EVENT_LISTENER = "NUnit.Engine.Listeners.TeamCityEventListener"; + private const string TEAMCITY_EVENT_LISTENER_FULLNAME = "NUnit.Engine.Listeners.TeamCityEventListener"; + private const string TEAMCITY_EVENT_LISTENER_NAME = "TeamCityEventListener"; + + private const string NUNIT_EXTENSION_DIRECTORIES = "NUNIT_EXTENSION_DIRECTORIES"; public static readonly int OK = 0; public static readonly int INVALID_ARG = -1; @@ -48,28 +52,42 @@ public class ConsoleRunner public ConsoleRunner(ITestEngine engine, ConsoleOptions options, ExtendedTextWriter writer) { - _engine = engine; - _options = options; - _outWriter = writer; - - _workDirectory = options.WorkDirectory ?? Directory.GetCurrentDirectory(); - - if (!Directory.Exists(_workDirectory)) - Directory.CreateDirectory(_workDirectory); + Guard.ArgumentNotNull(_engine = engine, nameof(engine)); + Guard.ArgumentNotNull(_options = options, nameof(options)); + Guard.ArgumentNotNull(_outWriter = writer, nameof(writer)); + // NOTE: Accessing Services triggers the engine to initialize all services _resultService = _engine.Services.GetService(); + Guard.OperationValid(_resultService != null, "Internal Error: ResultService was not found"); + _filterService = _engine.Services.GetService(); + Guard.OperationValid(_filterService != null, "Internal Error: TestFilterService was not found"); + _extensionService = _engine.Services.GetService(); + Guard.OperationValid(_extensionService != null, "Internal Error: ExtensionService was not found"); - // TODO: Exit with error if any of the services are not found + var extensionPath = Environment.GetEnvironmentVariable(NUNIT_EXTENSION_DIRECTORIES); + if (!string.IsNullOrEmpty(extensionPath)) + foreach (string extensionDirectory in extensionPath.Split(new[] { Path.PathSeparator }, StringSplitOptions.RemoveEmptyEntries)) + _extensionService.FindExtensionAssemblies(extensionDirectory); + + foreach (string extensionDirectory in _options.ExtensionDirectories) + _extensionService.FindExtensionAssemblies(extensionDirectory); + + _workDirectory = options.WorkDirectory; + if (_workDirectory != null) + Directory.CreateDirectory(_workDirectory); + else + _workDirectory = null; if (_options.TeamCity) { bool teamcityInstalled = false; foreach (var node in _extensionService.GetExtensionNodes(EVENT_LISTENER_EXTENSION_PATH)) - if (teamcityInstalled = node.TypeName == TEAMCITY_EVENT_LISTENER) + if (teamcityInstalled = node.TypeName == TEAMCITY_EVENT_LISTENER_FULLNAME) break; - if (!teamcityInstalled) throw new NUnitEngineException("Option --teamcity specified but the extension is not installed."); + if (!teamcityInstalled) + throw new RequiredExtensionException(TEAMCITY_EVENT_LISTENER_NAME, "--teamcity"); } // Enable TeamCityEventListener immediately, before the console is redirected @@ -149,49 +167,49 @@ private int RunTests(TestPackage package, TestFilter filter) { var writer = new ColorConsoleWriter(!_options.NoColor); - foreach (var spec in _options.ResultOutputSpecifications) - { - var outputPath = Path.Combine(_workDirectory, spec.OutputPath); + if (!_options.NoResult) + foreach (var spec in _options.ResultOutputSpecifications) + { + var outputPath = Path.Combine(_workDirectory, spec.OutputPath); - IResultWriter resultWriter; + IResultWriter resultWriter; - try - { - resultWriter = GetResultWriter(spec); - } - catch (Exception ex) - { - throw new NUnitEngineException($"Error encountered in resolving output specification: {spec}", ex); - } + try + { + resultWriter = GetResultWriter(spec); + } + catch (Exception ex) + { + throw new NUnitEngineException($"Error encountered in resolving output specification: {spec}", ex); + } - try - { - var outputDirectory = Path.GetDirectoryName(outputPath); - Directory.CreateDirectory(outputDirectory); - } - catch (Exception ex) - { - writer.WriteLine(ColorStyle.Error, String.Format( - "The directory in --result {0} could not be created", - spec.OutputPath)); - writer.WriteLine(ColorStyle.Error, ExceptionHelper.BuildMessage(ex)); - return ConsoleRunner.UNEXPECTED_ERROR; - } + try + { + var outputDirectory = Path.GetDirectoryName(outputPath); + Directory.CreateDirectory(outputDirectory); + } + catch (Exception ex) + { + writer.WriteLine(ColorStyle.Error, String.Format( + "The directory in --result {0} could not be created", + spec.OutputPath)); + writer.WriteLine(ColorStyle.Error, ExceptionHelper.BuildMessage(ex)); + return ConsoleRunner.UNEXPECTED_ERROR; + } - try - { - resultWriter.CheckWritability(outputPath); - } - catch (Exception ex) - { - throw new NUnitEngineException( - String.Format( - "The path specified in --result {0} could not be written to", - spec.OutputPath), ex); + try + { + resultWriter.CheckWritability(outputPath); + } + catch (Exception ex) + { + throw new NUnitEngineException( + String.Format( + "The path specified in --result {0} could not be written to", + spec.OutputPath), ex); + } } - } - var labels = _options.DisplayTestLabels != null ? _options.DisplayTestLabels.ToUpperInvariant() : "ON"; @@ -310,9 +328,17 @@ private static string GetOSVersion() private void DisplayExtensionList() { + if (_options.ExtensionDirectories.Count > 0) + { + _outWriter.WriteLine(ColorStyle.SectionHeader, "User Extension Directories"); + foreach (var dir in _options.ExtensionDirectories) + _outWriter.WriteLine($" {Path.GetFullPath(dir)}"); + _outWriter.WriteLine(); + } + _outWriter.WriteLine(ColorStyle.SectionHeader, "Installed Extensions"); - foreach (var ep in _extensionService?.ExtensionPoints ?? new IExtensionPoint[0]) + foreach (var ep in _extensionService.ExtensionPoints) { _outWriter.WriteLabelLine(" Extension Point: ", ep.Path); foreach (var node in ep.Extensions) diff --git a/src/NUnitConsole/nunit3-console/Program.cs b/src/NUnitConsole/nunit3-console/Program.cs index 06e8a96b1..02a5ad179 100644 --- a/src/NUnitConsole/nunit3-console/Program.cs +++ b/src/NUnitConsole/nunit3-console/Program.cs @@ -116,6 +116,11 @@ public static int Main(string[] args) { return new ConsoleRunner(engine, Options, OutWriter).Execute(); } + catch (RequiredExtensionException ex) + { + OutWriter.WriteLine(ColorStyle.Error, ex.Message); + return ConsoleRunner.INVALID_ARG; + } catch (TestSelectionParserException ex) { OutWriter.WriteLine(ColorStyle.Error, ex.Message); diff --git a/src/NUnitConsole/nunit3-console/RequiredExtensionException.cs b/src/NUnitConsole/nunit3-console/RequiredExtensionException.cs new file mode 100644 index 000000000..38de0082a --- /dev/null +++ b/src/NUnitConsole/nunit3-console/RequiredExtensionException.cs @@ -0,0 +1,47 @@ +// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt + +using System; + +namespace NUnit.ConsoleRunner +{ + /// + /// RequiredExtensionException is thrown when the console runner is executed + /// with a command-line option that requires a particular extension and + /// that extension has not been installed. + /// + public class RequiredExtensionException : Exception + { + private static string Message1(string extensionName) => $"Required extension {extensionName} is not installed."; + private static string Message2(string extensionName, string option) => $"Option {option} specified but {extensionName} is not installed."; + + /// + /// Construct with the name of an extension + /// + public RequiredExtensionException(string extensionName) : base(Message1(extensionName)) + { + } + + /// + /// Construct with the name of an extension and the command-line option requiring that extension. + /// + public RequiredExtensionException(string extensionName, string option) : base(Message2(extensionName, option)) + { + } + + /// + /// Construct with the name of an extension and inner exception + /// + public RequiredExtensionException(string extensionName, Exception innerException) + : base(Message1(extensionName), innerException) + { + } + + /// + /// Construct with the name of an extension, a command-line option and inner exception + /// + public RequiredExtensionException(string extensionName, string option, Exception innerException) + : base(Message2(extensionName, option), innerException) + { + } + } +} diff --git a/src/NUnitEngine/nunit.engine.api/IExtensionService.cs b/src/NUnitEngine/nunit.engine.api/IExtensionService.cs index 56ab7a7a8..6f2cc7f12 100644 --- a/src/NUnitEngine/nunit.engine.api/IExtensionService.cs +++ b/src/NUnitEngine/nunit.engine.api/IExtensionService.cs @@ -21,6 +21,13 @@ public interface IExtensionService /// IEnumerable Extensions { get; } + /// + /// Find and install extensions starting from a given base directory, + /// and using the contained '.addins' files to direct the search. + /// + /// Path to the initial directory. + void FindExtensionAssemblies(string initialDirectory); + /// /// Get an ExtensionPoint based on its unique identifying path. /// @@ -34,8 +41,6 @@ public interface IExtensionService /// /// Enable or disable an extension /// - /// - /// void EnableExtension(string typeName, bool enabled); } } diff --git a/src/NUnitEngine/nunit.engine.core.tests/Extensibility/ExtensionAssemblyTests.cs b/src/NUnitEngine/nunit.engine.core.tests/Extensibility/ExtensionAssemblyTests.cs index 87a16bf9f..a90c47ae0 100644 --- a/src/NUnitEngine/nunit.engine.core.tests/Extensibility/ExtensionAssemblyTests.cs +++ b/src/NUnitEngine/nunit.engine.core.tests/Extensibility/ExtensionAssemblyTests.cs @@ -11,7 +11,6 @@ public class ExtensionAssemblyTests { private static readonly Assembly THIS_ASSEMBLY = Assembly.GetExecutingAssembly(); private static readonly string THIS_ASSEMBLY_PATH = THIS_ASSEMBLY.Location; - private static readonly string THIS_ASSEMBLY_FULL_NAME = THIS_ASSEMBLY.GetName().FullName; private static readonly string THIS_ASSEMBLY_NAME = THIS_ASSEMBLY.GetName().Name; private static readonly Version THIS_ASSEMBLY_VERSION = THIS_ASSEMBLY.GetName().Version; @@ -23,18 +22,6 @@ public void CreateExtensionAssemblies() _ea = new ExtensionAssembly(THIS_ASSEMBLY_PATH, false); } - [Test] - public void AssemblyDefinition() - { - Assert.That(_ea.Assembly.FullName, Is.EqualTo(THIS_ASSEMBLY_FULL_NAME)); - } - - [Test] - public void MainModule() - { - Assert.That(_ea.MainModule.Assembly.FullName, Is.EqualTo(THIS_ASSEMBLY_FULL_NAME)); - } - [Test] public void AssemblyName() { diff --git a/src/NUnitEngine/nunit.engine.core.tests/Internal/ExtensionAssemblyTrackerTests.cs b/src/NUnitEngine/nunit.engine.core.tests/Internal/ExtensionAssemblyTrackerTests.cs new file mode 100644 index 000000000..dc1e6bd18 --- /dev/null +++ b/src/NUnitEngine/nunit.engine.core.tests/Internal/ExtensionAssemblyTrackerTests.cs @@ -0,0 +1,79 @@ +// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt + +using System; +using System.ComponentModel; +using System.Reflection; +using NUnit.Engine.Extensibility; +using NUnit.Framework; + +namespace NUnit.Engine.Internal.Tests +{ + public class ExtensionAssemblyTrackerTests + { + private static readonly Assembly THIS_ASSEMBLY = typeof(ExtensionAssemblyTrackerTests).Assembly; + private static readonly string THIS_ASSEMBLY_PATH = THIS_ASSEMBLY.Location; + private static readonly string THIS_ASSEMBLY_NAME = THIS_ASSEMBLY.GetName().Name; + private static readonly Version THIS_ASSEMBLY_VERSION = THIS_ASSEMBLY.GetName().Version; + private static readonly ExtensionAssembly TEST_EXTENSION_ASSEMBLY = + new ExtensionAssembly(THIS_ASSEMBLY_PATH, false, THIS_ASSEMBLY_NAME, THIS_ASSEMBLY_VERSION); + + private ExtensionAssemblyTracker _tracker; + + [SetUp] + public void CreateTracker() + { + _tracker = new ExtensionAssemblyTracker(); + } + + [Test] + public void AddToList() + { + _tracker.Add(TEST_EXTENSION_ASSEMBLY); + + Assert.That(_tracker.Count, Is.EqualTo(1)); + Assert.That(_tracker[0].FilePath, Is.EqualTo(THIS_ASSEMBLY_PATH)); + Assert.That(_tracker[0].AssemblyName, Is.EqualTo(THIS_ASSEMBLY_NAME)); + Assert.That(_tracker[0].AssemblyVersion, Is.EqualTo(THIS_ASSEMBLY_VERSION)); + } + + [Test] + public void AddUpdatesNameIndex() + { + _tracker.Add(TEST_EXTENSION_ASSEMBLY); + + Assert.That(_tracker.ByName.ContainsKey(THIS_ASSEMBLY_NAME)); + Assert.That(_tracker.ByName[THIS_ASSEMBLY_NAME].AssemblyName, Is.EqualTo(THIS_ASSEMBLY_NAME)); + Assert.That(_tracker.ByName[THIS_ASSEMBLY_NAME].FilePath, Is.EqualTo(THIS_ASSEMBLY_PATH)); + Assert.That(_tracker.ByName[THIS_ASSEMBLY_NAME].AssemblyVersion, Is.EqualTo(THIS_ASSEMBLY_VERSION)); + } + [Test] + public void AddUpdatesPathIndex() + { + _tracker.Add(TEST_EXTENSION_ASSEMBLY); + + Assert.That(_tracker.ByPath.ContainsKey(THIS_ASSEMBLY_PATH)); + Assert.That(_tracker.ByPath[THIS_ASSEMBLY_PATH].AssemblyName, Is.EqualTo(THIS_ASSEMBLY_NAME)); + Assert.That(_tracker.ByPath[THIS_ASSEMBLY_PATH].FilePath, Is.EqualTo(THIS_ASSEMBLY_PATH)); + Assert.That(_tracker.ByPath[THIS_ASSEMBLY_PATH].AssemblyVersion, Is.EqualTo(THIS_ASSEMBLY_VERSION)); + } + + [Test] + public void AddDuplicatePathThrowsArgumentException() + { + _tracker.Add(TEST_EXTENSION_ASSEMBLY); + + Assert.That(() => + _tracker.Add(TEST_EXTENSION_ASSEMBLY), + Throws.TypeOf()); + } + + [Test] + public void AddDuplicateAssemblyNameThrowsArgumentException() + { + _tracker.Add(TEST_EXTENSION_ASSEMBLY); + + Assert.That(() => _tracker.Add(new ExtensionAssembly("Some/Other/Path", false, THIS_ASSEMBLY_NAME, THIS_ASSEMBLY_VERSION)), + Throws.TypeOf()); + } + } +} diff --git a/src/NUnitEngine/nunit.engine.core.tests/Services/ExtensionServiceTests.cs b/src/NUnitEngine/nunit.engine.core.tests/Services/ExtensionServiceTests.cs index 9b8f3bc21..19263fe9d 100644 --- a/src/NUnitEngine/nunit.engine.core.tests/Services/ExtensionServiceTests.cs +++ b/src/NUnitEngine/nunit.engine.core.tests/Services/ExtensionServiceTests.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Reflection; using System.Text; @@ -57,7 +58,20 @@ public void StartServiceInitializesExtensionManager() _extensionService.StartService(); _extensionManager.ReceivedWithAnyArgs().FindExtensionPoints(typeof(ExtensionService).Assembly, typeof(ITestEngine).Assembly); - _extensionManager.Received().FindStandardExtensions(hostAssembly); + _extensionManager.Received().FindExtensionAssemblies(hostAssembly); + Assert.That(_extensionService.Status, Is.EqualTo(ServiceStatus.Started)); + } + + [Test] + public void StartServiceInitializesExtensionManagerUsingAdditionalDirectories() + { + Assembly hostAssembly = typeof(ExtensionService).Assembly; + _extensionService.StartService(); + + var tempPath = Path.GetTempPath(); + _extensionService.FindExtensionAssemblies(tempPath); + + _extensionManager.Received().FindExtensionAssemblies(tempPath); Assert.That(_extensionService.Status, Is.EqualTo(ServiceStatus.Started)); } diff --git a/src/NUnitEngine/nunit.engine.core/Extensibility/ExtensionAssembly.cs b/src/NUnitEngine/nunit.engine.core/Extensibility/ExtensionAssembly.cs index 0cf2e3747..774f10da5 100644 --- a/src/NUnitEngine/nunit.engine.core/Extensibility/ExtensionAssembly.cs +++ b/src/NUnitEngine/nunit.engine.core/Extensibility/ExtensionAssembly.cs @@ -1,6 +1,7 @@ // Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt using System; +using System.Collections.Generic; using System.IO; using TestCentric.Metadata; using NUnit.Engine.Internal; @@ -14,26 +15,26 @@ public ExtensionAssembly(string filePath, bool fromWildCard) FilePath = filePath; FromWildCard = fromWildCard; Assembly = GetAssemblyDefinition(); + AssemblyName = Assembly.Name.Name; + AssemblyVersion = Assembly.Name.Version; + } + + // Internal constructor used for certain tests. AssemblyDefinition is not initialized. + internal ExtensionAssembly(string filePath, bool fromWildCard, string assemblyName, Version version) + { + FilePath = filePath; + FromWildCard = fromWildCard; + AssemblyName = assemblyName; + AssemblyVersion = version; } public string FilePath { get; } public bool FromWildCard { get; } public AssemblyDefinition Assembly { get; } - public string AssemblyName - { - get { return Assembly.Name.Name; } - } - - public Version AssemblyVersion - { - get { return Assembly.Name.Version; } - } + public string AssemblyName { get; } - public ModuleDefinition MainModule - { - get { return Assembly.MainModule; } - } + public Version AssemblyVersion { get; } #if NETFRAMEWORK public RuntimeFramework TargetFramework diff --git a/src/NUnitEngine/nunit.engine.core/Internal/ExtensionAssemblyTracker.cs b/src/NUnitEngine/nunit.engine.core/Internal/ExtensionAssemblyTracker.cs new file mode 100644 index 000000000..96b3c153d --- /dev/null +++ b/src/NUnitEngine/nunit.engine.core/Internal/ExtensionAssemblyTracker.cs @@ -0,0 +1,25 @@ +// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt + +using NUnit.Engine.Extensibility; +using System.Collections.Generic; + +namespace NUnit.Engine.Internal +{ + /// + /// This is a simple utility class used by the ExtensionManager to keep track of ExtensionAssemblies. + /// It is a List of ExtensionAssemblies and also provides indices by file path and assembly name. + /// It allows writing tests to show that no duplicate extension assemblies are loaded. + /// + internal class ExtensionAssemblyTracker : List + { + public Dictionary ByPath = new Dictionary(); + public Dictionary ByName = new Dictionary(); + + public new void Add(ExtensionAssembly assembly) + { + base.Add(assembly); + ByPath.Add(assembly.FilePath, assembly); + ByName.Add(assembly.AssemblyName, assembly); + } + } +} diff --git a/src/NUnitEngine/nunit.engine.core/Services/ExtensionManager.cs b/src/NUnitEngine/nunit.engine.core/Services/ExtensionManager.cs index 5ebd6f0bf..a02d1b410 100644 --- a/src/NUnitEngine/nunit.engine.core/Services/ExtensionManager.cs +++ b/src/NUnitEngine/nunit.engine.core/Services/ExtensionManager.cs @@ -10,10 +10,11 @@ using NUnit.Engine.Internal.FileSystemAccess; using NUnit.Engine.Internal.FileSystemAccess.Default; using System.Linq; +using System.Diagnostics; namespace NUnit.Engine.Services { - public class ExtensionManager : IExtensionManager + public class ExtensionManager { static readonly Version CURRENT_ENGINE_VERSION = Assembly.GetExecutingAssembly().GetName().Version; @@ -23,11 +24,24 @@ public class ExtensionManager : IExtensionManager //private readonly IAddinsFileReader _addinsReader; private readonly IDirectoryFinder _directoryFinder; + // List of all ExtensionPoints discovered private readonly List _extensionPoints = new List(); - private readonly Dictionary _pathIndex = new Dictionary(); - private readonly List _extensions = new List(); - private readonly List _assemblies = new List(); + // Index to ExtensionPoints based on the Path + private readonly Dictionary _extensionPointIndex = new Dictionary(); + + // List of ExtensionNodes for all extensions discovered. + private List _extensions = new List(); + + private bool _extensionsAreLoaded; + + // AssemblyTracker is a List of candidate ExtensionAssemblies, with built-in indexing + // by file path and assembly name, eliminating the need to update indices separately. + private readonly ExtensionAssemblyTracker _assemblies = new ExtensionAssemblyTracker(); + + // List of all extensionDirectories specified on command-line or in environment, + // used to ignore duplicate calls to FindExtensionAssemblies. + private readonly List _extensionDirectories = new List(); public ExtensionManager() : this(new FileSystem()) @@ -47,28 +61,39 @@ internal ExtensionManager(IFileSystem fileSystem, IDirectoryFinder directoryFind #region IExtensionManager Implementation - /// + /// + /// Gets an enumeration of all ExtensionPoints in the engine. + /// public IEnumerable ExtensionPoints { get { return _extensionPoints.ToArray(); } } - /// + /// + /// Gets an enumeration of all installed Extensions. + /// public IEnumerable Extensions { - get { return _extensions.ToArray(); } + get + { + EnsureExtensionsAreLoaded(); + + return _extensions.ToArray(); + } } - /// + /// + /// Find the extension points in a loaded assembly. + /// public virtual void FindExtensionPoints(params Assembly[] targetAssemblies) { foreach (var assembly in targetAssemblies) { - log.Info("Scanning {0} assembly for extension points", assembly.GetName().Name); + log.Info("FindExtensionPoints scanning {0} assembly", assembly.GetName().Name); foreach (ExtensionPointAttribute attr in assembly.GetCustomAttributes(typeof(ExtensionPointAttribute), false)) { - if (_pathIndex.ContainsKey(attr.Path)) + if (_extensionPointIndex.ContainsKey(attr.Path)) { string msg = string.Format( "The Path {0} is already in use for another extension point.", @@ -82,9 +107,9 @@ public virtual void FindExtensionPoints(params Assembly[] targetAssemblies) }; _extensionPoints.Add(ep); - _pathIndex.Add(ep.Path, ep); + _extensionPointIndex.Add(ep.Path, ep); - log.Info(" Found Path={0}, Type={1}", ep.Path, ep.TypeName); + log.Info(" Found ExtensionPoint: Path={0}, Type={1}", ep.Path, ep.TypeName); } foreach (Type type in assembly.GetExportedTypes()) @@ -93,7 +118,7 @@ public virtual void FindExtensionPoints(params Assembly[] targetAssemblies) { string path = attr.Path ?? "/NUnit/Engine/TypeExtensions/" + type.Name; - if (_pathIndex.ContainsKey(path)) + if (_extensionPointIndex.ContainsKey(path)) { string msg = string.Format( "The Path {0} is already in use for another extension point.", @@ -107,74 +132,115 @@ public virtual void FindExtensionPoints(params Assembly[] targetAssemblies) }; _extensionPoints.Add(ep); - _pathIndex.Add(path, ep); + _extensionPointIndex.Add(path, ep); - log.Info(" Found Path={0}, Type={1}", ep.Path, ep.TypeName); + log.Info(" Found ExtensionPoint: Path={0}, Type={1}", ep.Path, ep.TypeName); } } } } - /// - public void FindExtensions(string startDir) + /// + /// Find extension assemblies starting from a given base directory, + /// and using the contained '.addins' files to direct the search. + /// + /// Path to the initial directory. + public void FindExtensionAssemblies(string startDir) { - // Create the list of possible extension assemblies, - // eliminating duplicates, start in the provided directory. - FindExtensionAssemblies(_fileSystem.GetDirectory(startDir)); + // Ignore a call for a directory we have already used + if (!_extensionDirectories.Contains(startDir)) + { + _extensionDirectories.Add(startDir); - // Check each assembly to see if it contains extensions - foreach (var candidate in _assemblies) - FindExtensionsInAssembly(candidate); + log.Info($"FindExtensionAssemblies examining extension directory {startDir}"); + + // Create the list of possible extension assemblies, + // eliminating duplicates, start in the provided directory. + // In this top level directory, we only look at .addins files. + ProcessAddinsFiles(_fileSystem.GetDirectory(startDir), false); + } } - /// - public void FindStandardExtensions(Assembly hostAssembly) + /// + /// Find ExtensionAssemblies for a host assembly using + /// a built-in algorithm that searches in certain known locations. + /// + /// An assembly that supports NUnit extensions. + public void FindExtensionAssemblies(Assembly hostAssembly) { + log.Info($"FindExtensionAssemblies called for host {hostAssembly.FullName}"); + bool isChocolateyPackage = System.IO.File.Exists(Path.Combine(hostAssembly.Location, "VERIFICATION.txt")); string[] extensionPatterns = isChocolateyPackage ? new[] { "nunit-extension-*/**/tools/", "nunit-extension-*/**/tools/*/" } : new[] { "NUnit.Extension.*/**/tools/", "NUnit.Extension.*/**/tools/*/" }; - FindExtensionAssemblies(hostAssembly, extensionPatterns); - foreach (var candidate in _assemblies) - FindExtensionsInAssembly(candidate); + IDirectory startDir = _fileSystem.GetDirectory(AssemblyHelper.GetDirectoryName(hostAssembly)); + + while (startDir != null) + { + foreach (var pattern in extensionPatterns) + foreach (var dir in _directoryFinder.GetDirectories(startDir, pattern)) + ProcessDirectory(dir, true); + + startDir = startDir.Parent; + } } - /// + /// + /// Get an ExtensionPoint based on its unique identifying path. + /// public IExtensionPoint GetExtensionPoint(string path) { - return _pathIndex.ContainsKey(path) ? _pathIndex[path] : null; + return _extensionPointIndex.TryGetValue(path, out ExtensionPoint ep) ? ep : null; } - /// + /// + /// Get extension objects for all nodes of a given type + /// public IEnumerable GetExtensions() { foreach (var node in GetExtensionNodes()) - yield return (T)((ExtensionNode)node).ExtensionObject; // HACK + yield return (T)node.ExtensionObject; } - /// + /// + /// Get all ExtensionNodes for a path + /// public IEnumerable GetExtensionNodes(string path) { + EnsureExtensionsAreLoaded(); + var ep = GetExtensionPoint(path); if (ep != null) foreach (var node in ep.Extensions) yield return node; } - /// + /// + /// Get the first or only ExtensionNode for a given ExtensionPoint + /// + /// The identifying path for an ExtensionPoint + /// public IExtensionNode GetExtensionNode(string path) { + EnsureExtensionsAreLoaded(); + // TODO: Remove need for the cast var ep = GetExtensionPoint(path) as ExtensionPoint; return ep != null && ep.Extensions.Count > 0 ? ep.Extensions[0] : null; } - /// + /// + /// Get all extension nodes of a given Type. + /// + /// If true, disabled nodes are included public IEnumerable GetExtensionNodes(bool includeDisabled = false) { + EnsureExtensionsAreLoaded(); + var ep = GetExtensionPoint(typeof(T)); if (ep != null) foreach (var node in ep.Extensions) @@ -182,9 +248,13 @@ public IEnumerable GetExtensionNodes(bool includeDisabled = fa yield return node; } - /// + /// + /// Enable or disable an extension + /// public void EnableExtension(string typeName, bool enabled) { + EnsureExtensionsAreLoaded(); + foreach (var node in _extensions) if (node.TypeName == typeName) node.Enabled = enabled; @@ -243,34 +313,18 @@ private ExtensionPoint DeduceExtensionPointFromType(TypeReference typeRef) } /// - /// Find candidate extension assemblies starting from a given base directory - /// and using the contained '.addins' files to direct the search. - /// - /// Path to the initial directory. - private void FindExtensionAssemblies(IDirectory initialDirectory) - { - // Since no patterns are provided, look for addins files - ProcessAddinsFiles(initialDirectory, false); - } - - /// - /// Find candidate extension assemblies for a given host assembly, - /// using the provided list of patterns to direct the search using - /// a built-in algorithm. + /// We can only load extensions after all candidate assemblies are identified. + /// This method is called internally to ensure the load happens before any + /// extensions are used. /// - /// An assembly that supports extensions - /// A list of patterns used to identify potential candidates. - private void FindExtensionAssemblies(Assembly hostAssembly, string[] patterns) + private void EnsureExtensionsAreLoaded() { - IDirectory startDir = _fileSystem.GetDirectory(AssemblyHelper.GetDirectoryName(hostAssembly)); - - while (startDir != null) - { - foreach (var pattern in patterns) - foreach (var dir in _directoryFinder.GetDirectories(startDir, pattern)) - ProcessDirectory(dir, true); + if (!_extensionsAreLoaded) + { + _extensionsAreLoaded = true; - startDir = startDir.Parent; + foreach (var candidate in _assemblies) + FindExtensionsInAssembly(candidate); } } @@ -282,23 +336,18 @@ private void FindExtensionAssemblies(Assembly hostAssembly, string[] patterns) private void ProcessDirectory(IDirectory startDir, bool fromWildCard) { var directoryName = startDir.FullName; - if (WasVisited(startDir.FullName, fromWildCard)) + if (WasVisited(directoryName, fromWildCard)) { log.Warning($"Skipping directory '{directoryName}' because it was already visited."); + return; } - else - { - log.Info($"Scanning directory '{directoryName}' for extensions."); - MarkAsVisited(directoryName, fromWildCard); - if (ProcessAddinsFiles(startDir, fromWildCard) == 0) - { - foreach (var file in startDir.GetFiles("*.dll")) - { - ProcessCandidateAssembly(file.FullName, true); - } - } - } + log.Info($"Scanning directory '{directoryName}' for extensions."); + Visit(directoryName, fromWildCard); + + if (ProcessAddinsFiles(startDir, fromWildCard) == 0) + foreach (var file in startDir.GetFiles("*.dll")) + ProcessCandidateAssembly(file.FullName, true); } /// @@ -363,32 +412,30 @@ private void ProcessAddinsFile(IFile addinsFile, bool fromWildCard) private void ProcessCandidateAssembly(string filePath, bool fromWildCard) { - if (WasVisited(filePath, fromWildCard)) + // Did we already process this file? + if (_assemblies.ByPath.ContainsKey(filePath)) return; - MarkAsVisited(filePath, fromWildCard); - try { - var candidate = new ExtensionAssembly(filePath, fromWildCard); + var candidateAssembly = new ExtensionAssembly(filePath, fromWildCard); + string assemblyName = candidateAssembly.AssemblyName; - if (!CanLoadTargetFramework(Assembly.GetEntryAssembly(), candidate)) + // We never add assemblies unless the host can load them + if (!CanLoadTargetFramework(Assembly.GetEntryAssembly(), candidateAssembly)) return; - - for (var i = 0; i < _assemblies.Count; i++) + + // Do we already have a copy of the same assembly at a different path? + //if (_assemblies.ByName.ContainsKey(assemblyName)) + if (_assemblies.ByName.TryGetValue(assemblyName, out ExtensionAssembly existing)) { - var assembly = _assemblies[i]; + if (candidateAssembly.IsBetterVersionOf(existing)) + _assemblies.ByName[assemblyName] = candidateAssembly; - if (candidate.IsDuplicateOf(assembly)) - { - if (candidate.IsBetterVersionOf(assembly)) - _assemblies[i] = candidate; - - return; - } + return; } - _assemblies.Add(candidate); + _assemblies.Add(candidateAssembly); } catch (BadImageFormatException e) { @@ -402,16 +449,17 @@ private void ProcessCandidateAssembly(string filePath, bool fromWildCard) } } + // Dictionary containing all directory paths already visited. private readonly Dictionary _visited = new Dictionary(); - private bool WasVisited(string filePath, bool fromWildcard) + private bool WasVisited(string path, bool fromWildcard) { - return _visited.ContainsKey($"path={ filePath }_visited={fromWildcard}"); + return _visited.ContainsKey($"path={path}_visited={fromWildcard}"); } - private void MarkAsVisited(string filePath, bool fromWildcard) + private void Visit(string path, bool fromWildcard) { - _visited.Add($"path={ filePath }_visited={fromWildcard}", null); + _visited.Add($"path={path}_visited={fromWildcard}", null); } /// @@ -419,35 +467,35 @@ private void MarkAsVisited(string filePath, bool fromWildcard) /// For each extension, create an ExtensionNode and link it to the /// correct ExtensionPoint. Public for testing. /// - internal void FindExtensionsInAssembly(ExtensionAssembly assembly) + internal void FindExtensionsInAssembly(ExtensionAssembly extensionAssembly) { - log.Info($"Scanning {assembly.FilePath} for Extensions"); + log.Info($"Scanning {extensionAssembly.FilePath} for Extensions"); - if (!CanLoadTargetFramework(Assembly.GetEntryAssembly(), assembly)) + if (!CanLoadTargetFramework(Assembly.GetEntryAssembly(), extensionAssembly)) { - log.Info($"{assembly.FilePath} cannot be loaded on this runtime"); + log.Info($"{extensionAssembly.FilePath} cannot be loaded on this runtime"); return; } IRuntimeFramework assemblyTargetFramework = null; #if NETFRAMEWORK var currentFramework = RuntimeFramework.CurrentFramework; - assemblyTargetFramework = assembly.TargetFramework; + assemblyTargetFramework = extensionAssembly.TargetFramework; if (!currentFramework.CanLoad(assemblyTargetFramework)) { - if (!assembly.FromWildCard) + if (!extensionAssembly.FromWildCard) { - throw new NUnitEngineException($"Extension {assembly.FilePath} targets {assemblyTargetFramework.DisplayName}, which is not available."); + throw new NUnitEngineException($"Extension {extensionAssembly.FilePath} targets {assemblyTargetFramework.DisplayName}, which is not available."); } else { - log.Info($"Assembly {assembly.FilePath} targets {assemblyTargetFramework.DisplayName}, which is not available. Assembly found via wildcard."); + log.Info($"Assembly {extensionAssembly.FilePath} targets {assemblyTargetFramework.DisplayName}, which is not available. Assembly found via wildcard."); return; } } #endif - foreach (var extensionType in assembly.MainModule.GetTypes()) + foreach (var extensionType in extensionAssembly.Assembly.MainModule.GetTypes()) { CustomAttribute extensionAttr = extensionType.GetAttribute("NUnit.Engine.Extensibility.ExtensionAttribute"); @@ -467,7 +515,7 @@ internal void FindExtensionsInAssembly(ExtensionAssembly assembly) } } - var node = new ExtensionNode(assembly.FilePath, assembly.AssemblyVersion, extensionType.FullName, assemblyTargetFramework) + var node = new ExtensionNode(extensionAssembly.FilePath, extensionAssembly.AssemblyVersion, extensionType.FullName, assemblyTargetFramework) { Path = extensionAttr.GetNamedArgument("Path") as string, Description = extensionAttr.GetNamedArgument("Description") as string @@ -612,9 +660,9 @@ private System.Runtime.Versioning.FrameworkName GetTargetRuntime(string filePath public void Dispose() { // Make sure all assemblies release the underlying file streams. - foreach (var assembly in _assemblies) + foreach (var candidate in _assemblies) { - assembly.Dispose(); + candidate.Dispose(); } } } diff --git a/src/NUnitEngine/nunit.engine.core/Services/ExtensionService.cs b/src/NUnitEngine/nunit.engine.core/Services/ExtensionService.cs index 7459ed8ca..d4f55929a 100644 --- a/src/NUnitEngine/nunit.engine.core/Services/ExtensionService.cs +++ b/src/NUnitEngine/nunit.engine.core/Services/ExtensionService.cs @@ -1,14 +1,10 @@ // Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt -using System; -using System.Collections.Generic; -using System.Reflection; -using TestCentric.Metadata; using NUnit.Engine.Extensibility; using NUnit.Engine.Internal; using NUnit.Engine.Internal.FileSystemAccess; -using NUnit.Engine.Internal.FileSystemAccess.Default; -using System.IO; +using System.Collections.Generic; +using System.Reflection; namespace NUnit.Engine.Services { @@ -19,7 +15,7 @@ namespace NUnit.Engine.Services /// public class ExtensionService : Service, IExtensionService { - private readonly IExtensionManager _extensionManager; + private readonly ExtensionManager _extensionManager; public ExtensionService() { @@ -42,32 +38,41 @@ internal ExtensionService(IFileSystem fileSystem, IDirectoryFinder directoryFind _extensionManager = new ExtensionManager(fileSystem, directoryFinder); } + #region IExtensionService Implementation + + /// public IEnumerable ExtensionPoints => _extensionManager.ExtensionPoints; + /// public IEnumerable Extensions => _extensionManager.Extensions; - /// - /// Get an ExtensionPoint based on its unique identifying path. - /// + /// + public void FindExtensionAssemblies(string initialDirectory) + { + _extensionManager.FindExtensionAssemblies(initialDirectory); + } + + /// IExtensionPoint IExtensionService.GetExtensionPoint(string path) { return _extensionManager.GetExtensionPoint(path); } - /// - /// Get an enumeration of ExtensionNodes based on their identifying path. - /// + /// IEnumerable IExtensionService.GetExtensionNodes(string path) { foreach (var node in _extensionManager.GetExtensionNodes(path)) yield return node; } + /// public void EnableExtension(string typeName, bool enabled) { _extensionManager.EnableExtension(typeName, enabled); } + #endregion + public IEnumerable GetExtensions() => _extensionManager.GetExtensions(); public IExtensionNode GetExtensionNode(string path) => _extensionManager.GetExtensionNode(path); @@ -78,11 +83,11 @@ public override void StartService() { Assembly thisAssembly = Assembly.GetExecutingAssembly(); Assembly apiAssembly = typeof(ITestEngine).Assembly; - + try { _extensionManager.FindExtensionPoints(thisAssembly, apiAssembly); - _extensionManager.FindStandardExtensions(thisAssembly); + _extensionManager.FindExtensionAssemblies(thisAssembly); Status = ServiceStatus.Started; } diff --git a/src/NUnitEngine/nunit.engine.core/Services/IExtensionManager.cs b/src/NUnitEngine/nunit.engine.core/Services/IExtensionManager.cs deleted file mode 100644 index e9c7858b3..000000000 --- a/src/NUnitEngine/nunit.engine.core/Services/IExtensionManager.cs +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt - -using System; -using System.Collections.Generic; -using System.Reflection; - -using NUnit.Engine.Extensibility; - -namespace NUnit.Engine.Services -{ - public interface IExtensionManager : IDisposable - { - /// - /// Gets an enumeration of all ExtensionPoints in the engine. - /// - IEnumerable ExtensionPoints { get; } - - /// - /// Gets an enumeration of all installed Extensions. - /// - IEnumerable Extensions { get; } - - /// - /// Find the extension points in a loaded assembly. - /// - void FindExtensionPoints(params Assembly[] targetAssemblies); - - /// - /// Find and install extensions starting from a given base directory, - /// and using the contained '.addins' files to direct the search. - /// - /// Path to the initial directory. - void FindExtensions(string initialDirectory); - - /// - /// Find and install standard extensions for a host assembly using - /// a built-in algorithm that searches in certain known locations. - /// - /// An assembly that supports NUnit extensions. - void FindStandardExtensions(Assembly hostAssembly); - - /// - /// Get an ExtensionPoint based on its unique identifying path. - /// - IExtensionPoint GetExtensionPoint(string path); - - /// - /// Get extension objects for all nodes of a given type - /// - IEnumerable GetExtensions(); - - /// - /// Get all ExtensionNodes for a give path - /// - IEnumerable GetExtensionNodes(string path); - - /// - /// Get the first or only ExtensionNode for a given ExtensionPoint - /// - /// The identifying path for an ExtensionPoint - /// - IExtensionNode GetExtensionNode(string path); - - /// - /// Get all extension nodes of a given Type. - /// - /// If true, disabled nodes are included - IEnumerable GetExtensionNodes(bool includeDisabled = false); - - /// - /// Enable or disable an extension - /// - void EnableExtension(string typeName, bool enabled); - } - -}