From 447fe2aea7e97fba8c1332936a76ae959b1d2aab Mon Sep 17 00:00:00 2001 From: Markus Ewald Date: Tue, 9 Dec 2008 19:41:43 +0000 Subject: [PATCH] Changed file name of IAbstractFactory interface to match with conventions; refactored the assembly loading code into a separate class using an interface, this allows unit testing to simulate assembly loading errors easily and might help the actual application to act on assembly load errors if so required; all classes in the Nuclex.Support.Plugins namespace now have 100% test coverage git-svn-id: file:///srv/devel/repo-conversion/nusu@104 d2e56fa2-650e-0410-a79f-9358c0239efd --- Nuclex.Support.csproj | 6 +- ...AbstractFactory.cs => IAbstractFactory.cs} | 0 ...ractFactory.Test.cs => IAssemblyLoader.cs} | 23 ++-- Source/Plugins/PluginHost.Test.cs | 128 +++++++++++++++++- Source/Plugins/PluginHost.cs | 25 +++- Source/Plugins/PluginRepository.Test.cs | 81 +++++++++++ Source/Plugins/PluginRepository.cs | 124 ++++++++++++----- 7 files changed, 330 insertions(+), 57 deletions(-) rename Source/Plugins/{AbstractFactory.cs => IAbstractFactory.cs} (100%) rename Source/Plugins/{AbstractFactory.Test.cs => IAssemblyLoader.cs} (54%) 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; }