--- a
+++ b/Interfaces/+APUnitTestFramework/Testbed.cs
@@ -0,0 +1,510 @@
+using _3S.CoDeSys.Core.ComponentModel;
+using _3S.CoDeSys.Core.Components;
+using _3S.CoDeSys.Core.Licensing;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.Serialization.Json;
+using System.Threading;
+using System.Xml;
+using System.Xml.Linq;
+using System.Xml.XPath;
+
+namespace _3S.APUnitTestFramework
+{
+    /// <summary>
+    /// Use this class to create a single unit test environment. It can be considered being a
+    /// lightweight Automation Platform process containing the testee plug-in and mocks for all
+    /// dependencies.
+    /// </summary>
+    /// <remarks>
+    /// Note that the testbed will only work properly if all involved dependencies are handled
+    /// using our Dependency Injection mechanisms, and if the (FxCop-enforced)
+    /// APEnvironment-DependencyBag implementation pattern is used.
+    /// </remarks>
+    public class Testbed : IDisposable
+    {
+        /// <summary>
+        /// Creates a new testbed.
+        /// </summary>
+        public Testbed()
+        {
+        }
+
+        /// <summary>
+        /// Gets or sets the root folder of the Automation Platform installation. If this property
+        /// is not explicitly set, or explicitly set to <c>null</c>, then the root folder will be
+        /// automatically determined as described in the Remarks section.
+        /// </summary>
+        /// <exception cref="InvalidOperationException">
+        /// The value of this property is set after <see cref="Initialize"/> has been called.
+        /// </exception>
+        /// <remarks>
+        /// There are two heuristics how to find out the root folder of the Automation Platform
+        /// installation, applied in that order given:
+        /// <list type="bullet">
+        /// <item>
+        /// If the unit test assembly is within an Apaddon directory structure, then the default
+        /// target in the corresponding <c>AddOn.json</c> file will be used as root folder of the
+        /// test environment.
+        /// </item>
+        /// <item>
+        /// If the unit test assembly is not within an Apaddon directory structure, or the
+        /// <c>AddOn.json</c> file could not be found or evaluated, then the root folder as
+        /// specified in the <c>%CODESYS_OUTPUTDIR%</c> or <c>%CODESYS_64_OUTPUTDIR%</c> environment variable will be used
+        /// depending on <c>%CODESYS_PLATFORM%</c>. This
+        /// scenario should work in all scenarios where unit tests are developed for standard
+        /// CODESYS plug-ins.
+        /// </item>
+        /// </list>
+        /// </remarks>
+        public string RootFolder
+        {
+            get
+            {
+                if (_rootFolder == null)
+                {
+                    _rootFolder = DeriveRootFolderFromApaddon();
+                    if (string.IsNullOrEmpty(_rootFolder) || !Directory.Exists(_rootFolder))
+                        _rootFolder = DeriveRootFolderFromStandard();
+                    if (string.IsNullOrEmpty(_rootFolder) || !Directory.Exists(_rootFolder))
+                        _rootFolder = string.Empty;
+                }
+                return _rootFolder;
+            }
+
+            set
+            {
+                if (_initializeCalled)
+                    throw new InvalidOperationException("Attempt to set the RootFolder property after calling Initialize()");
+
+                _rootFolder = value;
+            }
+        }
+
+        private static string DeriveRootFolderFromApaddon()
+        {
+            try
+            {
+                var thisAssemblyDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
+                var targetsDirectory = Path.Combine(thisAssemblyDirectory, @"..\..\..\..\..\Targets");
+                var targetsFile = Path.Combine(targetsDirectory, @".addon");
+
+                // NewtonSoft.Json would be a nice alternative, but unfortunately we are not
+                // allowed to reference it from an interface assembly.
+                // However, .NET has got built-in support.
+                var jsonData = File.ReadAllBytes(targetsFile);
+                var jsonReader = JsonReaderWriterFactory.CreateJsonReader(jsonData, new XmlDictionaryReaderQuotas());
+                var jsonRoot = XElement.Load(jsonReader);
+                var nameElement = jsonRoot.XPathSelectElement("//active/name");
+                var name = nameElement.Value;
+
+                var targetDirectory = Path.Combine(targetsDirectory, name);
+                return targetDirectory;
+            }
+            catch
+            {
+                return null;
+            }
+        }
+ 
+        private static string DeriveRootFolderFromStandard()
+        {
+            if (Environment.GetEnvironmentVariable("CODESYS_PLATFORM") != null && 
+                (Environment.GetEnvironmentVariable("CODESYS_PLATFORM").Equals("x64", StringComparison.CurrentCultureIgnoreCase) ||
+                Environment.GetEnvironmentVariable("CODESYS_PLATFORM").Equals("BOTH", StringComparison.CurrentCultureIgnoreCase)))
+            {
+                return Environment.ExpandEnvironmentVariables(Environment.GetEnvironmentVariable("CODESYS_64_OUTPUTDIR"));
+            }
+            else
+            {
+                return Environment.ExpandEnvironmentVariables(Environment.GetEnvironmentVariable("CODESYS_OUTPUTDIR"));
+            }
+
+        }
+
+        /// <summary>
+        /// Call this method in order to add a plug-in to the test environment.
+        /// </summary>
+        /// <param name="plugInGuid">
+        /// The GUID of the plug-in to be added to the test environment. The newest version will be
+        /// used.
+        /// </param>
+        /// <exception cref="InvalidOperationException">
+        /// This method is called after <see cref="Initialize"/> has been called.
+        /// </exception>
+        /// <remarks>
+        /// One should use this possibility with care. When adding "real" plug-ins to the test
+        /// environment, the idea of a "pure" unit test will be compromised, where every dependency
+        /// should be a mock with an exactly specified behavior.
+        /// Furthermore, please do not add the <c>Engine.plugin</c> to the environment. The reason
+        /// is that two Component Model implementations would then be part of the environment,
+        /// causing the Component Manager to throw an exception during initialization.
+        /// </remarks>
+        public void IncludeAdditionalPlugInGuid(Guid plugInGuid)
+        {
+            if (_initializeCalled)
+                throw new InvalidOperationException("Attempt to include additional plug-ins after calling Initialize()");
+
+            if (_additionalPlugInGuids == null)
+                _additionalPlugInGuids = new HashSet<Guid>();
+            _additionalPlugInGuids.Add(plugInGuid);
+        }
+
+        /// <summary>
+        /// Gets the plug-in GUIDs of the additional plug-ins that have been added via <see cref=
+        /// "IncludeAdditionalPlugInGuid(Guid)"/>.
+        /// </summary>
+        public IEnumerable<Guid> AdditionalPlugInGuids
+        {
+            get
+            {
+                if (_additionalPlugInGuids != null)
+                    return _additionalPlugInGuids;
+                else
+                    return new Guid[0];
+            }
+        }
+
+        /// <summary>
+        /// Adds a mock to the test environment. This is the preferred way to fulfill any
+        /// dependency of the testee.
+        /// <seealso cref="AddMock(Guid, Func{object}, bool)"/>
+        /// <seealso cref="AddMock(Guid, Func{object}, Func{object}, bool)"/>
+        /// </summary>
+        /// <param name="typeGuid">
+        /// The type GUID of the mock type. If the testee's dependency is described by an <see
+        /// cref="InjectSpecificInstanceAttribute"/> or an <see cref=
+        /// "InjectSpecificTypeInformationAttribute"/>, then the specified GUID must match the
+        /// requested one, otherwise a new GUID can be created.
+        /// </param>
+        /// <param name="createFunc">
+        /// A delegate which creates the mock instance on request.
+        /// </param>
+        /// <exception cref="InvalidOperationException">
+        /// This method is called after <see cref="Initialize"/> has been called.
+        /// </exception>
+        /// <exception cref="ArgumentNullException">
+        /// <paramref name="createFunc"/> is <c>null</c>.
+        /// </exception>
+        /// <exception cref="ArgumentException">
+        /// This method is called multiple times with the same <paramref name="typeGuid"/> value.
+        /// </exception>
+        public void AddMock(Guid typeGuid, Func<object> createFunc)
+        {
+            AddMock(typeGuid, createFunc, createFunc, false);
+        }
+
+        /// <summary>
+        /// Adds a mock to the test environment. This is the preferred way to fulfill any
+        /// dependency of the testee.
+        /// <seealso cref="AddMock(Guid, Func{object})"/>
+        /// <seealso cref="AddMock(Guid, Func{object}, Func{object}, bool)"/>
+        /// </summary>
+        /// <param name="typeGuid">
+        /// The type GUID of the mock type. If the testee's dependency is described by an <see
+        /// cref="InjectSpecificInstanceAttribute"/> or an <see cref=
+        /// "InjectSpecificTypeInformationAttribute"/>, then the specified GUID must match the
+        /// requested one, otherwise a new GUID can be created.
+        /// </param>
+        /// <param name="createFunc">
+        /// A delegate which creates the mock instance on request.
+        /// </param>
+        /// <param name="systemInstance">
+        /// This mock is a system instance, i.e. it will be a global singleton.
+        /// </param>
+        /// <exception cref="InvalidOperationException">
+        /// This method is called after <see cref="Initialize"/> has been called.
+        /// </exception>
+        /// <exception cref="ArgumentNullException">
+        /// <paramref name="createFunc"/> is <c>null</c>.
+        /// </exception>
+        /// <exception cref="ArgumentException">
+        /// This method is called multiple times with the same <paramref name="typeGuid"/> value.
+        /// </exception>
+        public void AddMock(Guid typeGuid, Func<object> createFunc, bool systemInstance)
+        {
+            AddMock(typeGuid, createFunc, createFunc, systemInstance);
+        }
+
+        /// <summary>
+        /// Adds a mock to the test environment. This is the preferred way to fulfill any
+        /// dependency of the testee.
+        /// <seealso cref="AddMock(Guid, Func{object})"/>
+        /// <seealso cref="AddMock(Guid, Func{object}, bool)"/>
+        /// </summary>
+        /// <param name="typeGuid">
+        /// The type GUID of the mock type. If the testee's dependency is described by an <see
+        /// cref="InjectSpecificInstanceAttribute"/> or an <see cref=
+        /// "InjectSpecificTypeInformationAttribute"/>, then the specified GUID must match the
+        /// requested one, otherwise a new GUID can be created.
+        /// </param>
+        /// <param name="createFunc">
+        /// A delegate which creates the mock instance on request.
+        /// </param>
+        /// <param name="createPrototypeFunc">
+        /// The testbed will create prototypes for the mock instance during initialization. If
+        /// there is a different logic for prototype instances compared to "real" instances, then
+        /// a dedicated creation function can be specified using this parameter. (For example, a
+        /// prototype instance should not attach itself to events, whereas a "real" instance might
+        /// do it.) The other overloads of this method use the same creation function for both
+        /// construction methods.
+        /// </param>
+        /// <param name="systemInstance">
+        /// This mock is a system instance, i.e. it will be a global singleton.
+        /// </param>
+        /// <exception cref="InvalidOperationException">
+        /// This method is called after <see cref="Initialize"/> has been called.
+        /// </exception>
+        /// <exception cref="ArgumentNullException">
+        /// <paramref name="createFunc"/> is <c>null</c>.
+        /// </exception>
+        /// <exception cref="ArgumentException">
+        /// This method is called multiple times with the same <paramref name="typeGuid"/> value.
+        /// </exception>
+        public void AddMock(Guid typeGuid, Func<object> createFunc, Func<object> createPrototypeFunc, bool systemInstance)
+        {
+            if (_initializeCalled)
+                throw new InvalidOperationException("Attempt to add mock after calling Initialize()");
+
+            if (_mocks == null)
+                _mocks = new Dictionary<Guid, MockConstructor>();
+
+            var mockConstructor = new MockConstructor(createFunc, createPrototypeFunc, systemInstance);
+            _mocks.Add(typeGuid, mockConstructor);
+        }
+
+        /// <summary>
+        /// Gets a read-only dictionary of all mock constructors that have been added via <see
+        /// cref="AddMock(Guid, MockConstructor)"/>.
+        /// </summary>
+        public IDictionary<Guid, MockConstructor> Mocks
+        {
+            get
+            {
+                if (_mocks != null)
+                    return new ReadOnlyDictionary<Guid, MockConstructor>(_mocks);
+                else
+                    return new ReadOnlyDictionary<Guid, MockConstructor>(new Dictionary<Guid, MockConstructor>());
+            }
+        }
+
+        /// <summary>
+        /// Initializes the test environment, after the caller has performed all necessary
+        /// preparations using <see cref="RootFolder"/>, <see cref="IncludeAdditionalPlugInGuid
+        /// (Guid)"/>, and <see cref="AddMock(Guid, MockConstructor)"/>.
+        /// </summary>
+        /// <exception cref="InvalidOperationException">
+        /// The <see cref="RootFolder"/> property returns an invalid or non-existing directory.
+        /// </exception>
+        /// <exception cref="PlugInDoesNotExistException">
+        /// Either the <c>APUnitTestFramework.plugin</c> or one of the plug-ins denoted by <see
+        /// cref="AdditionalPlugInGuids"/> is not installed.
+        /// </exception>
+        /// <exception cref="Exception">
+        /// This method rethrows all exceptions that are thrown by the standard <see cref=
+        /// "ComponentManager"/> implementation.
+        /// </exception>
+        /// <remarks>
+        /// This initialization routine comprises the following steps:
+        /// <list type="bullet">
+        /// <item>
+        /// Resetting the <c>ComponentManager</c> singleton.
+        /// </item>
+        /// <item>
+        /// Resetting the <c>ComponentModel</c> singleton.
+        /// </item>
+        /// <item>
+        /// Scanning all loaded assemblies for <c>APEnvironment</c> classes and resetting their
+        /// lazily initialized bag field (type <c>Lazy&lt;DependencyBag&gt;</c>). This is the
+        /// reason why the testbed only works reliably for plug-ins which conform to the
+        /// APEnvironment-DependencyBag implementation pattern, as enforced by our internal FxCop
+        /// coding rules.
+        /// </item>
+        /// <item>
+        /// Setting the root folder as specified by the <see cref="RootFolder"/> property.
+        /// </item>
+        /// <item>
+        /// Creating a temporary profile which only contains the <c>APUnitTestFramework.plugin</c>
+        /// and the plug-ins as specified via the <see cref="IncludeAdditionalPlugInGuid(Guid)"/>
+        /// method.
+        /// </item>
+        /// <item>
+        /// Creating the system instances.
+        /// </item>
+        /// </list>
+        /// </remarks>
+        public void Initialize()
+        {
+            _initializeCalled = true;
+
+            // Reset all static things.
+
+            // Unregister component Manager from AppDomain events
+           var fiAssemblyLoad = AppDomain.CurrentDomain.GetType().GetField("AssemblyLoad", BindingFlags.Instance | BindingFlags.NonPublic);
+            if (fiAssemblyLoad != null)
+            {
+                AssemblyLoadEventHandler eventHandler = fiAssemblyLoad.GetValue(AppDomain.CurrentDomain) as AssemblyLoadEventHandler;
+                if (eventHandler != null)
+                {
+                    AppDomain.CurrentDomain.AssemblyLoad -= eventHandler;
+                }
+            }
+            var fiAssemblyResolve = AppDomain.CurrentDomain.GetType().GetField("_AssemblyResolve", BindingFlags.Instance | BindingFlags.NonPublic);
+            if (fiAssemblyResolve != null)
+            {
+                ResolveEventHandler eventHandler = fiAssemblyResolve.GetValue(AppDomain.CurrentDomain) as ResolveEventHandler;
+                if (eventHandler != null)
+                {
+                    AppDomain.CurrentDomain.AssemblyResolve -= eventHandler;
+                }
+            }
+
+            // The private s_singleton field is obfuscated! As a hack, discover all fields for
+            // that one which is of type ComponentManager. Ugh!
+            var fieldInfo = typeof(ComponentManager).GetFields(BindingFlags.Static | BindingFlags.NonPublic)
+                .Single(fi => typeof(ComponentManager).IsAssignableFrom(fi.FieldType));
+            fieldInfo.SetValue(null, null);
+
+            // The private s_singleton field is obfuscated! As a hack, discover all fields for
+            // that one which is of type ComponentModel. Doubled ugh!
+            fieldInfo = typeof(ComponentModel).GetFields(BindingFlags.Static | BindingFlags.NonPublic)
+                .Single(fi => typeof(ComponentModel).IsAssignableFrom(fi.FieldType));
+            fieldInfo.SetValue(null, null);
+
+            // Finally, we must reset all the dependency bags that are around. Obviously, this is
+            // only possible by reflection.
+            // Note: We are heavily tweaking the Lazy<T> object's internal members. A different
+            // idea would be to create new Lazy<T> objects with the original value factories.
+            // However, it turns out to be impossible because the value factory is internally
+            // replaced by something else in the Lazy<T> implementation
+            // ("ALREADY_INVOKED_SENTINEL") so that it becomes impossible for us to get hold of the
+            // original value factory as specified in the constructor.
+            var alreadyInitializedLazyDependencyBags = AppDomain.CurrentDomain.GetAssemblies()
+                .Where(asm => PlugInGuidAttribute.FromAssembly(asm) != null)
+                .SelectMany(asm => asm.GetTypes())
+                .Where(type => type.Name == "APEnvironment")
+                .SelectMany(type => type.GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic))
+                .Where(field => field.FieldType.IsGenericType && typeof(Lazy<>).IsAssignableFrom(field.FieldType.GetGenericTypeDefinition()))
+                .Select(field => field.GetValue(null));
+            foreach (var dependencyBag in alreadyInitializedLazyDependencyBags)
+            {
+                var boxed = dependencyBag.GetType().GetField("m_boxed", BindingFlags.Instance | BindingFlags.NonPublic);
+                boxed.SetValue(dependencyBag, null);
+
+                var valueFactory = dependencyBag.GetType().GetField("m_valueFactory", BindingFlags.Instance | BindingFlags.NonPublic);
+                valueFactory.SetValue(dependencyBag, null);
+
+                var isValueCreated = (bool)dependencyBag.GetType().GetProperty("IsValueCreated").GetValue(dependencyBag);
+                Debug.Assert(!isValueCreated);
+            }
+
+            // Initialize the component manager at the specified or derived root folder.
+
+            var rootFolder = RootFolder;
+            if (string.IsNullOrEmpty(rootFolder) || !Directory.Exists(rootFolder))
+                throw new InvalidOperationException("The RootFolder property has not been set correctly and could not be derived from your development environment.");
+
+            ComponentManager.SetRootFolder(RootFolder);
+            new ComponentManager();
+            bool foo;
+            ComponentManager.Singleton.InitializePlugInCache(null, new LicensingInitializationReporter(), out foo);
+
+            // Create a temporary profile which contains
+            // - the APUnitTestFramework plug-in
+            // - the list of plugins as specified in the AdditionalPlugInGuids property.
+            // - (nothing else).
+            // Use a unique profile name so that concurrent independent unit test runs are possible
+            // without interference.
+
+            var profileName = "APUnitTestFramework_" + DateTime.Now.Ticks;
+            var profile = ComponentManager.Singleton.ProfileList.CreateProfile(profileName);
+            profile.SetVersionConstraint(PLUGINGUID_APUNITTESTFRAMEWORK, new NewestVersionConstraint());
+            foreach (var plugInGuid in AdditionalPlugInGuids)
+                profile.SetVersionConstraint(plugInGuid, new NewestVersionConstraint());
+            ComponentManager.Singleton.SetActiveProfile(profileName, profile);
+
+            // Check whether all plug-ins are really existing. Otherwise throw an exception.
+            foreach (var plugInGuid in profile.GetEntries())
+            {
+                var versionConstraint = profile.GetVersionConstraint(plugInGuid);
+                var allVersions = ComponentManager.Singleton.PlugInCache.GetPlugInVersionsFast(plugInGuid);
+                var version = versionConstraint.FindVersionFast(allVersions);
+                if (version == null)
+                    throw new PlugInDoesNotExistException(new PlugInName(plugInGuid, new Version("0.0.0.0")));
+            }
+
+            var implementationProperty = typeof(ComponentModel).GetProperty("Implementation", BindingFlags.Instance | BindingFlags.NonPublic);
+            var componentModelImpl = (IComponentModelImplementationForUnitTest)implementationProperty.GetValue(ComponentModel.Singleton);
+            componentModelImpl.SetupForUnitTest(this, profileName, profile);
+            ComponentManager.Singleton.CreateSystemInstances(null);
+        }
+
+        public void Dispose()
+        {
+            if (!_initializeCalled)
+                return; // No temporary profile has been set yet, so there is nothing to clean up.
+
+            for (var i = 0; i < 10; i++)
+            {
+                try
+                {
+                    var profile = ComponentManager.Singleton.ActiveProfileName;
+                    ComponentManager.Singleton.ProfileList.DeleteProfile(profile);
+                    break;
+                }
+                catch
+                {
+                    Thread.Sleep(500);
+                }
+            }
+        }
+
+        /// <summary>
+        /// This event is triggered for all dependencies that could not be resolved. While
+        /// developing a unit test, subscribing this event can be useful to recognize all
+        /// dependencies that need a mock.
+        /// </summary>
+        public event EventHandler<DependencyErrorEventArgs> DependencyError;
+
+        /// <summary>
+        /// For internal use.
+        /// </summary>
+        /// <param name="exception"></param>
+        /// <param name="requiredInterfaceType"></param>
+        /// <param name="specificTypeGuid"></param>
+        public void RaiseDependencyError(ComponentModelException exception, Type requiredInterfaceType, Guid? specificTypeGuid)
+        {
+            DependencyError?.Invoke(this, new DependencyErrorEventArgs(exception, requiredInterfaceType, specificTypeGuid));
+        }
+
+        private string _rootFolder;
+        private HashSet<Guid> _additionalPlugInGuids;
+        private Dictionary<Guid, MockConstructor> _mocks;
+        private bool _initializeCalled;
+        private static readonly Guid PLUGINGUID_APUNITTESTFRAMEWORK = new Guid("{DFAA33F9-68D4-4BFB-B9AF-2EAD069DD7CA}");
+
+        class LicensingInitializationReporter : ILicensingInitializationReporter2
+        {
+            public ReporterUsage Usage { get; set; } = ReporterUsage.StartUp;
+
+            public bool CheckLicense(PlugInInformation plugInInfo)
+            {
+                // Licensed plug-ins with valid license -> OK
+                // Licensed plug-ins without valid license -> No check. Probably the test will
+                // crash afterwards due to the missing decryption.
+                return true;
+            }
+
+            public bool ReportMissingLicenses(IEnumerable<PlugInInformation> plugInInfos)
+            {
+                return false;
+            }
+        }
+    }
+}