Skip to content

Commit

Permalink
Add support for additional extension directories
Browse files Browse the repository at this point in the history
  • Loading branch information
CharliePoole committed Dec 7, 2024
1 parent 037b828 commit 4330169
Show file tree
Hide file tree
Showing 11 changed files with 159 additions and 39 deletions.
7 changes: 7 additions & 0 deletions src/NUnitConsole/nunit3-console.tests/CommandLineTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
4 changes: 2 additions & 2 deletions src/NUnitConsole/nunit3-console.tests/ConsoleRunnerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ public void ThrowsNUnitEngineExceptionWhenTestResultsAreNotWriteable()
}

[Test]
public void ThrowsNUnitExceptionWhenTeamcityOptionIsSpecifiedButNotAvailable()
public void ThrowsRequiredExtensionExceptionWhenTeamcityOptionIsSpecifiedButNotAvailable()
{
var ex = Assert.Throws<NUnitEngineException>(
var ex = Assert.Throws<RequiredExtensionException>(
() => new ConsoleRunner(_testEngine, ConsoleMocks.Options("mock-assembly.dll", "--teamcity"), new ColorConsoleWriter()));

Assert.That(ex, Has.Message.Contains("teamcity"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
12 changes: 9 additions & 3 deletions src/NUnitConsole/nunit3-console/ConsoleOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,19 @@ 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; }

public bool ShowHelp { get; private set; }

public bool ShowVersion { get; private set; }

public bool ListExtensions { get; private set; }

// Additional directories to be used to search for user extensions
public IList<string> ExtensionDirectories { get; } = new List<string>();

// Select tests

public IList<string> InputFiles { get; } = new List<string>();
Expand Down Expand Up @@ -153,8 +158,6 @@ public IList<OutputSpecification> 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; }
Expand Down Expand Up @@ -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")));

Expand Down
47 changes: 35 additions & 12 deletions src/NUnitConsole/nunit3-console/ConsoleRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<IResultService>();
Guard.OperationValid(_resultService != null, "Internal Error: ResultService was not found");

_filterService = _engine.Services.GetService<ITestFilterService>();
Guard.OperationValid(_filterService != null, "Internal Error: TestFilterService was not found");

_extensionService = _engine.Services.GetService<IExtensionService>();
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
Expand Down Expand Up @@ -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])
Expand Down
5 changes: 5 additions & 0 deletions src/NUnitConsole/nunit3-console/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
47 changes: 47 additions & 0 deletions src/NUnitConsole/nunit3-console/RequiredExtensionException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt

using System;

namespace NUnit.ConsoleRunner
{
/// <summary>
/// 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.
/// </summary>
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.";

/// <summary>
/// Construct with the name of an extension
/// </summary>
public RequiredExtensionException(string extensionName) : base(Message1(extensionName))
{
}

/// <summary>
/// Construct with the name of an extension and the command-line option requiring that extension.
/// </summary>
public RequiredExtensionException(string extensionName, string option) : base(Message2(extensionName, option))
{
}

/// <summary>
/// Construct with the name of an extension and inner exception
/// </summary>
public RequiredExtensionException(string extensionName, Exception innerException)
: base(Message1(extensionName), innerException)
{
}

/// <summary>
/// Construct with the name of an extension, a command-line option and inner exception
/// </summary>
public RequiredExtensionException(string extensionName, string option, Exception innerException)
: base(Message2(extensionName, option), innerException)
{
}
}
}
9 changes: 7 additions & 2 deletions src/NUnitEngine/nunit.engine.api/IExtensionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ public interface IExtensionService
/// </summary>
IEnumerable<IExtensionNode> Extensions { get; }

/// <summary>
/// Find and install extensions starting from a given base directory,
/// and using the contained '.addins' files to direct the search.
/// </summary>
/// <param name="initialDirectory">Path to the initial directory.</param>
void FindExtensions(string initialDirectory);

/// <summary>
/// Get an ExtensionPoint based on its unique identifying path.
/// </summary>
Expand All @@ -34,8 +41,6 @@ public interface IExtensionService
/// <summary>
/// Enable or disable an extension
/// </summary>
/// <param name="typeName"></param>
/// <param name="enabled"></param>
void EnableExtension(string typeName, bool enabled);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
Expand Down Expand Up @@ -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()
{
Expand Down
20 changes: 14 additions & 6 deletions src/NUnitEngine/nunit.engine.core/Services/ExtensionManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ public class ExtensionManager : IExtensionManager
private readonly List<ExtensionNode> _extensions = new List<ExtensionNode>();
private readonly List<ExtensionAssembly> _assemblies = new List<ExtensionAssembly>();

private readonly List<string> _extensionDirectories = new List<string>();

public ExtensionManager()
: this(new FileSystem())
{
Expand Down Expand Up @@ -118,13 +120,19 @@ public virtual void FindExtensionPoints(params Assembly[] targetAssemblies)
/// <inheritdoc/>
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);
}
}

/// <inheritdoc/>
Expand Down
31 changes: 18 additions & 13 deletions src/NUnitEngine/nunit.engine.core/Services/ExtensionService.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand Down Expand Up @@ -42,32 +38,41 @@ internal ExtensionService(IFileSystem fileSystem, IDirectoryFinder directoryFind
_extensionManager = new ExtensionManager(fileSystem, directoryFinder);
}

#region IExtensionService Implementation

/// <inheritdoc/>
public IEnumerable<IExtensionPoint> ExtensionPoints => _extensionManager.ExtensionPoints;

/// <inheritdoc/>
public IEnumerable<IExtensionNode> Extensions => _extensionManager.Extensions;

/// <summary>
/// Get an ExtensionPoint based on its unique identifying path.
/// </summary>
/// <inheritdoc/>
public void FindExtensions(string initialDirectory)
{
_extensionManager.FindExtensions(initialDirectory);
}

/// <inheritdoc/>
IExtensionPoint IExtensionService.GetExtensionPoint(string path)
{
return _extensionManager.GetExtensionPoint(path);
}

/// <summary>
/// Get an enumeration of ExtensionNodes based on their identifying path.
/// </summary>
/// <inheritdoc/>
IEnumerable<IExtensionNode> IExtensionService.GetExtensionNodes(string path)
{
foreach (var node in _extensionManager.GetExtensionNodes(path))
yield return node;
}

/// <inheritdoc/>
public void EnableExtension(string typeName, bool enabled)
{
_extensionManager.EnableExtension(typeName, enabled);
}

#endregion

public IEnumerable<T> GetExtensions<T>() => _extensionManager.GetExtensions<T>();

public IExtensionNode GetExtensionNode(string path) => _extensionManager.GetExtensionNode(path);
Expand All @@ -78,7 +83,7 @@ public override void StartService()
{
Assembly thisAssembly = Assembly.GetExecutingAssembly();
Assembly apiAssembly = typeof(ITestEngine).Assembly;

try
{
_extensionManager.FindExtensionPoints(thisAssembly, apiAssembly);
Expand Down

0 comments on commit 4330169

Please sign in to comment.