File: MEF\TestComposition.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.Linq;
using System.Reflection;
using System.Text;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.VisualStudio.Composition;
using Roslyn.Utilities;
using Xunit;
 
namespace Microsoft.CodeAnalysis.Test.Utilities
{
    /// <summary>
    /// Represents a MEF composition used for testing.
    /// </summary>
    public sealed class TestComposition
    {
        public static readonly TestComposition Empty = new TestComposition([], [], []);
 
        private static readonly Dictionary<CacheKey, IExportProviderFactory> s_factoryCache = [];
 
        private readonly struct CacheKey : IEquatable<CacheKey>
        {
            private readonly ImmutableArray<Assembly> _assemblies;
            private readonly ImmutableArray<Type> _parts;
            private readonly ImmutableArray<Type> _excludedPartTypes;
 
            public CacheKey(ImmutableHashSet<Assembly> assemblies, ImmutableHashSet<Type> parts, ImmutableHashSet<Type> excludedPartTypes)
            {
                _assemblies = [.. assemblies.OrderBy((a, b) => string.CompareOrdinal(a.FullName, b.FullName))];
                _parts = [.. parts.OrderBy((a, b) => string.CompareOrdinal(a.FullName, b.FullName))];
                _excludedPartTypes = [.. excludedPartTypes.OrderBy((a, b) => string.CompareOrdinal(a.FullName, b.FullName))];
            }
 
            public override bool Equals(object? obj)
                => obj is CacheKey key && Equals(key);
 
            public bool Equals(CacheKey other)
                => _parts.SequenceEqual(other._parts) &&
                   _excludedPartTypes.SequenceEqual(other._excludedPartTypes) &&
                   _assemblies.SequenceEqual(other._assemblies);
 
            public override int GetHashCode()
                => Hash.Combine(Hash.Combine(Hash.CombineValues(_assemblies), Hash.CombineValues(_parts)), Hash.CombineValues(_excludedPartTypes));
 
            public static bool operator ==(CacheKey left, CacheKey right)
                => left.Equals(right);
 
            public static bool operator !=(CacheKey left, CacheKey right)
                => !(left == right);
        }
 
        /// <summary>
        /// Assemblies to include in the composition.
        /// </summary>
        public readonly ImmutableHashSet<Assembly> Assemblies;
 
        /// <summary>
        /// Types to exclude from the composition.
        /// All subtypes of types specified in <see cref="ExcludedPartTypes"/> and defined in <see cref="Assemblies"/> are excluded before <see cref="Parts"/> are added.
        /// </summary>
        public readonly ImmutableHashSet<Type> ExcludedPartTypes;
 
        /// <summary>
        /// Additional part types to add to the composition.
        /// </summary>
        public readonly ImmutableHashSet<Type> Parts;
 
        private readonly Lazy<IExportProviderFactory> _exportProviderFactory;
 
        private TestComposition(ImmutableHashSet<Assembly> assemblies, ImmutableHashSet<Type> parts, ImmutableHashSet<Type> excludedPartTypes)
        {
            Assemblies = assemblies;
            Parts = parts;
            ExcludedPartTypes = excludedPartTypes;
 
            _exportProviderFactory = new Lazy<IExportProviderFactory>(GetOrCreateFactory);
        }
 
        /// <summary>
        /// Returns a new instance of <see cref="HostServices"/> for the composition. This will either be a MEF composition or VS MEF composition host, 
        /// depending on what layer the composition is for. Editor Features and VS layers use VS MEF composition while anything else uses System.Composition.
        /// </summary>
        public HostServices GetHostServices()
            => VisualStudioMefHostServices.Create(ExportProviderFactory.CreateExportProvider());
 
        /// <summary>
        /// VS MEF <see cref="ExportProvider"/>.
        /// </summary>
        public IExportProviderFactory ExportProviderFactory => _exportProviderFactory.Value;
 
        private IExportProviderFactory GetOrCreateFactory()
        {
            var key = new CacheKey(Assemblies, Parts, ExcludedPartTypes);
 
            lock (s_factoryCache)
            {
                if (s_factoryCache.TryGetValue(key, out var existing))
                {
                    return existing;
                }
            }
 
            var newFactory = ExportProviderCache.CreateExportProviderFactory(GetCatalog(), IsRemote);
 
            lock (s_factoryCache)
            {
                if (s_factoryCache.TryGetValue(key, out var existing))
                {
                    return existing;
                }
 
                s_factoryCache.Add(key, newFactory);
            }
 
            return newFactory;
        }
 
        public bool IsRemote
            => Assemblies.Contains(typeof(Remote.BrokeredServiceBase).Assembly);
 
