File: CommandLine.cs
Web Access
Project: src\src\Microsoft.DotNet.XUnitConsoleRunner\src\Microsoft.DotNet.XUnitConsoleRunner.csproj (xunit.console)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
 
namespace Xunit.ConsoleClient
{
    public class CommandLine
    {
        readonly Stack<string> arguments = new Stack<string>();
        readonly List<string> unknownOptions = new List<string>();
 
        protected CommandLine(string[] args, Predicate<string> fileExists = null)
        {
            if (fileExists == null)
                fileExists = File.Exists;
 
            for (var i = args.Length - 1; i >= 0; i--)
                arguments.Push(args[i]);
 
            Project = Parse(fileExists);
        }
 
        public AppDomainSupport? AppDomains { get; protected set; }
 
        public bool Debug { get; protected set; }
 
        public bool DiagnosticMessages { get; protected set; }
 
        public bool InternalDiagnosticMessages { get; protected set; }
 
        public bool FailSkips { get; protected set; }
 
        public int? MaxParallelThreads { get; set; }
 
        public bool NoAutoReporters { get; protected set; }
 
        public bool NoColor { get; protected set; }
 
        public bool NoLogo { get; protected set; }
 
#if DEBUG
        public bool Pause { get; protected set; }
#endif
 
        public XunitProject Project { get; protected set; }
 
        public bool? ParallelizeAssemblies { get; protected set; }
 
        public bool? ParallelizeTestCollections { get; set; }
 
        public bool Serialize { get; protected set; }
 
        public bool StopOnFail { get; protected set; }
 
        public bool Wait { get; protected set; }
 
        public IRunnerReporter ChooseReporter(IReadOnlyList<IRunnerReporter> reporters)
        {
            var result = default(IRunnerReporter);
 
            foreach (var unknownOption in unknownOptions)
            {
                var reporter = reporters.FirstOrDefault(r => r.RunnerSwitch == unknownOption) ?? throw new ArgumentException($"unknown option: -{unknownOption}");
 
                if (result != null)
                    throw new ArgumentException("only one reporter is allowed");
 
                result = reporter;
            }
 
            if (!NoAutoReporters)
                result = reporters.FirstOrDefault(r => r.IsEnvironmentallyEnabled) ?? result;
 
            return result ?? new DefaultRunnerReporterWithTypes();
        }
 
        protected virtual string GetFullPath(string fileName)
        {
            return Path.GetFullPath(fileName);
        }
 
        XunitProject GetProjectFile(List<Tuple<string, string>> assemblies)
        {
            var result = new XunitProject();
 
            foreach (var assembly in assemblies)
                result.Add(new XunitProjectAssembly
                {
                    AssemblyFilename = GetFullPath(assembly.Item1),
                    ConfigFilename = assembly.Item2 != null ? GetFullPath(assembly.Item2) : null,
                });
 
            return result;
        }
 
        static void GuardNoOptionValue(KeyValuePair<string, string> option)
        {
            if (option.Value != null)
                throw new ArgumentException($"error: unknown command line option: {option.Value}");
        }
 
        static bool IsConfigFile(string fileName)
        {
            return fileName.EndsWith(".config", StringComparison.OrdinalIgnoreCase)
                || fileName.EndsWith(".json", StringComparison.OrdinalIgnoreCase);
        }
 
        public static CommandLine Parse(params string[] args)
            => new CommandLine(args);
 
        protected void ParseRspFile(string path)
        {
            // derived from https://github.com/natemcmaster/CommandLineUtils/blob/f498cc7383b27730fd24486510573ab61ccab9d6/src/CommandLineUtils/Internal/ResponseFileParser.cs
            var rspLines = File.ReadAllLines(path);
            var args = new List<string>(capacity: rspLines.Length);
            var sb = new System.Text.StringBuilder();
            foreach (var line in rspLines)
            {
                if (line.Length == 0) continue;
                if (line[0] == '#') continue;
 
                var breakOn = default(char?);
 
                var shouldCreateNewArg = false;
 
                for (var j = 0; j < line.Length; j++)
                {
                    var ch = line[j];
                    if (ch == '\\')
                    {
                        j++;
                        if (j >= line.Length)
                        {
                            // the backslash ended the document
                            sb.Append('\\');
                            break;
                        }
 
                        ch = line[j];
 
                        if (ch != '"' && ch != '\'')
                        {
                            // not a recognized special character, so add the backlash
                            sb.Append('\\');
                        }
 
                        sb.Append(ch);
                        continue;
                    }
 
                    if (breakOn == ch)
                    {
                        shouldCreateNewArg = true;
                        breakOn = null;
                        continue;
                    }
 
                    if (breakOn.HasValue)
                    {
                        sb.Append(ch);
                        continue;
                    }
 
                    if (char.IsWhiteSpace(ch))
                    {
                        if (sb.Length > 0 || shouldCreateNewArg)
                        {
                            shouldCreateNewArg = false;
                            args.Add(sb.ToString());
                            sb.Clear();
                        }
                    }
                    else if (ch == '"')
                    {
                        // the loop will search for the next unescaped "
                        breakOn = '"';
                    }
                    else if (ch == '\'')
                    {
                        // the loop will search for the next unescaped '
                        breakOn = '\'';
                    }
                    else
                    {
                        sb.Append(ch);
                    }
                }
 
                if (sb.Length > 0 || breakOn.HasValue || shouldCreateNewArg)
                {
                    // if we hit the end of the line, regardless of quoting, append everything as an arg
                    args.Add(sb.ToString());
                    sb.Clear();
                }
            }
            
            foreach (var arg in args.Reverse<string>())
            {
                arguments.Push(arg);
            }
        }
 
