File: CommandLine\InferHelper.cs
Web Access
Project: src\src\vstest\src\vstest.console\vstest.console.csproj (vstest.console)
// 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.Generic;
using System.IO;
using System.Runtime.Versioning;

using Microsoft.VisualStudio.TestPlatform.CommandLine.Processors;
using Microsoft.VisualStudio.TestPlatform.ObjectModel;

namespace Microsoft.VisualStudio.TestPlatform.CommandLineUtilities;

internal class InferHelper
{
    private readonly IAssemblyMetadataProvider _assemblyMetadataProvider;

    internal InferHelper(IAssemblyMetadataProvider assemblyMetadataProvider)
    {
        _assemblyMetadataProvider = assemblyMetadataProvider;
    }

    /// <summary>
    /// Determines Architecture from sources.
    /// </summary>
    public Architecture AutoDetectArchitecture(IList<string>? sources, Architecture defaultArchitecture, out IDictionary<string, Architecture> sourceToPlatformMap)
    {
        sourceToPlatformMap = new Dictionary<string, Architecture>();
        if (sources == null || sources.Count == 0)
            return defaultArchitecture;

        // Set the default for all sources.
        foreach (var source in sources)
        {
            // TODO: Add default architecture to runtime providers info, or something and that will allow us to have test
            // cases without any sources. Otherwise change test AutoDetectArchitectureShouldReturnDefaultArchitectureOnNullItemInSources
            // because this condition is making that test happy.
            if (source != null)
            {
                sourceToPlatformMap.Add(source, defaultArchitecture);
            }
        }

        try
        {
            Architecture? commonArchitecture = null;
            foreach (var source in sources)
            {
                if (source == null)
                    continue;

                try
                {
                    Architecture detectedArchitecture;
                    if (IsDllOrExe(source))
                    {
                        detectedArchitecture = _assemblyMetadataProvider.GetArchitecture(source);

                        if (detectedArchitecture == Architecture.AnyCPU)
                        {
                            // This is AnyCPU .NET assembly, this source should run using the default architecture,
                            // which we've already set for the source.
                            EqtTrace.Info("Determined platform for source '{0}' was AnyCPU and it will use the default plaform {1}.", source, defaultArchitecture);
                        }
                        else
                        {
                            sourceToPlatformMap[source] = detectedArchitecture;
                            EqtTrace.Info("Determined platform for source '{0}' was '{1}'.", source, detectedArchitecture);
                        }
                    }
                    else
                    {
                        // This is non-dll source, this source should run using the default architecture,
                        // which we've already set for the source.
                        EqtTrace.Info("No platform was determined for source '{0}' because it is not a dll or an executable.", source);

                        // This source has no associated architecture so it does not help use determine a common architecture for
                        // all the sources, so we continue to next one.
                        sourceToPlatformMap[source] = defaultArchitecture;
                        continue;
                    }

                    if (Architecture.AnyCPU.Equals(detectedArchitecture))
                    {
                        // The architecture of the source is AnyCPU and so we can skip to the next one,
                        // because it does not help use determine a common architecture for all the sources.
                        continue;
                    }

                    // This is the first source that provided some architecture use that as a candidate
                    // for the common architecture.
                    if (commonArchitecture == null)
                    {
                        commonArchitecture = detectedArchitecture;
                        continue;
                    }

                    // The detected architecture, is different than the common architecture. So at least
                    // one of the sources is incompatible with the others. Use the default architecture as the common
                    // fallback.
                    if (!commonArchitecture.Equals(detectedArchitecture))
                    {
                        commonArchitecture = defaultArchitecture;
                    }
                }
                catch (Exception ex)
                {
                    EqtTrace.Error("Failed to determine platform for source: {0}, using default: {1}, exception: {2}", source, defaultArchitecture, ex);
                    sourceToPlatformMap[source] = defaultArchitecture;
                }
            }

            if (commonArchitecture != null)
            {
                EqtTrace.Info("Determined platform for all sources: {0}", commonArchitecture);
                return commonArchitecture.Value;
            }

            EqtTrace.Info("None of the sources provided any runnable platform, using the default platform: {0}", defaultArchitecture);

            return defaultArchitecture;
        }
        catch (Exception ex)
        {
            EqtTrace.Error("Failed to determine platform for all sources: {0}, using default: {1}", ex, defaultArchitecture);
            return defaultArchitecture;
        }
    }

