File: Helpers\MicrosoftTestingPlatformDetector.cs
Web Access
Project: src\vstest\src\Microsoft.TestPlatform.CoreUtilities\Microsoft.TestPlatform.CoreUtilities.csproj (Microsoft.TestPlatform.CoreUtilities)
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections.Concurrent;
using System.IO;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;

using Microsoft.VisualStudio.TestPlatform.ObjectModel;

namespace Microsoft.VisualStudio.TestPlatform.CoreUtilities.Helpers;

/// <summary>
/// Detects whether an assembly is a Microsoft.Testing.Platform application by reading the
/// <c>[assembly: AssemblyMetadata("Microsoft.Testing.Platform.Application", "true")]</c> attribute
/// that the Microsoft.Testing.Platform MSBuild targets stamp onto the entry assembly at build time.
/// </summary>
/// <remarks>
/// This lives in CoreUtilities so both the up-front detection in vstest.console and the routing
/// decision in the CrossPlatEngine TestEngine can share the exact same logic instead of duplicating
/// the (subtle) custom-attribute blob parsing.
/// </remarks>
internal static class MicrosoftTestingPlatformDetector
{
    private const string MicrosoftTestingPlatformApplicationMetadataKey = "Microsoft.Testing.Platform.Application";

    // Detection reads the assembly's PE metadata from disk, and a single run can ask about the same source
    // several times (grouping, provider resolution, and per-source proxy creation). Memoize per source path so
    // we read each assembly at most once. Concurrent because resolution can happen on parallel proxy threads.
    private static readonly ConcurrentDictionary<string, bool> Cache = new(StringComparer.OrdinalIgnoreCase);

    /// <summary>
    /// Returns <see langword="true"/> if the assembly at <paramref name="filePath"/> is a
    /// Microsoft.Testing.Platform application. Never throws; returns <see langword="false"/> on any error.
    /// </summary>
    public static bool IsMicrosoftTestingPlatformApp(string filePath)
    {
        if (filePath is null)
        {
            return false;
        }

        return Cache.GetOrAdd(filePath, static path =>
        {
            try
            {
                using var assemblyStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
                var result = IsMicrosoftTestingPlatformApp(assemblyStream);
                EqtTrace.Info("MicrosoftTestingPlatformDetector.IsMicrosoftTestingPlatformApp: '{0}' for source: '{1}'", result, path);
                return result;
            }
            catch (Exception ex)
            {
                EqtTrace.Warning("MicrosoftTestingPlatformDetector.IsMicrosoftTestingPlatformApp: failed to read assembly metadata, exception: {0} for assembly: {1}", ex, path);
                return false;
            }
        });
    }

    /// <summary>
    /// Returns <see langword="true"/> if <paramref name="assemblyStream"/> is a Microsoft.Testing.Platform
    /// application. The caller owns the stream lifetime.
    /// </summary>
    public static bool IsMicrosoftTestingPlatformApp(Stream assemblyStream)
    {
        using var peReader = new PEReader(assemblyStream);
        if (!peReader.HasMetadata)
        {
            return false;
        }

        var metadataReader = peReader.GetMetadataReader();

        // Microsoft.Testing.Platform applications are marked at build time with
        // [assembly: AssemblyMetadata("Microsoft.Testing.Platform.Application", "true")] by the
        // Microsoft.Testing.Platform MSBuild targets. We only look at assembly-level attributes.
        foreach (var handle in metadataReader.GetAssemblyDefinition().GetCustomAttributes())
        {
            var attribute = metadataReader.GetCustomAttribute(handle);
            if (!IsAssemblyMetadataAttribute(metadataReader, attribute))
            {
                continue;
            }

            try
            {
                // AssemblyMetadataAttribute has a (string key, string value) constructor. The custom attribute
                // blob is: 2-byte prolog (0x0001), then the two serialized strings, then the named-argument count.
                var blob = metadataReader.GetBlobReader(attribute.Value);
                if (blob.ReadUInt16() != 1)
                {
                    continue;
                }

                var key = blob.ReadSerializedString();
                var value = blob.ReadSerializedString();
                if (string.Equals(key, MicrosoftTestingPlatformApplicationMetadataKey, StringComparison.Ordinal)
                    && string.Equals(value, "true", StringComparison.OrdinalIgnoreCase))
                {
                    return true;
                }
            }
            catch (Exception ex)
            {
                EqtTrace.Verbose("MicrosoftTestingPlatformDetector.IsMicrosoftTestingPlatformApp: could not decode AssemblyMetadata attribute: {0}", ex);
            }
        }

        return false;
    }

    private static bool IsAssemblyMetadataAttribute(MetadataReader metadataReader, CustomAttribute attribute)
    {
        StringHandle typeNameHandle;
        StringHandle typeNamespaceHandle;
        switch (attribute.Constructor.Kind)
        {
            case HandleKind.MemberReference:
                var memberReference = metadataReader.GetMemberReference((MemberReferenceHandle)attribute.Constructor);
                switch (memberReference.Parent.Kind)
                {
                    case HandleKind.TypeReference:
                        var typeReference = metadataReader.GetTypeReference((TypeReferenceHandle)memberReference.Parent);
                        typeNameHandle = typeReference.Name;
                        typeNamespaceHandle = typeReference.Namespace;
                        break;
                    case HandleKind.TypeDefinition:
                        var typeDefinition = metadataReader.GetTypeDefinition((TypeDefinitionHandle)memberReference.Parent);
                        typeNameHandle = typeDefinition.Name;
                        typeNamespaceHandle = typeDefinition.Namespace;
                        break;
                    default:
                        return false;
                }

                break;

            case HandleKind.MethodDefinition:
                var methodDefinition = metadataReader.GetMethodDefinition((MethodDefinitionHandle)attribute.Constructor);
                var declaringType = metadataReader.GetTypeDefinition(methodDefinition.GetDeclaringType());
                typeNameHandle = declaringType.Name;
                typeNamespaceHandle = declaringType.Namespace;
                break;

            default:
                return false;
        }

        return string.Equals(metadataReader.GetString(typeNameHandle), "AssemblyMetadataAttribute", StringComparison.Ordinal)
            && string.Equals(metadataReader.GetString(typeNamespaceHandle), "System.Reflection", StringComparison.Ordinal);
    }
}