File: MEF\ExportProviderCache.cs
Web Access
Project: src\src\Workspaces\CoreTestUtilities\Microsoft.CodeAnalysis.Workspaces.Test.Utilities.csproj (Microsoft.CodeAnalysis.Workspaces.Test.Utilities)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.UnitTests.Remote;
using Microsoft.VisualStudio.Composition;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Test.Utilities
{
    public static class ExportProviderCache
    {
        private static readonly PartDiscovery s_partDiscovery = CreatePartDiscovery(Resolver.DefaultInstance);
 
        private static readonly TestComposition s_defaultHostExportProviderComposition = TestComposition.Empty
            .AddAssemblies(MefHostServices.DefaultAssemblies)
            .AddParts(typeof(TestSerializerService.Factory));
        private static readonly Scope _localCompositionScope = new Scope("local");
        private static readonly Scope _remoteCompositionScope = new Scope("remote");
 
        internal static bool Enabled { get; private set; }
 
        internal static ExportProvider? LocalExportProviderForCleanup => _localCompositionScope.CurrentExportProvider;
        internal static ExportProvider? RemoteExportProviderForCleanup => _remoteCompositionScope.CurrentExportProvider;
 
        internal static void SetEnabled_OnlyUseExportProviderAttributeCanCall(bool value)
        {
            Enabled = value;
            if (!Enabled)
            {
                _localCompositionScope.Clear();
                _remoteCompositionScope.Clear();
            }
        }
 
        /// <summary>
        /// Use to create <see cref="IExportProviderFactory"/> for default instances of <see cref="MefHostServices"/>.
        /// </summary>
        public static IExportProviderFactory GetOrCreateExportProviderFactory(IEnumerable<Assembly> assemblies)
        {
            if (assemblies is ImmutableArray<Assembly> assembliesArray &&
                assembliesArray == MefHostServices.DefaultAssemblies)
            {
                return s_defaultHostExportProviderComposition.ExportProviderFactory;
            }
 
            return CreateExportProviderFactory(CreateAssemblyCatalog(assemblies), isRemoteHostComposition: false);
        }
 
        public static ComposableCatalog CreateAssemblyCatalog(IEnumerable<Assembly> assemblies, Resolver? resolver = null)
        {
            var discovery = resolver == null ? s_partDiscovery : CreatePartDiscovery(resolver);
 
            // If we run CreatePartsAsync on the test thread we may deadlock since it'll schedule stuff back
            // on the thread.
            var parts = Task.Run(async () => await discovery.CreatePartsAsync(assemblies).ConfigureAwait(false)).Result;
 
            return ComposableCatalog.Create(resolver ?? Resolver.DefaultInstance).AddParts(parts);
        }
 
        public static ComposableCatalog CreateTypeCatalog(IEnumerable<Type> types, Resolver? resolver = null)
        {
            var discovery = resolver == null ? s_partDiscovery : CreatePartDiscovery(resolver);
 
            // If we run CreatePartsAsync on the test thread we may deadlock since it'll schedule stuff back
            // on the thread.
            var parts = Task.Run(async () => await discovery.CreatePartsAsync(types).ConfigureAwait(false)).Result;
 
            return ComposableCatalog.Create(resolver ?? Resolver.DefaultInstance).AddParts(parts);
        }
 
        public static Resolver CreateResolver()
        {
            // simple assembly loader is stateless, so okay to share
            return new Resolver(SimpleAssemblyLoader.Instance);
        }
 
        public static PartDiscovery CreatePartDiscovery(Resolver resolver)
            => PartDiscovery.Combine(new AttributedPartDiscoveryV1(resolver), new AttributedPartDiscovery(resolver, isNonPublicSupported: true));
 
        public static ComposableCatalog WithParts(this ComposableCatalog catalog, IEnumerable<Type> types)
            => catalog.AddParts(CreateTypeCatalog(types).DiscoveredParts);
 
        /// <summary>
        /// Creates a <see cref="ComposableCatalog"/> derived from <paramref name="catalog"/>, but with all exported
        /// parts assignable to any type in <paramref name="types"/> removed from the catalog.
        /// </summary>
        public static ComposableCatalog WithoutPartsOfTypes(this ComposableCatalog catalog, IEnumerable<Type> types)
        {
            var parts = catalog.Parts.Where(composablePartDefinition => !IsExcludedPart(composablePartDefinition));
            return ComposableCatalog.Create(Resolver.DefaultInstance).AddParts(parts);
 
            bool IsExcludedPart(ComposablePartDefinition part)
            {
                return types.Any(excludedType => excludedType.IsAssignableFrom(part.Type));
            }
        }
 
        public static IExportProviderFactory CreateExportProviderFactory(ComposableCatalog catalog, bool isRemoteHostComposition)
        {
            var scope = isRemoteHostComposition ? _remoteCompositionScope : _localCompositionScope;
            var configuration = CompositionConfiguration.Create(catalog.WithCompositionService());
            var runtimeComposition = RuntimeComposition.CreateRuntimeComposition(configuration);
            var exportProviderFactory = runtimeComposition.CreateExportProviderFactory();
 
            return new SingleExportProviderFactory(scope, catalog, configuration, exportProviderFactory);
        }
 
        private sealed class SingleExportProviderFactory : IExportProviderFactory
        {
            private readonly Scope _scope;
            private readonly ComposableCatalog _catalog;
            private readonly CompositionConfiguration _configuration;
            private readonly IExportProviderFactory _exportProviderFactory;
 
            public SingleExportProviderFactory(Scope scope, ComposableCatalog catalog, CompositionConfiguration configuration, IExportProviderFactory exportProviderFactory)
            {
                _scope = scope;
                _catalog = catalog;
                _configuration = configuration;
                _exportProviderFactory = exportProviderFactory;
            }
 
            public ExportProvider GetOrCreateExportProvider()
            {
                if (!Enabled)
                {
                    // The [UseExportProvider] attribute on tests ensures that the pre- and post-conditions of methods
                    // in this type are met during test conditions.
                    throw new InvalidOperationException($"{nameof(ExportProviderCache)} may only be used from tests marked with {nameof(UseExportProviderAttribute)}");
                }
 
                var expectedCatalog = Interlocked.CompareExchange(ref _scope.ExpectedCatalog, _catalog, null) ?? _catalog;
                RequireForSingleExportProvider(expectedCatalog == _catalog);
 
                var expected = _scope.ExpectedProviderForCatalog;
                if (expected == null)
                {
                    foreach (var errorCollection in _configuration.CompositionErrors)
                    {
                        foreach (var error in errorCollection)
                        {
                            foreach (var part in error.Parts)
                            {
                                foreach (var (importBinding, exportBindings) in part.SatisfyingExports)
                                {
                                    if (exportBindings.Count <= 1)
                                    {
                                        // Ignore composition errors for missing parts
                                        continue;
                                    }
 
                                    if (importBinding.ImportDefinition.Cardinality != ImportCardinality.ZeroOrMore)
                                    {
                                        // This failure occurs when a binding fails because multiple exports were
                                        // provided but only a single one (at most) is expected. This typically occurs
                                        // when a test ExportProvider is created with a mock implementation without
                                        // first removing a value provided by default.
                                        throw new InvalidOperationException(
                                            "Failed to construct the MEF catalog for testing. Multiple exports were found for a part for which only one export is expected:" + Environment.NewLine
                                            + error.Message);
                                    }
                                }
                            }
                        }
                    }
 
                    expected = _exportProviderFactory.CreateExportProvider();
                    expected = Interlocked.CompareExchange(ref _scope.ExpectedProviderForCatalog, expected, null) ?? expected;
                    Interlocked.CompareExchange(ref _scope.CurrentExportProvider, expected, null);
                }
 
                var exportProvider = _scope.CurrentExportProvider;
                RequireForSingleExportProvider(exportProvider == expected);
 
                return exportProvider;
            }
 
            ExportProvider IExportProviderFactory.CreateExportProvider()
            {
                // Currently this implementation deviates from the typical behavior of IExportProviderFactory. For the
                // duration of a single test, an instance of SingleExportProviderFactory will continue returning the
                // same ExportProvider instance each time this method is called.
                //
                // It may be clearer to refactor the implementation to only allow one call to CreateExportProvider in
                // the context of a single test. https://github.com/dotnet/roslyn/issues/25863
                return GetOrCreateExportProvider();
            }
 
            private void RequireForSingleExportProvider([DoesNotReturnIf(false)] bool condition)
            {
                if (!condition)
                {
                    // The ExportProvider provides services that act as singleton instances in the context of an
                    // application (this include cases of multiple exports, where the 'singleton' is the list of all
                    // exports matching the contract). When reasoning about the behavior of test code, it is valuable to
                    // know service instances will be used in a consistent manner throughout the execution of a test,
                    // regardless of whether they are passed as arguments or obtained through requests to the
                    // ExportProvider.
                    //
                    // Restricting a test to a single ExportProvider guarantees that objects that *look* like singletons
                    // will *behave* like singletons for the duration of the test. Each test is expected to create and
                    // use its ExportProvider in a consistent manner.
                    //
                    // A test that validates remote services is allowed to create a couple of ExportProviders:
                    // one for local workspace and the other for the remote one. 
                    //
                    // When this exception is thrown by a test, it typically means one of the following occurred:
                    //
                    // * A test failed to pass an ExportProvider via an optional argument to a method, resulting in the
                    //   method attempting to create a default ExportProvider which did not match the one assigned to
                    //   the test.
                    // * A test attempted to perform multiple test sequences in the context of a single test method,
                    //   rather than break up the test into distinct tests for each case.
                    // * A test referenced different predefined ExportProvider instances within the context of a test.
                    //   Each test is expected to use the same ExportProvider throughout the test.
                    throw new InvalidOperationException($"Only one {_scope.Name} {nameof(ExportProvider)} can be created in the context of a single test.");
                }
            }
        }
 
        private sealed class Scope
        {
            public readonly string Name;
            public ExportProvider? CurrentExportProvider;
            public ComposableCatalog? ExpectedCatalog;
            public ExportProvider? ExpectedProviderForCatalog;
 
            public Scope(string name)
            {
                Name = name;
            }
 
            public void Clear()
            {
                CurrentExportProvider = null;
                ExpectedCatalog = null;
                ExpectedProviderForCatalog = null;
            }
        }
 
        private sealed class SimpleAssemblyLoader : IAssemblyLoader
        {
            public static readonly IAssemblyLoader Instance = new SimpleAssemblyLoader();
 
            public Assembly LoadAssembly(AssemblyName assemblyName)
                => Assembly.Load(assemblyName);
 
            public Assembly LoadAssembly(string assemblyFullName, string? codeBasePath)
            {
                var assemblyName = new AssemblyName(assemblyFullName);
                if (!string.IsNullOrEmpty(codeBasePath))
                {
#pragma warning disable SYSLIB0044
                    assemblyName.CodeBase = codeBasePath;
#pragma warning restore SYSLIB0044
                }
 
                return this.LoadAssembly(assemblyName);
            }
        }
    }
}