|
// 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.
#nullable disable
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Test.Utilities;
using Xunit;
namespace Microsoft.CodeAnalysis.Test.Utilities
{
internal class MetadataSignatureUnitTestHelper
{
/// <summary>
/// Uses Reflection to verify that the specified member signatures are present in emitted metadata
/// </summary>
/// <param name="appDomainHost">Unit test AppDomain host</param>
/// <param name="expectedSignatures">Baseline signatures - use the Signature() factory method to create instances of SignatureDescription</param>
internal static void VerifyMemberSignatures(
IRuntimeEnvironment appDomainHost, params SignatureDescription[] expectedSignatures)
{
Assert.NotNull(expectedSignatures);
Assert.NotEmpty(expectedSignatures);
var succeeded = true;
var expected = new List<string>();
var actual = new List<string>();
foreach (var signature in expectedSignatures)
{
var expectedSignature = signature.ExpectedSignature;
if (!VerifyMemberSignatureHelper(
appDomainHost, signature.FullyQualifiedTypeName, signature.MemberName,
ref expectedSignature, out var actualSignatures))
{
succeeded = false;
}
expected.Add(expectedSignature);
actual.AddRange(actualSignatures);
}
if (!succeeded)
{
TriggerSignatureMismatchFailure(expected, actual);
}
}
/// <summary>
/// Uses Reflection to verify that the specified member signature is present in emitted metadata
/// </summary>
/// <param name="appDomainHost">Unit test AppDomain host</param>
/// <param name="fullyQualifiedTypeName">
/// Fully qualified type name for member
/// Names must be in format recognized by reflection
/// e.g. MyType<T>.MyNestedType<T, U> => MyType`1+MyNestedType`2
/// </param>
/// <param name="memberName">
/// Name of member on specified type whose signature needs to be verified
/// Names must be in format recognized by reflection
/// e.g. For explicitly implemented member - I1<string>.Method => I1<System.String>.Method
/// </param>
/// <param name="expectedSignature">
/// Baseline string for signature of specified member
/// Skip this argument to get an error message that shows all available signatures for specified member
/// This argument is passed by reference and it will be updated with a formatted form of the baseline signature for error reporting purposes
/// </param>
/// <param name="actualSignatures">List of found signatures matching member name</param>
/// <returns>True if a matching member signature was found, false otherwise</returns>
private static bool VerifyMemberSignatureHelper(
IRuntimeEnvironment appDomainHost, string fullyQualifiedTypeName, string memberName,
ref string expectedSignature, out List<string> actualSignatures)
{
Assert.False(string.IsNullOrWhiteSpace(fullyQualifiedTypeName), "'fullyQualifiedTypeName' can't be null or empty");
Assert.False(string.IsNullOrWhiteSpace(memberName), "'memberName' can't be null or empty");
var retVal = true;
actualSignatures = new List<string>();
var signatures = appDomainHost.GetMemberSignaturesFromMetadata(fullyQualifiedTypeName, memberName);
var signatureAssertText = "Signature(\"" + fullyQualifiedTypeName + "\", \"" + memberName + "\", \"{0}\"),";
if (!string.IsNullOrWhiteSpace(expectedSignature))
{
expectedSignature = expectedSignature.Replace("\"", "\\\"");
}
expectedSignature = string.Format(signatureAssertText, expectedSignature);
if (signatures.Count > 1)
{
var found = false;
foreach (var signature in signatures)
{
var actualSignature = signature.Replace("\"", "\\\"");
actualSignature = string.Format(signatureAssertText, actualSignature);
if (actualSignature == expectedSignature)
{
actualSignatures.Clear();
actualSignatures.Add(actualSignature);
found = true;
break;
}
else
{
actualSignatures.Add(actualSignature);
}
}
if (!found)
{
retVal = false;
}
}
else if (signatures.Count == 1)
{
var actualSignature = signatures.First().Replace("\"", "\\\"");
actualSignature = string.Format(signatureAssertText, actualSignature);
actualSignatures.Add(actualSignature);
if (expectedSignature != actualSignature)
{
retVal = false;
}
}
else
{
retVal = false;
}
return retVal;
}
/// <summary>
/// Triggers assert when expected and actual signatures don't match
/// </summary>
/// <param name="expectedSignatures">List of baseline signature strings</param>
/// <param name="actualSignatures">List of actually found signature strings</param>
private static void TriggerSignatureMismatchFailure(List<string> expectedSignatures, List<string> actualSignatures)
{
var expectedText = string.Empty;
var actualText = string.Empty;
var distinctSignatures = new HashSet<string>();
foreach (var signature in expectedSignatures)
{
// We need to preserve the order as well as prevent duplicates
if (!distinctSignatures.Contains(signature))
{
expectedText += "\n\t" + signature;
distinctSignatures.Add(signature);
}
}
distinctSignatures.Clear();
foreach (var signature in actualSignatures)
{
// We need to preserve the order as well as prevent duplicates
if (!distinctSignatures.Contains(signature))
{
actualText += "\n\t" + signature;
distinctSignatures.Add(signature);
}
}
expectedText = expectedText.TrimEnd(',');
actualText = actualText.TrimEnd(',');
var diffText = DiffUtil.DiffReport(expectedText, actualText);
Assert.True(false, "\n\nExpected:" + expectedText + "\n\nActual:" + actualText + "\n\nDifferences:\n" + diffText);
}
}
}
|