diff --git a/Nuclex.Support.csproj b/Nuclex.Support.csproj
index 3d2726a..57a2698 100644
--- a/Nuclex.Support.csproj
+++ b/Nuclex.Support.csproj
@@ -123,16 +123,14 @@
PathHelper.cs
-
-
- AbstractFactory.cs
-
+
Employer.cs
FactoryEmployer.cs
+
InstanceEmployer.cs
diff --git a/Source/Plugins/AbstractFactory.cs b/Source/Plugins/IAbstractFactory.cs
similarity index 100%
rename from Source/Plugins/AbstractFactory.cs
rename to Source/Plugins/IAbstractFactory.cs
diff --git a/Source/Plugins/AbstractFactory.Test.cs b/Source/Plugins/IAssemblyLoader.cs
similarity index 54%
rename from Source/Plugins/AbstractFactory.Test.cs
rename to Source/Plugins/IAssemblyLoader.cs
index ca07177..c8c84d8 100644
--- a/Source/Plugins/AbstractFactory.Test.cs
+++ b/Source/Plugins/IAssemblyLoader.cs
@@ -19,21 +19,22 @@ License along with this library
#endregion
using System;
-using System.IO;
-
-#if UNITTEST
-
-using NUnit.Framework;
-using NUnit.Framework.SyntaxHelpers;
+using System.Collections.Generic;
+using System.Reflection;
namespace Nuclex.Support.Plugins {
- /// Unit Test for the abstract factory interface
- [TestFixture]
- public class AbtractFactoryTest {
+ /// Interface for an assembly loading helper
+ public interface IAssemblyLoader {
+
+ /// Tries to loads an assembly from a file
+ /// Path to the file that is loaded as an assembly
+ ///
+ /// Output parameter that receives the loaded assembly or null
+ ///
+ /// True if the assembly was loaded successfully, otherwise false
+ bool TryLoadFile(string path, out Assembly loadedAssembly);
}
} // namespace Nuclex.Support.Plugins
-
-#endif // UNITTEST
diff --git a/Source/Plugins/PluginHost.Test.cs b/Source/Plugins/PluginHost.Test.cs
index 503d5a3..63e1fa7 100644
--- a/Source/Plugins/PluginHost.Test.cs
+++ b/Source/Plugins/PluginHost.Test.cs
@@ -20,6 +20,7 @@ License along with this library
using System;
using System.IO;
+using System.Reflection;
#if UNITTEST
@@ -29,8 +30,133 @@ using NUnit.Framework.SyntaxHelpers;
namespace Nuclex.Support.Plugins {
/// Unit Test for the plugin host class
- [TestFixture]
+ [TestFixture, NoPlugin] // NoPlugin is used in one of the unit tests
public class PluginHostTest {
+
+ #region class FailingEmployer
+
+ /// Employer that unexpectedly fails to employ a given type
+ private class FailingEmployer : Employer {
+
+ /// Employs the specified plugin type
+ /// Type to be employed
+ public override void Employ(Type type) {
+ if(type.Equals(typeof(PluginRepository))) {
+ throw new InvalidOperationException();
+ }
+ }
+
+ }
+
+ #endregion // class FailingEmployer
+
+ /// Tests whether the simple constructor is working
+ [Test]
+ public void TestSimpleConstructor() {
+ new PluginHost(new FactoryEmployer());
+ }
+
+ /// Tests whether the full constructor is working
+ [Test]
+ public void TestFullConstructor() {
+ new PluginHost(new FactoryEmployer(), new PluginRepository());
+ }
+
+ ///
+ /// Tests whether the AddAssembly() method works by adding the test assembly
+ /// itself to the repository
+ ///
+ [Test]
+ public void TestFullConstructorWithPreloadedAssembly() {
+ PluginRepository testRepository = new PluginRepository();
+ FactoryEmployer testEmployer = new FactoryEmployer();
+
+ // Might also use Assembly.GetCallingAssembly() here, but this leads to the exe of
+ // the unit testing tool
+ Assembly self = Assembly.GetAssembly(GetType());
+ testRepository.AddAssembly(self);
+
+ PluginHost testHost = new PluginHost(testEmployer, testRepository);
+
+ Assert.AreEqual(1, testEmployer.Factories.Count);
+ }
+
+ ///
+ /// Verifies that the plugin host correctly stores the provided repository
+ ///
+ [Test]
+ public void TestRepositoryStorage() {
+ PluginRepository testRepository = new PluginRepository();
+ FactoryEmployer testEmployer = new FactoryEmployer();
+ PluginHost testHost = new PluginHost(testEmployer, testRepository);
+
+ Assert.AreSame(testRepository, testHost.Repository);
+ }
+
+ ///
+ /// Verifies that the plugin host correctly stores the provided employer
+ ///
+ [Test]
+ public void TestEmployerStorage() {
+ PluginRepository testRepository = new PluginRepository();
+ FactoryEmployer testEmployer = new FactoryEmployer();
+ PluginHost testHost = new PluginHost(testEmployer, testRepository);
+
+ Assert.AreSame(testEmployer, testHost.Employer);
+ }
+
+ ///
+ /// Tests whether the plugin host noticed when new assemblies are loaded into
+ /// the repository
+ ///
+ [Test]
+ public void TestAssemblyLoading() {
+ PluginRepository testRepository = new PluginRepository();
+ FactoryEmployer testEmployer = new FactoryEmployer();
+
+ PluginHost testHost = new PluginHost(testEmployer, testRepository);
+
+ // Might also use Assembly.GetCallingAssembly() here, but this leads to the exe of
+ // the unit testing tool
+ Assembly self = Assembly.GetAssembly(GetType());
+ testRepository.AddAssembly(self);
+
+ Assert.AreEqual(1, testEmployer.Factories.Count);
+ }
+
+ ///
+ /// Tests whether the plugin host isolates the caller from an exception when the
+ /// employer fails to employ a type in the assembly
+ ///
+ [Test]
+ public void TestAssemblyLoadingWithEmployFailure() {
+ PluginRepository testRepository = new PluginRepository();
+ PluginHost testHost = new PluginHost(new FailingEmployer(), testRepository);
+
+ // Might also use Assembly.GetCallingAssembly() here, but this leads to the exe of
+ // the unit testing tool
+ Assembly self = Assembly.GetAssembly(GetType());
+ testRepository.AddAssembly(self);
+ }
+
+ ///
+ /// Verifies that the plugin host ignores types which have the NoPluginAttribute
+ /// assigned to them
+ ///
+ [Test]
+ public void TestAssemblyLoadingWithNoPluginAttribute() {
+ PluginRepository testRepository = new PluginRepository();
+ FactoryEmployer testEmployer = new FactoryEmployer();
+ PluginHost testHost = new PluginHost(testEmployer, testRepository);
+
+ // Might also use Assembly.GetCallingAssembly() here, but this leads to the exe of
+ // the unit testing tool
+ Assembly self = Assembly.GetAssembly(GetType());
+ testRepository.AddAssembly(self);
+
+ Assert.AreEqual(0, testEmployer.Factories.Count);
+ }
+
}
} // namespace Nuclex.Support.Plugins
diff --git a/Source/Plugins/PluginHost.cs b/Source/Plugins/PluginHost.cs
index e45e660..c0b5601 100644
--- a/Source/Plugins/PluginHost.cs
+++ b/Source/Plugins/PluginHost.cs
@@ -19,6 +19,7 @@ License along with this library
#endregion
using System;
+using System.Diagnostics;
using System.Reflection;
namespace Nuclex.Support.Plugins {
@@ -83,10 +84,8 @@ namespace Nuclex.Support.Plugins {
// Types that have been tagged with the [NoPlugin] attribute will be ignored
object[] attributes = type.GetCustomAttributes(true);
- for(int index = 0; index < attributes.Length; ++index) {
- if(attributes[index] is NoPluginAttribute) {
- continue;
- }
+ if(containsNoPluginAttribute(attributes)) {
+ continue;
}
// Type seems to be acceptable, assess and possibly employ it
@@ -96,14 +95,26 @@ namespace Nuclex.Support.Plugins {
}
}
catch(Exception exception) {
- System.Console.WriteLine(
- "Could not employ " + type.ToString() + ": " + exception.Message
- );
+ Trace.WriteLine("Could not employ " + type.ToString() + ": " + exception.Message);
}
}
}
+ ///
+ /// Determines whether the specifies list of attributes contains a NoPluginAttribute
+ ///
+ /// List of attributes to check
+ /// True if the list contained a NoPluginAttribute, false otherwise
+ private static bool containsNoPluginAttribute(object[] attributes) {
+ for(int index = 0; index < attributes.Length; ++index) {
+ if(attributes[index] is NoPluginAttribute) {
+ return true;
+ }
+ }
+ return false;
+ }
+
/// Employs and manages types in the loaded plugin assemblies
private Employer employer;
/// Repository containing all plugins loaded, shared with other hosts
diff --git a/Source/Plugins/PluginRepository.Test.cs b/Source/Plugins/PluginRepository.Test.cs
index 56910f1..dfa2525 100644
--- a/Source/Plugins/PluginRepository.Test.cs
+++ b/Source/Plugins/PluginRepository.Test.cs
@@ -19,6 +19,7 @@ License along with this library
#endregion
using System;
+using System.Diagnostics;
using System.IO;
using System.Reflection;
@@ -53,6 +54,40 @@ namespace Nuclex.Support.Plugins {
#endregion // interface IProgressTrackerSubscriber
+ #region class TestAssemblyLoader
+
+ /// Special assembly loader for the unit test
+ public class TestAssemblyLoader : PluginRepository.DefaultAssemblyLoader {
+
+ /// Loads an assembly from a file system path
+ /// Path the assembly will be loaded from
+ /// The loaded assembly
+ protected override Assembly LoadAssemblyFromFile(string path) {
+ switch(path) {
+ case "DllNotFound": {
+ Trace.WriteLine("Simulating DllNotFoundException for unit test");
+ throw new DllNotFoundException();
+ }
+ case "UnauthorizedAccess": {
+ Trace.WriteLine("Simulating UnauthorizedAccessException for unit test");
+ throw new UnauthorizedAccessException();
+ }
+ case "BadImageFormat": {
+ Trace.WriteLine("Simulating BadImageFormatException for unit test");
+ throw new BadImageFormatException();
+ }
+ case "IO": {
+ Trace.WriteLine("Simulating IOException for unit test");
+ throw new IOException();
+ }
+ default: { return Assembly.LoadFile(path); }
+ }
+ }
+
+ }
+
+ #endregion // class TestAssemblyLoader
+
///
/// Tests whether the default constructor of the plugin repository class works
///
@@ -81,6 +116,8 @@ namespace Nuclex.Support.Plugins {
Assembly self = Assembly.GetAssembly(GetType());
testRepository.AddFiles(self.Location);
+
+ Assert.AreEqual(1, testRepository.LoadedAssemblies.Count);
}
///
@@ -118,6 +155,50 @@ namespace Nuclex.Support.Plugins {
mockery.VerifyAllExpectationsHaveBeenMet();
}
+ ///
+ /// Verifies that no exceptions come through when a DllNotFoundException is thrown
+ /// during assembly loading
+ ///
+ [Test]
+ public void TestDllNotFoundExceptionDuringAssemblyLoad() {
+ TestAssemblyLoader loader = new TestAssemblyLoader();
+ Assembly loadedAssembly;
+ Assert.IsFalse(loader.TryLoadFile("DllNotFound", out loadedAssembly));
+ }
+
+ ///
+ /// Verifies that no exceptions come through when a UnauthorizedAccessException is
+ /// thrown during assembly loading
+ ///
+ [Test]
+ public void TestUnauthorizedAccessExceptionDuringAssemblyLoad() {
+ TestAssemblyLoader loader = new TestAssemblyLoader();
+ Assembly loadedAssembly;
+ Assert.IsFalse(loader.TryLoadFile("UnauthorizedAccess", out loadedAssembly));
+ }
+
+ ///
+ /// Verifies that no exceptions come through when a BadImageFormatException is
+ /// thrown during assembly loading
+ ///
+ [Test]
+ public void TestBadImageFormatExceptionDuringAssemblyLoad() {
+ TestAssemblyLoader loader = new TestAssemblyLoader();
+ Assembly loadedAssembly;
+ Assert.IsFalse(loader.TryLoadFile("BadImageFormat", out loadedAssembly));
+ }
+
+ ///
+ /// Verifies that no exceptions come through when an IOException is
+ /// thrown during assembly loading
+ ///
+ [Test]
+ public void TestIOExceptionDuringAssemblyLoad() {
+ TestAssemblyLoader loader = new TestAssemblyLoader();
+ Assembly loadedAssembly;
+ Assert.IsFalse(loader.TryLoadFile("IO", out loadedAssembly));
+ }
+
/// Mocks a subscriber for the events of a plugin repository
/// Mockery to create an event subscriber in
/// Repository to subscribe the mocked subscriber to
diff --git a/Source/Plugins/PluginRepository.cs b/Source/Plugins/PluginRepository.cs
index b59e35a..b11a5e1 100644
--- a/Source/Plugins/PluginRepository.cs
+++ b/Source/Plugins/PluginRepository.cs
@@ -20,8 +20,9 @@ License along with this library
using System;
using System.Collections.Generic;
-using System.Reflection;
+using System.Diagnostics;
using System.IO;
+using System.Reflection;
namespace Nuclex.Support.Plugins {
@@ -33,12 +34,94 @@ namespace Nuclex.Support.Plugins {
///
public class PluginRepository {
+ #region class DefaultAssemblyLoader
+
+ /// Default assembly loader used to read assemblies from files
+ public class DefaultAssemblyLoader : IAssemblyLoader {
+
+ /// Initializes a new default assembly loader
+ ///
+ /// Made protected to provide users with a small incentive for using
+ /// the Instance property instead of creating new instances all around.
+ ///
+ protected DefaultAssemblyLoader() { }
+
+ /// Loads an assembly from a file system path
+ /// Path the assembly will be loaded from
+ /// The loaded assembly
+ protected virtual Assembly LoadAssemblyFromFile(string path) {
+ return Assembly.LoadFile(path);
+ }
+
+ /// Tries to loads an assembly from a file
+ /// Path to the file that is loaded as an assembly
+ ///
+ /// Output parameter that receives the loaded assembly or null
+ ///
+ /// True if the assembly was loaded successfully, otherwise false
+ public bool TryLoadFile(string path, out Assembly loadedAssembly) {
+
+ // A lot of errors can occur when attempting to load an assembly...
+ try {
+ loadedAssembly = LoadAssemblyFromFile(path);
+ return true;
+ }
+ // File not found - Most likely a missing dependency of the assembly we
+ // attempted to load since the assembly itself has been found by the GetFiles() method
+ catch(DllNotFoundException) {
+ Trace.WriteLine(
+ "Assembly '" + path + "' or one of its dependencies is missing"
+ );
+ }
+ // Unauthorized acccess - Either the assembly is not trusted because it contains
+ // code that imposes a security risk on the system or a user rights problem
+ catch(UnauthorizedAccessException) {
+ Trace.WriteLine(
+ "Not authorized to load assembly '" + path + "', " +
+ "possible rights problem"
+ );
+ }
+ // Bad image format - This exception is often thrown when the assembly we
+ // attempted to load requires a different version of the .NET framework
+ catch(BadImageFormatException) {
+ Trace.WriteLine(
+ "'" + path + "' is not a .NET assembly, requires a different version " +
+ "of the .NET Runtime or does not support the current instruction set (x86/x64)"
+ );
+ }
+ // Unknown error - Our last resort is to show a default error message
+ catch(Exception exception) {
+ Trace.WriteLine(
+ "Failed to load plugin assembly '" + path + "': " + exception.Message
+ );
+ }
+
+ loadedAssembly = null;
+ return false;
+
+ }
+
+ /// The only instance of the DefaultAssemblyLoader
+ public static readonly DefaultAssemblyLoader Instance =
+ new DefaultAssemblyLoader();
+
+ }
+
+ #endregion // class DefaultAssemblyLoader
+
/// Triggered whenever a new assembly is loaded into this repository
public event AssemblyLoadEventHandler AssemblyLoaded;
/// Initializes a new instance of the plugin repository
- public PluginRepository() {
+ public PluginRepository() : this(DefaultAssemblyLoader.Instance) { }
+
+ /// Initializes a new instance of the plugin repository
+ ///
+ /// Loader to use for loading assemblies into this repository
+ ///
+ public PluginRepository(IAssemblyLoader loader) {
this.assemblies = new List();
+ this.assemblyLoader = loader;
}
/// Loads all plugins matching a wildcard specification
@@ -63,38 +146,9 @@ namespace Nuclex.Support.Plugins {
string[] assemblyFiles = Directory.GetFiles(directory, search);
foreach(string assemblyFile in assemblyFiles) {
- // A lot of errors can occur when attempting to load an assembly...
- try {
- AddAssembly(Assembly.LoadFile(assemblyFile));
- }
- // File not found - Most likely a missing dependency of the assembly we
- // attempted to load since the assembly itself has been found by the GetFiles() method
- catch(DllNotFoundException) {
- Console.WriteLine(
- "Assembly '" + assemblyFile + "' or one of its dependencies is missing"
- );
- }
- // Unauthorized acccess - Either the assembly is not trusted because it contains
- // code that imposes a security risk on the system or a user rights problem
- catch(UnauthorizedAccessException) {
- Console.WriteLine(
- "Not authorized to load assembly '" + assemblyFile + "', " +
- "possible rights problem"
- );
- }
- // Bad image format - This exception is often thrown when the assembly we
- // attempted to load requires a different version of the .NET framework
- catch(BadImageFormatException) {
- Console.WriteLine(
- "'" + assemblyFile +"' is not a .NET assembly, requires a different version " +
- "of the .NET Runtime or does not support the current instruction set (x86/x64)"
- );
- }
- // Unknown error - Our last resort is to show a default error message
- catch(Exception exception) {
- Console.WriteLine(
- "Failed to load plugin assembly '" + assemblyFile + "': " + exception.Message
- );
+ Assembly loadedAssembly;
+ if(this.assemblyLoader.TryLoadFile(assemblyFile, out loadedAssembly)) {
+ AddAssembly(loadedAssembly);
}
}
@@ -121,6 +175,8 @@ namespace Nuclex.Support.Plugins {
/// Loaded plugin assemblies
private List assemblies;
+ /// Takes care of loading assemblies for the repositories
+ private IAssemblyLoader assemblyLoader;
}