ExportProviderCache.cs 14.6 KB
Newer Older
1 2 3 4
// Copyright (c) Microsoft.  All Rights Reserved.  Licensed under the Apache License, Version 2.0.  See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
5
using System.Collections.Immutable;
6
using System.Linq;
7
using System.Reflection;
8
using System.Threading;
9
using System.Threading.Tasks;
10 11
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Remote;
12 13
using Microsoft.VisualStudio.Composition;
using Roslyn.Utilities;
14
using Xunit;
15 16 17 18 19 20 21

namespace Microsoft.CodeAnalysis.Test.Utilities
{
    public static class ExportProviderCache
    {
        private static readonly PartDiscovery s_partDiscovery = CreatePartDiscovery(Resolver.DefaultInstance);

22 23
        // Cache the catalog and export provider factory for MefHostServices.DefaultAssemblies
        private static readonly ComposableCatalog s_defaultHostCatalog =
24
            CreateAssemblyCatalog(MefHostServices.DefaultAssemblies);
25 26

        private static readonly IExportProviderFactory s_defaultHostExportProviderFactory =
27
            CreateExportProviderFactory(s_defaultHostCatalog);
28 29 30

        // Cache the catalog and export provider factory for RoslynServices.RemoteHostAssemblies
        private static readonly ComposableCatalog s_remoteHostCatalog =
31
            CreateAssemblyCatalog(RoslynServices.RemoteHostAssemblies);
32 33

        private static readonly IExportProviderFactory s_remoteHostExportProviderFactory =
34
            CreateExportProviderFactory(s_remoteHostCatalog);
35

36 37 38 39 40 41
        private static bool _enabled;

        private static ExportProvider _currentExportProvider;
        private static ComposableCatalog _expectedCatalog;
        private static ExportProvider _expectedProviderForCatalog;

42 43 44
        internal static bool Enabled => _enabled;

        internal static ExportProvider ExportProviderForCleanup => _currentExportProvider;
45

46 47 48 49
        internal static void SetEnabled_OnlyUseExportProviderAttributeCanCall(bool value)
        {
            _enabled = value;
            if (!_enabled)
50
            {
51 52 53
                _currentExportProvider = null;
                _expectedCatalog = null;
                _expectedProviderForCatalog = null;
54 55 56
            }
        }

57
        public static ComposableCatalog GetOrCreateAssemblyCatalog(Assembly assembly)
58
        {
59
            return GetOrCreateAssemblyCatalog(SpecializedCollections.SingletonEnumerable(assembly));
60 61
        }

62
        public static ComposableCatalog GetOrCreateAssemblyCatalog(IEnumerable<Assembly> assemblies, Resolver resolver = null)
63
        {
64 65
            if (assemblies is ImmutableArray<Assembly> assembliesArray)
            {
S
Sam Harwell 已提交
66
                if (assembliesArray == MefHostServices.DefaultAssemblies)
67 68 69
                {
                    return s_defaultHostCatalog;
                }
S
Sam Harwell 已提交
70
                else if (assembliesArray == RoslynServices.RemoteHostAssemblies)
71 72 73 74 75
                {
                    return s_remoteHostCatalog;
                }
            }

76
            return CreateAssemblyCatalog(assemblies, resolver);
77 78
        }

79
        private static ComposableCatalog CreateAssemblyCatalog(IEnumerable<Assembly> assemblies, Resolver resolver = null)
80
        {
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
            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);
        }

101 102 103 104 105 106
        public static Resolver CreateResolver()
        {
            // simple assembly loader is stateless, so okay to share
            return new Resolver(SimpleAssemblyLoader.Instance);
        }

107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130
        public static PartDiscovery CreatePartDiscovery(Resolver resolver)
        {
            return PartDiscovery.Combine(new AttributedPartDiscoveryV1(resolver), new AttributedPartDiscovery(resolver, isNonPublicSupported: true));
        }

        public static ComposableCatalog WithParts(this ComposableCatalog @this, ComposableCatalog catalog)
        {
            return @this.AddParts(catalog.DiscoveredParts);
        }

        public static ComposableCatalog WithParts(this ComposableCatalog catalog, IEnumerable<Type> types)
        {
            return catalog.WithParts(CreateTypeCatalog(types));
        }

        public static ComposableCatalog WithParts(this ComposableCatalog catalog, params Type[] types)
        {
            return WithParts(catalog, (IEnumerable<Type>)types);
        }