    /// <summary>
    /// Determines Framework from sources.
    /// </summary>
    public Framework AutoDetectFramework(IList<string?>? sources, out IDictionary<string, Framework> sourceToFrameworkMap)
    {
        sourceToFrameworkMap = new Dictionary<string, Framework>();

        if (sources == null || sources.Count == 0)
            return Framework.DefaultFramework;

        var framework = DetermineFramework(sources, out sourceToFrameworkMap, out var conflictInFxIdentifier);
        if (conflictInFxIdentifier)
        {
            // TODO Log to console and client.
            EqtTrace.Info(
                "conflicts in Framework identifier of provided sources(test assemblies), using default framework: {0}",
                framework);
        }

        EqtTrace.Info("Determined framework for all sources: {0}", framework);
        return framework!;
    }

    private Framework? DetermineFramework(IEnumerable<string?> sources, out IDictionary<string, Framework> sourceToFrameworkMap, out bool conflictInFxIdentifier)
    {
        sourceToFrameworkMap = new Dictionary<string, Framework>();

        var defaultFramework = Framework.DefaultFramework;
        FrameworkName? finalFx = null;
        conflictInFxIdentifier = false;
        foreach (var source in sources)
        {
            if (source is null)
            {
                continue;
            }

            try
            {
                FrameworkName fx;
                if (IsDllOrExe(source))
                {
                    fx = _assemblyMetadataProvider.GetFrameworkName(source);
                }
                else
                {
                    // TODO What else to do with appx, js and other?
                    var extension = Path.GetExtension(source);
                    if (extension.Equals(".js", StringComparison.OrdinalIgnoreCase))
                    {
                        // Currently to run tests for .NET Core, assembly need dependency to Microsoft.NET.Test.Sdk. Which is not
                        // possible for js files. So using default .NET Full framework version.
                        fx = new FrameworkName(Constants.DotNetFramework40);
                    }
                    else
                    {
                        fx = extension.Equals(".appx", StringComparison.OrdinalIgnoreCase)
                             || extension.Equals(".msix", StringComparison.OrdinalIgnoreCase)
                             || extension.Equals(".appxrecipe", StringComparison.OrdinalIgnoreCase)
                            ? new FrameworkName(Constants.DotNetFrameworkUap10)
                            : new FrameworkName(Framework.DefaultFramework.Name);
                    }
                }

                sourceToFrameworkMap.Add(source, Framework.FromString(fx.FullName)!);

                if (finalFx == null)
                {
                    finalFx = fx;
                    continue;
                }

                if (finalFx.Identifier.Equals(fx.Identifier))
                {
                    // Use latest version.
                    if (finalFx.Version < fx.Version)
                    {
                        finalFx = fx;
                    }
                }
                else
                {
                    conflictInFxIdentifier = true;
                    finalFx = new FrameworkName(defaultFramework.Name);
                }
            }
            catch (Exception ex)
            {
                sourceToFrameworkMap.Add(source, defaultFramework);
                EqtTrace.Error("Failed to determine framework for source: {0} using default framework: {1}, exception: {2}", source, defaultFramework.Name, ex);
            }
        }

        return finalFx != null
            ? Framework.FromString(finalFx.FullName)
            : defaultFramework;
    }

    private static bool IsDllOrExe(string? filePath)
    {
        var extType = Path.GetExtension(filePath);
        return extType != null && (extType.Equals(".dll", StringComparison.OrdinalIgnoreCase) ||
                                   extType.Equals(".exe", StringComparison.OrdinalIgnoreCase));
    }
}