        protected XunitProject Parse(Predicate<string> fileExists)
        {
            var assemblies = new List<Tuple<string, string>>();
 
            while (arguments.Count > 0)
            {
                if (arguments.Peek().StartsWith("-", StringComparison.Ordinal) || arguments.Peek().StartsWith("@", StringComparison.Ordinal))
                    break;
 
                var assemblyFile = arguments.Pop();
                if (IsConfigFile(assemblyFile))
                    throw new ArgumentException($"expecting assembly, got config file: {assemblyFile}");
                if (!fileExists(assemblyFile))
                    throw new ArgumentException($"file not found: {assemblyFile}");
 
                string configFile = null;
                if (arguments.Count > 0)
                {
                    var value = arguments.Peek();
                    if (!value.StartsWith("-", StringComparison.Ordinal) && IsConfigFile(value))
                    {
                        configFile = arguments.Pop();
                        if (!fileExists(configFile))
                            throw new ArgumentException($"config file not found: {configFile}");
                    }
                }
 
                assemblies.Add(Tuple.Create(assemblyFile, configFile));
            }
 
            var project = GetProjectFile(assemblies);
 
            while (arguments.Count > 0)
            {
                var option = PopOption(arguments);
                var optionName = option.Key.ToLowerInvariant();
 
                if (!optionName.StartsWith("-", StringComparison.Ordinal) && !optionName.StartsWith("@", StringComparison.Ordinal))
                    throw new ArgumentException($"unknown command line option: {option.Key}");
 
                if (optionName.StartsWith("@", StringComparison.Ordinal))
                {
                    ParseRspFile(option.Key.Substring(1));
                    continue;
                }
 
                optionName = optionName.Substring(1);
 
                if (optionName == "nologo")
                {
                    GuardNoOptionValue(option);
                    NoLogo = true;
                }
                else if (optionName == "failskips")
                {
                    GuardNoOptionValue(option);
                    FailSkips = true;
                }
                else if (optionName == "stoponfail")
                {
                    GuardNoOptionValue(option);
                    StopOnFail = true;
                }
                else if (optionName == "nocolor")
                {
                    GuardNoOptionValue(option);
                    NoColor = true;
                    TransformFactory.NoErrorColoring = NoColor;
                }
                else if (optionName == "noappdomain")    // Here for historical reasons
                {
                    GuardNoOptionValue(option);
                    AppDomains = AppDomainSupport.Denied;
                }
                else if (optionName == "noautoreporters")
                {
                    GuardNoOptionValue(option);
                    NoAutoReporters = true;
                }
#if DEBUG
                else if (optionName == "pause")
                {
                    GuardNoOptionValue(option);
                    Pause = true;
                }
#endif
                else if (optionName == "debug")
                {
                    GuardNoOptionValue(option);
                    Debug = true;
                }
                else if (optionName == "serialize")
                {
                    GuardNoOptionValue(option);
                    Serialize = true;
                }
                else if (optionName == "wait")
                {
                    GuardNoOptionValue(option);
                    Wait = true;
                }
                else if (optionName == "diagnostics")
                {
                    GuardNoOptionValue(option);
                    DiagnosticMessages = true;
                }
                else if (optionName == "internaldiagnostics")
                {
                    GuardNoOptionValue(option);
                    InternalDiagnosticMessages = true;
                }
                else if (optionName == "appdomains")
                {
                    if (option.Value == null)
                        throw new ArgumentException("missing argument for -appdomains");
 
                    switch (option.Value)
                    {
                        case "ifavailable":
                            AppDomains = AppDomainSupport.IfAvailable;
                            break;
 
                        case "required":
                            break;
 
                        case "denied":
                            AppDomains = AppDomainSupport.Denied;
                            break;
 
                        default:
                            throw new ArgumentException("incorrect argument value for -appdomains (must be 'ifavailable', 'required', or 'denied')");
 
                    }
                }
                else if (optionName == "maxthreads")
                {
                    if (option.Value == null)
                        throw new ArgumentException("missing argument for -maxthreads");
 
                    switch (option.Value)
                    {
                        case "default":
                            MaxParallelThreads = 0;
                            break;
 
                        case "unlimited":
                            MaxParallelThreads = -1;
                            break;
 
                        default:
                            int threadValue;
                            if (!int.TryParse(option.Value, out threadValue) || threadValue < 1)
                                throw new ArgumentException("incorrect argument value for -maxthreads (must be 'default', 'unlimited', or a positive number)");
 
                            MaxParallelThreads = threadValue;
                            break;
                    }
                }
                else if (optionName == "parallel")
                {
                    if (option.Value == null)
                        throw new ArgumentException("missing argument for -parallel");
 
                    if (!Enum.TryParse(option.Value, out ParallelismOption parallelismOption))
                        throw new ArgumentException("incorrect argument value for -parallel");
 
                    switch (parallelismOption)
                    {
                        case ParallelismOption.all:
                            ParallelizeAssemblies = true;
                            ParallelizeTestCollections = true;
                            break;
 
                        case ParallelismOption.assemblies:
                            ParallelizeAssemblies = true;
                            ParallelizeTestCollections = false;
                            break;
 
                        case ParallelismOption.collections:
                            ParallelizeAssemblies = false;
                            ParallelizeTestCollections = true;
                            break;
 
                        default:
                            ParallelizeAssemblies = false;
                            ParallelizeTestCollections = false;
                            break;
                    }
                }
                else if (optionName == "noshadow")
                {
                    GuardNoOptionValue(option);
                    foreach (var assembly in project.Assemblies)
                        assembly.Configuration.ShadowCopy = false;
                }
                else if (optionName == "trait")
                {
                    if (option.Value == null)
                        throw new ArgumentException("missing argument for -trait");
 
                    var pieces = option.Value.Split('=');
                    if (pieces.Length != 2 || string.IsNullOrEmpty(pieces[0]) || string.IsNullOrEmpty(pieces[1]))
                        throw new ArgumentException("incorrect argument format for -trait (should be \"name=value\")");
 
                    var name = pieces[0];
                    var value = pieces[1];
                    project.Filters.IncludedTraits.Add(name, value);
                }
                else if (optionName == "notrait")
                {
                    if (option.Value == null)
                        throw new ArgumentException("missing argument for -notrait");
 
                    var pieces = option.Value.Split('=');
                    if (pieces.Length != 2 || string.IsNullOrEmpty(pieces[0]) || string.IsNullOrEmpty(pieces[1]))
                        throw new ArgumentException("incorrect argument format for -notrait (should be \"name=value\")");
 
                    var name = pieces[0];
                    var value = pieces[1];
                    project.Filters.ExcludedTraits.Add(name, value);
                }
                else if (optionName == "class")
                {
                    if (option.Value == null)
                        throw new ArgumentException("missing argument for -class");
 
                    project.Filters.IncludedClasses.Add(option.Value);
                }
                else if (optionName == "noclass")
                {
                    if (option.Value == null)
                        throw new ArgumentException("missing argument for -noclass");
 
                    project.Filters.ExcludedClasses.Add(option.Value);
                }
                else if (optionName == "method")
                {
                    if (option.Value == null)
                        throw new ArgumentException("missing argument for -method");
 
                    project.Filters.IncludedMethods.Add(option.Value);
                }
                else if (optionName == "nomethod")
                {
                    if (option.Value == null)
                        throw new ArgumentException("missing argument for -nomethod");
 
                    project.Filters.ExcludedMethods.Add(option.Value);
                }
                else if (optionName == "namespace")
                {
                    if (option.Value == null)
                        throw new ArgumentException("missing argument for -namespace");
 
                    project.Filters.IncludedNamespaces.Add(option.Value);
                }
                else if (optionName == "nonamespace")
                {
                    if (option.Value == null)
                        throw new ArgumentException("missing argument for -nonamespace");
 
                    project.Filters.ExcludedNamespaces.Add(option.Value);
                }
                else
                {
                    // Might be a result output file...
                    if (TransformFactory.AvailableTransforms.Any(t => t.CommandLine.Equals(optionName, StringComparison.OrdinalIgnoreCase)))
                    {
                        if (option.Value == null)
                            throw new ArgumentException($"missing filename for {option.Key}");
 
                        EnsurePathExists(option.Value);
 
                        project.Output.Add(optionName, option.Value);
                    }
                    // ...or it might be a reporter (we won't know until later)
                    else
                    {
                        GuardNoOptionValue(option);
                        unknownOptions.Add(optionName);
                    }
                }
            }
 
            return project;
        }
 
        static KeyValuePair<string, string> PopOption(Stack<string> arguments)
        {
            var option = arguments.Pop();
            string value = null;
 
            if (arguments.Count > 0 && 
                !arguments.Peek().StartsWith("-", StringComparison.Ordinal) &&
                !arguments.Peek().StartsWith("@", StringComparison.Ordinal))
            {
                value = arguments.Pop();
            }
 
            return new KeyValuePair<string, string>(option, value);
        }
 
        static void EnsurePathExists(string path)
        {
            var directory = Path.GetDirectoryName(path);
 
            if (string.IsNullOrEmpty(directory))
                return;
 
            Directory.CreateDirectory(directory);
        }
    }
}