        public static ComposableCatalog WithPart(this ComposableCatalog catalog, Type t)
        {
            return catalog.WithParts(CreateTypeCatalog(SpecializedCollections.SingletonEnumerable(t)));
        }
131

132 133 134 135
        /// <summary>
        /// Creates a <see cref="ComposableCatalog"/> derived from <paramref name="catalog"/>, but with all exported
        /// parts assignable to type <paramref name="t"/> removed from the catalog.
        /// </summary>
136 137 138 139 140
        public static ComposableCatalog WithoutPartsOfType(this ComposableCatalog catalog, Type t)
        {
            return catalog.WithoutPartsOfTypes(SpecializedCollections.SingletonEnumerable(t));
        }

141 142 143 144
        /// <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>
145 146 147 148 149 150 151 152 153 154 155
        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));
            }
        }

156
        public static IExportProviderFactory GetOrCreateExportProviderFactory(ComposableCatalog catalog)
157
        {
S
Sam Harwell 已提交
158
            if (catalog == s_defaultHostCatalog)
159 160 161
            {
                return s_defaultHostExportProviderFactory;
            }
S
Sam Harwell 已提交
162
            else if (catalog == s_remoteHostCatalog)
163 164
            {
                return s_remoteHostExportProviderFactory;
165 166
            }

167
            return CreateExportProviderFactory(catalog);
168 169
        }

170
        private static IExportProviderFactory CreateExportProviderFactory(ComposableCatalog catalog)
171
        {
172 173 174
            var configuration = CompositionConfiguration.Create(catalog.WithCompositionService());
            var runtimeComposition = RuntimeComposition.CreateRuntimeComposition(configuration);
            var exportProviderFactory = runtimeComposition.CreateExportProviderFactory();
175
            return new SingleExportProviderFactory(catalog, configuration, exportProviderFactory);
176 177 178 179 180
        }

        private class SingleExportProviderFactory : IExportProviderFactory
        {
            private readonly ComposableCatalog _catalog;
181
            private readonly CompositionConfiguration _configuration;
182 183
            private readonly IExportProviderFactory _exportProviderFactory;

184
            public SingleExportProviderFactory(ComposableCatalog catalog, CompositionConfiguration configuration, IExportProviderFactory exportProviderFactory)
185
            {
186
                _catalog = catalog;
187
                _configuration = configuration;
188
                _exportProviderFactory = exportProviderFactory;
189 190
            }

191
            public ExportProvider GetOrCreateExportProvider()
192
            {
193
                if (!Enabled)
194
                {
195 196
                    // The [UseExportProvider] attribute on tests ensures that the pre- and post-conditions of methods
                    // in this type are met during test conditions.
197
                    throw new InvalidOperationException($"{nameof(ExportProviderCache)} may only be used from tests marked with {nameof(UseExportProviderAttribute)}");
198 199
                }

200
                var expectedCatalog = Interlocked.CompareExchange(ref _expectedCatalog, _catalog, null) ?? _catalog;
201
                RequireForSingleExportProvider(expectedCatalog == _catalog);
202

203 204 205
                var expected = _expectedProviderForCatalog;
                if (expected == null)
                {
206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234
                    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);
                                    }
                                }
                            }
                        }
                    }

235 236 237 238
                    expected = _exportProviderFactory.CreateExportProvider();
                    expected = Interlocked.CompareExchange(ref _expectedProviderForCatalog, expected, null) ?? expected;
                    Interlocked.CompareExchange(ref _currentExportProvider, expected, null);
                }
239

240
                var exportProvider = _currentExportProvider;
241 242 243 244 245
                RequireForSingleExportProvider(exportProvider == expected);

                return exportProvider;
            }

246 247 248 249 250 251 252 253 254 255 256
            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();
            }

257 258 259
            private static void RequireForSingleExportProvider(bool condition)
            {
                if (!condition)
260
                {
261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277
                    // 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.
                    //
                    // 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,
G
Gen Lu 已提交
278
                    //   rather than break up the test into distinct tests for each case.
279 280
                    // * 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.
281 282 283
                    throw new InvalidOperationException($"Only one {nameof(ExportProvider)} can be created in the context of a single test.");
                }
            }
284
        }
285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305

        private class SimpleAssemblyLoader : IAssemblyLoader
        {
            public static readonly IAssemblyLoader Instance = new SimpleAssemblyLoader();

            public Assembly LoadAssembly(AssemblyName assemblyName)
            {
                return Assembly.Load(assemblyName);
            }

            public Assembly LoadAssembly(string assemblyFullName, string codeBasePath)
            {
                var assemblyName = new AssemblyName(assemblyFullName);
                if (!string.IsNullOrEmpty(codeBasePath))
                {
                    assemblyName.CodeBase = codeBasePath;
                }

                return this.LoadAssembly(assemblyName);
            }
        }
306 307
    }
}