        private ComposableCatalog GetCatalog()
        {
            // Compositions should not be realized if they contain the same part in both the explicit include list and
            // the explicit exclude list.
            var configurationOverlap = Parts.Intersect(ExcludedPartTypes);
            Assert.Empty(configurationOverlap);
 
            return ExportProviderCache.CreateAssemblyCatalog(Assemblies, ExportProviderCache.CreateResolver()).WithoutPartsOfTypes(ExcludedPartTypes).WithParts(Parts);
        }
 
        public CompositionConfiguration GetCompositionConfiguration()
            => CompositionConfiguration.Create(GetCatalog());
 
        public TestComposition Add(TestComposition composition)
            => AddAssemblies(composition.Assemblies).AddParts(composition.Parts).AddExcludedPartTypes(composition.ExcludedPartTypes);
 
        public TestComposition AddAssemblies(params Assembly[]? assemblies)
            => AddAssemblies((IEnumerable<Assembly>?)assemblies);
 
        public TestComposition AddAssemblies(IEnumerable<Assembly>? assemblies)
            => WithAssemblies(Assemblies.Union(assemblies ?? []));
 
        public TestComposition AddParts(IEnumerable<Type>? types)
            => WithParts(Parts.Union(types ?? []));
 
        public TestComposition AddParts(params Type[]? types)
            => AddParts((IEnumerable<Type>?)types);
 
        public TestComposition AddExcludedPartTypes(IEnumerable<Type>? types)
            => WithExcludedPartTypes(ExcludedPartTypes.Union(types ?? []));
 
        public TestComposition AddExcludedPartTypes(params Type[]? types)
            => AddExcludedPartTypes((IEnumerable<Type>?)types);
 
        public TestComposition Remove(TestComposition composition)
            => RemoveAssemblies(composition.Assemblies).RemoveParts(composition.Parts).RemoveExcludedPartTypes(composition.ExcludedPartTypes);
 
        public TestComposition RemoveAssemblies(params Assembly[]? assemblies)
            => RemoveAssemblies((IEnumerable<Assembly>?)assemblies);
 
        public TestComposition RemoveAssemblies(IEnumerable<Assembly>? assemblies)
            => WithAssemblies(Assemblies.Except(assemblies ?? []));
 
        public TestComposition RemoveParts(IEnumerable<Type>? types)
            => WithParts(Parts.Except(types ?? []));
 
        public TestComposition RemoveParts(params Type[]? types)
            => RemoveParts((IEnumerable<Type>?)types);
 
        public TestComposition RemoveExcludedPartTypes(IEnumerable<Type>? types)
            => WithExcludedPartTypes(ExcludedPartTypes.Except(types ?? []));
 
        public TestComposition RemoveExcludedPartTypes(params Type[]? types)
            => RemoveExcludedPartTypes((IEnumerable<Type>?)types);
 
        public TestComposition WithAssemblies(ImmutableHashSet<Assembly> assemblies)
        {
            if (assemblies == Assemblies)
            {
                return this;
            }
 
            var testAssembly = assemblies.FirstOrDefault(IsTestAssembly);
            Contract.ThrowIfFalse(testAssembly == null, $"Test assemblies are not allowed in test composition: {testAssembly}. Specify explicit test parts instead.");
 
            return new TestComposition(assemblies, Parts, ExcludedPartTypes);
 
            static bool IsTestAssembly(Assembly assembly)
            {
                var name = assembly.GetName().Name!;
                return
                    name.EndsWith(".Tests", StringComparison.OrdinalIgnoreCase) ||
                    name.EndsWith(".UnitTests", StringComparison.OrdinalIgnoreCase) ||
                    name.IndexOf("Test.Utilities", StringComparison.OrdinalIgnoreCase) >= 0;
            }
        }
 
        public TestComposition WithParts(ImmutableHashSet<Type> parts)
            => (parts == Parts) ? this : new TestComposition(Assemblies, parts, ExcludedPartTypes);
 
        public TestComposition WithExcludedPartTypes(ImmutableHashSet<Type> excludedPartTypes)
            => (excludedPartTypes == ExcludedPartTypes) ? this : new TestComposition(Assemblies, Parts, excludedPartTypes);
 
        /// <summary>
        /// Use for VS MEF composition troubleshooting.
        /// </summary>
        /// <returns>All composition error messages.</returns>
        internal string GetCompositionErrorLog()
        {
            var configuration = CompositionConfiguration.Create(GetCatalog());
 
            var sb = new StringBuilder();
            foreach (var errorGroup in configuration.CompositionErrors)
            {
                foreach (var error in errorGroup)
                {
                    sb.Append(error.Message);
                    sb.AppendLine();
                }
            }
 
            return sb.ToString();
        }
    }
}