File: TestCommandLine.cs
Web Access
Project: ..\..\..\test\Microsoft.NET.TestFramework\Microsoft.NET.TestFramework.csproj (Microsoft.NET.TestFramework)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Xml;
 
namespace Microsoft.NET.TestFramework
{
    public class TestCommandLine
    {
        public List<string>? RemainingArgs { get; private set; }
 
        public bool UseFullFrameworkMSBuild { get; private set; }
 
        public string? FullFrameworkMSBuildPath { get; private set; }
 
        public string? DotnetHostPath { get; private set; }
 
        public string? SDKRepoPath { get; private set; }
 
        public string? SDKRepoConfiguration { get; private set; }
 
        public bool NoRepoInference { get; private set; }
 
        public bool ShouldShowHelp { get; private set; }
 
        public string? SdkVersion { get; private set; }
 
        public string? TestExecutionDirectory { get; set; }
 
        public string? MsbuildAdditionalSdkResolverFolder { get; set; }
 
        public List<(string name, string value)> EnvironmentVariables { get; set; } = [];
 
        public List<string> TestConfigFiles { get; private set; } = [];
 
        public HashSet<string> TestListsToRun { get; private set; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 
        public bool ShowSdkInfo { get; private set; }
 
        public static TestCommandLine Parse(string[] args)
        {
            TestCommandLine ret = new()
            {
                RemainingArgs = new List<string>()
            };
            Stack<string> argStack = new(args.Reverse());
 
            while (argStack.Any())
            {
                string arg = argStack.Pop();
                if (arg.Equals("-useFullMSBuild", StringComparison.InvariantCultureIgnoreCase))
                {
                    ret.UseFullFrameworkMSBuild = true;
                }
                else if (arg.Equals("-fullMSBuildPath", StringComparison.InvariantCultureIgnoreCase) && argStack.Any())
                {
                    ret.FullFrameworkMSBuildPath = argStack.Pop();
                }
                else if (arg.Equals("-dotnetPath", StringComparison.InvariantCultureIgnoreCase) && argStack.Any())
                {
                    ret.DotnetHostPath = argStack.Pop();
                }
                else if (arg.Equals("-sdkRepo", StringComparison.InvariantCultureIgnoreCase) && argStack.Any())
                {
                    ret.SDKRepoPath = argStack.Pop();
                }
                else if (arg.Equals("-sdkConfig", StringComparison.InvariantCultureIgnoreCase) && argStack.Any())
                {
                    ret.SDKRepoConfiguration = argStack.Pop();
                }
                else if (arg.Equals("-noRepoInference", StringComparison.CurrentCultureIgnoreCase))
                {
                    ret.NoRepoInference = true;
                }
                else if (arg.Equals("-sdkVersion", StringComparison.CurrentCultureIgnoreCase))
                {
                    ret.SdkVersion = argStack.Pop();
                }
                else if (arg.Equals("-testExecutionDirectory", StringComparison.CurrentCultureIgnoreCase))
                {
                    ret.TestExecutionDirectory = argStack.Pop();
                }
                else if (arg.Equals("-msbuildAdditionalSdkResolverFolder", StringComparison.CurrentCultureIgnoreCase))
                {
                    ret.MsbuildAdditionalSdkResolverFolder = argStack.Pop();
                }
                else if (arg.Equals("-testConfigFile", StringComparison.CurrentCultureIgnoreCase) ||
                         arg.Equals("-testConfig", StringComparison.CurrentCultureIgnoreCase))
                {
                    ret.TestConfigFiles.Add(argStack.Pop());
                }
                else if (arg.Equals("-testList", StringComparison.CurrentCultureIgnoreCase))
                {
                    ret.TestListsToRun.Add(argStack.Pop());
                }
                else if (arg.Equals("-e", StringComparison.CurrentCultureIgnoreCase))
                {
                    ret.EnvironmentVariables.Add(ParseEnvironmentVariableArg(argStack.Pop()));
                }
                else if (arg.Equals("-showSdkInfo", StringComparison.CurrentCultureIgnoreCase))
                {
                    ret.ShowSdkInfo = true;
                }
                else if (arg.Equals("-help", StringComparison.CurrentCultureIgnoreCase) ||
                         arg.Equals("--help", StringComparison.CurrentCultureIgnoreCase) ||
                         arg.Equals("/?") || arg.Equals("-?"))
                {
                    ret.ShouldShowHelp = true;
                }
                else
                {
                    ret.RemainingArgs.Add(arg);
                }
            }
 
            if (!string.IsNullOrEmpty(ret.SDKRepoPath) && string.IsNullOrEmpty(ret.SDKRepoConfiguration))
            {
                ret.SDKRepoConfiguration = "Release";
            }
 
            if (string.IsNullOrEmpty(ret.FullFrameworkMSBuildPath))
            {
                //  Run tests on full framework MSBuild if environment variable is set pointing to it
                string? msbuildPath = Environment.GetEnvironmentVariable("DOTNET_SDK_TEST_MSBUILD_PATH");
                if (!string.IsNullOrEmpty(msbuildPath))
                {
                    ret.FullFrameworkMSBuildPath = msbuildPath;
                }
            }
 
            return ret;
        }
 
        private static (string name, string value) ParseEnvironmentVariableArg(string arg)
        {
            var i = arg.IndexOf('=');
            if (i <= 0)
            {
                throw new ArgumentException($"Invalid environment variable specification (expected 'name=value'): '{arg}'");
            }
 
            return (arg.Substring(0, i), arg.Substring(i + 1));
        }
 
        public List<string> GetXunitArgsFromTestConfig()
        {
            List<TestSpecifier> testsToSkip = new();
            List<TestList> testLists = new();
 
            List<string> ret = new();
            foreach (var testConfigFile in TestConfigFiles)
            {
                var testConfig = XDocument.Load(testConfigFile);
                if (testConfig.Root is not null)
                {
                    foreach (var item in testConfig.Root.Elements())
                    {
                        if (item.Name.LocalName.Equals("TestList", StringComparison.OrdinalIgnoreCase))
                        {
                            testLists.Add(TestList.Parse(item));
                        }
                        else if (item.Name.LocalName.Equals("SkippedTests", StringComparison.OrdinalIgnoreCase))
                        {
                            var skippedGroup = TestList.Parse(item);
                            testsToSkip.AddRange(skippedGroup.TestSpecifiers);
                        }
                        else
                        {
                            if (bool.TryParse(item.Attribute("Skip")?.Value ?? string.Empty, out bool shouldSkip) &&
                                shouldSkip)
                            {
                                testsToSkip.Add(TestSpecifier.Parse(item));
                            }
                        }
                    }
                }
            }
 
            foreach (var testList in testLists.Where(g => TestListsToRun.Contains(g.Name ?? string.Empty)))
            {
                foreach (var testSpec in testList.TestSpecifiers)
                {
                    if (testSpec.Type == TestSpecifier.TestSpecifierType.Method)
                    {
                        ret.Add("-method");
                    }
                    else if (testSpec.Type == TestSpecifier.TestSpecifierType.Class)
                    {
                        ret.Add("-class");
                    }
                    else if (testSpec.Type == TestSpecifier.TestSpecifierType.Namespace)
                    {
                        ret.Add("-namespace");
                    }
                    else
                    {
                        throw new ArgumentException("Unrecognized test specifier type: " + testSpec.Type);
                    }
                    ret.Add(testSpec.Specifier ?? string.Empty);
                }
            }
 
            foreach (var testSpec in testsToSkip)
            {
                if (testSpec.Type == TestSpecifier.TestSpecifierType.Method)
                {
                    ret.Add("-nomethod");
                }
                else if (testSpec.Type == TestSpecifier.TestSpecifierType.Class)
                {
                    ret.Add("-noclass");
                }
                else if (testSpec.Type == TestSpecifier.TestSpecifierType.Namespace)
                {
                    ret.Add("-nonamespace");
                }
                else
                {
                    throw new ArgumentException("Unrecognized test specifier type: " + testSpec.Type);
                }
                ret.Add(testSpec.Specifier ?? string.Empty);
            }
 
            return ret;
        }
 
        private class TestList
        {
            public string? Name { get; set; }
 
            public List<TestSpecifier> TestSpecifiers { get; set; } = new List<TestSpecifier>();
 
            public static TestList Parse(XElement element)
            {
                TestList group = new()
                {
                    Name = element.Attribute("Name")?.Value
                };
 
                foreach (var item in element.Elements())
                {
                    group.TestSpecifiers.Add(TestSpecifier.Parse(item));
                }
 
                return group;
            }
        }
 
        private class TestSpecifier
        {
            public enum TestSpecifierType
            {
                Method,
                Class,
                Namespace
            }
 
            public TestSpecifierType Type { get; set; }
            public string? Specifier { get; set; }
 
            public static TestSpecifier Parse(XElement element)
            {
                TestSpecifier spec = new();
                switch (element.Name.LocalName.ToLowerInvariant())
                {
                    case "method":
                        spec.Type = TestSpecifierType.Method;
                        break;
                    case "class":
                        spec.Type = TestSpecifierType.Class;
                        break;
                    case "namespace":
                        spec.Type = TestSpecifierType.Namespace;
                        break;
                    default:
                        throw new XmlException("Unrecognized node: " + element.Name);
                }
 
                spec.Specifier = element.Attribute("Name")?.Value;
 
                return spec;
            }
        }
 
        public static TestCommandLine HandleCommandLine(string[] args)
        {
            TestCommandLine commandLine = Parse(args);
 
            if (!commandLine.ShouldShowHelp)
            {
                TestContext.Initialize(commandLine);
            }
 
            return commandLine;
        }
 
        public static void ShowHelp()
        {
            Console.WriteLine(
@"
 
.NET Core SDK test runner
 
Options to control toolset to test:
  -useFullMSBuild         : Use full framework (instead of .NET Core) version of MSBuild found in PATH
  -fullMSBuildPath <path> : Use full framework version of MSBuild in specified path
  -dotnetPath <path>      : Use specified path for dotnet host
  -sdkRepo <path>         : Use specified SDK repo for Microsoft.NET.SDK tasks / targets
  -sdkConfig <config>     : Use specified configuration for SDK repo
  -noRepoInference        : Don't automatically find SDK repo to use based on path to test binaries
  -sdkVersion             : Use specified SDK version
 
Other options:
  -testConfigFile         : XML file with tests to skip, or test lists that can be run (can specify multiple)
  -testList               : List of tests (from config file) which should be run (can specify multiple)
  -testExecutionDirectory : Folder for tests to create and build projects
  -msbuildAdditionalSdkResolverFolder
                          : Folder for tests to override 'MsbuildAdditionalSdkResolverFolder' environment variable in tests
                            in order to test build built sdk resolvers.
  -showSdkInfo            : Shows SDK info (dotnet --info) for SDK which will be used
  -help                   : Show help");
        }
    }
}