diff --git a/src/NUnitConsole/nunit3-console.tests/CommandLineTests.cs b/src/NUnitConsole/nunit3-console.tests/CommandLineTests.cs index cf69e7c5e..1bdcc2470 100644 --- a/src/NUnitConsole/nunit3-console.tests/CommandLineTests.cs +++ b/src/NUnitConsole/nunit3-console.tests/CommandLineTests.cs @@ -859,6 +859,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..e5441f89a 100644 --- a/src/NUnitConsole/nunit3-console/ConsoleOptions.cs +++ b/src/NUnitConsole/nunit3-console/ConsoleOptions.cs @@ -41,7 +41,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,6 +49,11 @@ 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 IList ExtensionDirectories { get; } = new List(); + // Select tests public IList InputFiles { get; } = new List(); @@ -153,8 +158,6 @@ 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; } @@ -383,6 +386,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"))); diff --git a/src/NUnitConsole/nunit3-console/ConsoleRunner.cs b/src/NUnitConsole/nunit3-console/ConsoleRunner.cs index f06503cec..9f5fe2b79 100644 --- a/src/NUnitConsole/nunit3-console/ConsoleRunner.cs +++ b/src/NUnitConsole/nunit3-console/ConsoleRunner.cs @@ -26,7 +26,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 +51,40 @@ 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)); _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"); + + 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); - // TODO: Exit with error if any of the services are not found + foreach (string extensionDirectory in _options.ExtensionDirectories) + _extensionService.FindExtensions(extensionDirectory); + + _workDirectory = options.WorkDirectory ?? Directory.GetCurrentDirectory(); + if (!Directory.Exists(_workDirectory)) + Directory.CreateDirectory(_workDirectory); 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."); + //Guard.ArgumentValid(teamcityInstalled, "Option --teamcity specified but the extension is not installed.", "--teamCity"); + if (!teamcityInstalled) + throw new RequiredExtensionException(TEAMCITY_EVENT_LISTENER_NAME, "--teamcity"); } // Enable TeamCityEventListener immediately, before the console is redirected @@ -310,6 +325,14 @@ 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]) 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..2a368be5a 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 FindExtensions(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/Services/ExtensionServiceTests.cs b/src/NUnitEngine/nunit.engine.core.tests/Services/ExtensionServiceTests.cs index 9b8f3bc21..ead9f7a0d 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; @@ -61,6 +62,19 @@ public void StartServiceInitializesExtensionManager() Assert.That(_extensionService.Status, Is.EqualTo(ServiceStatus.Started)); } + [Test] + public void StartServiceInitializesExtensionManagerUsingAdditionalDirectories() + { + Assembly hostAssembly = typeof(ExtensionService).Assembly; + _extensionService.StartService(); + + var tempPath = Path.GetTempPath(); + _extensionService.FindExtensions(tempPath); + + _extensionManager.Received().FindExtensions(tempPath); + Assert.That(_extensionService.Status, Is.EqualTo(ServiceStatus.Started)); + } + [Test] public void GetExtensionPointCallsExtensionManager() { diff --git a/src/NUnitEngine/nunit.engine.core/Services/ExtensionManager.cs b/src/NUnitEngine/nunit.engine.core/Services/ExtensionManager.cs index 5ebd6f0bf..0498d8d27 100644 --- a/src/NUnitEngine/nunit.engine.core/Services/ExtensionManager.cs +++ b/src/NUnitEngine/nunit.engine.core/Services/ExtensionManager.cs @@ -29,6 +29,8 @@ public class ExtensionManager : IExtensionManager private readonly List _extensions = new List(); private readonly List _assemblies = new List(); + private readonly List _extensionDirectories = new List(); + public ExtensionManager() : this(new FileSystem()) { @@ -118,13 +120,19 @@ public virtual void FindExtensionPoints(params Assembly[] targetAssemblies) /// public void FindExtensions(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); + // 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); + } } /// diff --git a/src/NUnitEngine/nunit.engine.core/Services/ExtensionService.cs b/src/NUnitEngine/nunit.engine.core/Services/ExtensionService.cs index 7459ed8ca..ac8142cfb 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 { @@ -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 FindExtensions(string initialDirectory) + { + _extensionManager.FindExtensions(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,7 +83,7 @@ public override void StartService() { Assembly thisAssembly = Assembly.GetExecutingAssembly(); Assembly apiAssembly = typeof(ITestEngine).Assembly; - + try { _extensionManager.FindExtensionPoints(thisAssembly, apiAssembly);