|
// 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();
}
}
}
|