diff --git a/src/NUnitConsole/nunit3-console/ConsoleRunner.cs b/src/NUnitConsole/nunit3-console/ConsoleRunner.cs index bbd1b6484..2f6ffaede 100644 --- a/src/NUnitConsole/nunit3-console/ConsoleRunner.cs +++ b/src/NUnitConsole/nunit3-console/ConsoleRunner.cs @@ -56,6 +56,7 @@ public ConsoleRunner(ITestEngine engine, ConsoleOptions options, ExtendedTextWri Guard.ArgumentNotNull(_options = options, nameof(options)); Guard.ArgumentNotNull(_outWriter = writer, nameof(writer)); + // NOTE: Accessing Services triggerss the engine to initialize all services _resultService = _engine.Services.GetService(); Guard.OperationValid(_resultService != null, "Internal Error: ResultService was not found"); @@ -68,10 +69,14 @@ public ConsoleRunner(ITestEngine engine, ConsoleOptions options, ExtendedTextWri var extensionPath = Environment.GetEnvironmentVariable(NUNIT_EXTENSION_DIRECTORIES); if (!string.IsNullOrEmpty(extensionPath)) foreach (string extensionDirectory in extensionPath.Split(new[] { Path.PathSeparator }, StringSplitOptions.RemoveEmptyEntries)) - _extensionService.FindExtensions(extensionDirectory); + _extensionService.FindExtensionAssemblies(extensionDirectory); foreach (string extensionDirectory in _options.ExtensionDirectories) - _extensionService.FindExtensions(extensionDirectory); + _extensionService.FindExtensionAssemblies(extensionDirectory); + + // Trigger lazy loading of extensions + var dummy = _extensionService.Extensions; + _workDirectory = options.WorkDirectory; if (_workDirectory != null) diff --git a/src/NUnitEngine/nunit.engine.api/IExtensionService.cs b/src/NUnitEngine/nunit.engine.api/IExtensionService.cs index 2a368be5a..6f2cc7f12 100644 --- a/src/NUnitEngine/nunit.engine.api/IExtensionService.cs +++ b/src/NUnitEngine/nunit.engine.api/IExtensionService.cs @@ -26,7 +26,7 @@ public interface IExtensionService /// and using the contained '.addins' files to direct the search. /// /// Path to the initial directory. - void FindExtensions(string initialDirectory); + void FindExtensionAssemblies(string initialDirectory); /// /// Get an ExtensionPoint based on its unique identifying path. diff --git a/src/NUnitEngine/nunit.engine.core.tests/Services/ExtensionServiceTests.cs b/src/NUnitEngine/nunit.engine.core.tests/Services/ExtensionServiceTests.cs index ead9f7a0d..19263fe9d 100644 --- a/src/NUnitEngine/nunit.engine.core.tests/Services/ExtensionServiceTests.cs +++ b/src/NUnitEngine/nunit.engine.core.tests/Services/ExtensionServiceTests.cs @@ -58,7 +58,7 @@ 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)); } @@ -69,9 +69,9 @@ public void StartServiceInitializesExtensionManagerUsingAdditionalDirectories() _extensionService.StartService(); var tempPath = Path.GetTempPath(); - _extensionService.FindExtensions(tempPath); + _extensionService.FindExtensionAssemblies(tempPath); - _extensionManager.Received().FindExtensions(tempPath); + _extensionManager.Received().FindExtensionAssemblies(tempPath); Assert.That(_extensionService.Status, Is.EqualTo(ServiceStatus.Started)); } diff --git a/src/NUnitEngine/nunit.engine.core/Services/ExtensionManager.cs b/src/NUnitEngine/nunit.engine.core/Services/ExtensionManager.cs index bc6795abb..b033414cd 100644 --- a/src/NUnitEngine/nunit.engine.core/Services/ExtensionManager.cs +++ b/src/NUnitEngine/nunit.engine.core/Services/ExtensionManager.cs @@ -23,17 +23,29 @@ public class ExtensionManager //private readonly IAddinsFileReader _addinsReader; private readonly IDirectoryFinder _directoryFinder; + // List of all ExtensionPoints discovered private readonly List _extensionPoints = new List(); + + // Index to ExtensionPoints based on the Path private readonly Dictionary _pathIndex = new Dictionary(); - private readonly List _extensions = new List(); - private readonly List _assemblies = new List(); + // List of ExtensionNodes for all extensions discovered. + private List _extensions = new List(); + + private bool _extensionsAreLoaded; - // List of all extensionDirectories specified on command-line or in environment + // List of candidate ExtensionAssemblies, which should be examined for extensions. + // This list must be completely built before we examine any of the assemblies, since + // it's possible that the same assembly may be found in multiple versions. + private readonly List _candidateAssemblies = new List(); + + // List of all extensionDirectories specified on command-line or in environment, + // used to ignore duplicate calls to FindExtensions. private readonly List _extensionDirectories = new List(); - // Dictionary containing all directory paths already examined - private readonly Dictionary _directoriesExamined = new Dictionary(); + // Dictionary containing all directory paths and assembly paths already processed, + // used to prevent processing a directory or assembly more than once. + private readonly Dictionary _visited = new Dictionary(); public ExtensionManager() : this(new FileSystem()) @@ -53,19 +65,30 @@ 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) @@ -121,30 +144,35 @@ public virtual void FindExtensionPoints(params Assembly[] targetAssemblies) } } - /// - 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) { // Ignore a call for a directory we have already used if (!_extensionDirectories.Contains(startDir)) { _extensionDirectories.Add(startDir); - log.Info($"FindExtensions examining extension directory {startDir}"); + log.Info($"FindExtensionAssemblies examining extension directory {startDir}"); // Create the list of possible extension assemblies, // eliminating duplicates, start in the provided directory. - FindExtensionAssemblies(_fileSystem.GetDirectory(startDir)); - - // Check each assembly to see if it contains extensions - foreach (var candidate in _assemblies) - FindExtensionsInAssembly(candidate); + // 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($"FindStandardExtensions called for host {hostAssembly.FullName}"); + log.Info($"FindExtensionAssemblies called for host {hostAssembly.FullName}"); bool isChocolateyPackage = System.IO.File.Exists(Path.Combine(hostAssembly.Location, "VERIFICATION.txt")); string[] extensionPatterns = isChocolateyPackage @@ -152,45 +180,77 @@ public void FindStandardExtensions(Assembly hostAssembly) : new[] { "NUnit.Extension.*/**/tools/", "NUnit.Extension.*/**/tools/*/" }; FindExtensionAssemblies(hostAssembly, extensionPatterns); + } + + /// + /// 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. + /// + private void EnsureExtensionsAreLoaded() + { + if (!_extensionsAreLoaded) + { + _extensionsAreLoaded = true; - foreach (var candidate in _assemblies) - FindExtensionsInAssembly(candidate); + foreach (var candidate in _candidateAssemblies) + FindExtensionsInAssembly(candidate); + } } - /// + /// + /// Get an ExtensionPoint based on its unique identifying path. + /// public IExtensionPoint GetExtensionPoint(string path) { return _pathIndex.ContainsKey(path) ? _pathIndex[path] : 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) @@ -198,9 +258,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; @@ -258,17 +322,6 @@ private ExtensionPoint DeduceExtensionPointFromType(TypeReference typeRef) : null; } - /// - /// 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 @@ -391,20 +444,20 @@ private void ProcessCandidateAssembly(string filePath, bool fromWildCard) if (!CanLoadTargetFramework(Assembly.GetEntryAssembly(), candidate)) return; - for (var i = 0; i < _assemblies.Count; i++) + for (var i = 0; i < _candidateAssemblies.Count; i++) { - var assembly = _assemblies[i]; + var assembly = _candidateAssemblies[i]; if (candidate.IsDuplicateOf(assembly)) { if (candidate.IsBetterVersionOf(assembly)) - _assemblies[i] = candidate; + _candidateAssemblies[i] = candidate; return; } } - _assemblies.Add(candidate); + _candidateAssemblies.Add(candidate); } catch (BadImageFormatException e) { @@ -418,14 +471,14 @@ private void ProcessCandidateAssembly(string filePath, bool fromWildCard) } } - private bool WasVisited(string filePath, bool fromWildcard) + private bool WasVisited(string path, bool fromWildcard) { - return _directoriesExamined.ContainsKey($"path={filePath}_visited={fromWildcard}"); + return _visited.ContainsKey($"path={path}_visited={fromWildcard}"); } - private void MarkAsVisited(string filePath, bool fromWildcard) + private void MarkAsVisited(string path, bool fromWildcard) { - _directoriesExamined.Add($"path={filePath}_visited={fromWildcard}", null); + _visited.Add($"path={path}_visited={fromWildcard}", null); } /// @@ -626,7 +679,7 @@ 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 assembly in _candidateAssemblies) { assembly.Dispose(); } diff --git a/src/NUnitEngine/nunit.engine.core/Services/ExtensionService.cs b/src/NUnitEngine/nunit.engine.core/Services/ExtensionService.cs index 536ea3974..d4f55929a 100644 --- a/src/NUnitEngine/nunit.engine.core/Services/ExtensionService.cs +++ b/src/NUnitEngine/nunit.engine.core/Services/ExtensionService.cs @@ -47,9 +47,9 @@ internal ExtensionService(IFileSystem fileSystem, IDirectoryFinder directoryFind public IEnumerable Extensions => _extensionManager.Extensions; /// - public void FindExtensions(string initialDirectory) + public void FindExtensionAssemblies(string initialDirectory) { - _extensionManager.FindExtensions(initialDirectory); + _extensionManager.FindExtensionAssemblies(initialDirectory); } /// @@ -87,7 +87,7 @@ public override void StartService() try { _extensionManager.FindExtensionPoints(thisAssembly, apiAssembly); - _extensionManager.FindStandardExtensions(thisAssembly); + _extensionManager.FindExtensionAssemblies(thisAssembly); Status = ServiceStatus.Started; }