<commit_before>﻿using System;
using System.Linq;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Loggers;
using BenchmarkDotNet.Properties;
using System.Collections.Generic;

namespace BenchmarkDotNet.Configs
{
    public class ConfigParser
    {
        // TODO, refactor this, at the moment they keys for availableOptions and availableParameters MUST stay in sync (which is brittle)
        private static Dictionary<string, Action<ManualConfig, string>> availableOptions = 
            new Dictionary<string, Action<ManualConfig, string>>
            {
                { "jobs", (config, value) => config.Add(ParseJobs(value)) },
                { "columns", (config, value) => config.Add(ParseColumns(value)) },
                { "exporters", (config, value) => { } },
                { "diagnosers", (config, value) => config.Add(ParseDiagnosers(value)) },
                { "analysers", (config, value) => { } },
                { "loggers", (config, value) => { } }
            };
        
        // NOTE: These need to be Lazy<T>, so so that we know they are only called AFTER availableOptions has been initialised (it's static)
        private static Dictionary<string, Lazy<IEnumerable<string>>> availableParameters =
            new Dictionary<string, Lazy<IEnumerable<string>>>
            {
                { "jobs", new Lazy<IEnumerable<string>>(() => availableJobs.Keys) },
                { "columns", new Lazy<IEnumerable<string>>(() => availableColumns.Keys) },
                { "exporters", new Lazy<IEnumerable<string>>(() => Enumerable.Empty<string>()) },
                { "diagnosers", new Lazy<IEnumerable<string>>(() => Enumerable.Empty<string>()) },
                { "analysers", new Lazy<IEnumerable<string>>(() => Enumerable.Empty<string>()) },
                { "loggers", new Lazy<IEnumerable<string>>(() => Enumerable.Empty<string>()) }
            };

        private static Dictionary<string, IJob[]> availableJobs =
            new Dictionary<string, IJob[]>
            {
                { "default", new [] { Job.Default } },
                { "legacyjitx86", new[] { Job.LegacyJitX86 } },
                { "legacyjitx64", new[] { Job.LegacyJitX64 } } ,
                { "ryujitx64", new[] { Job.RyuJitX64 } },
                { "dry", new[] { Job.Dry } },
                { "alljits", Job.AllJits },
                { "clr", new[] { Job.Clr } },
                { "mono", new[] { Job.Mono } },
                { "longrun", new[] { Job.LongRun } }
            };

        private static Dictionary<string, IColumn[]> availableColumns =
            new Dictionary<string, IColumn[]>
            {
                { "mean", new [] { StatisticColumn.Mean } },
                { "stderror", new[] { StatisticColumn.StdError } },
                { "stddev", new[] { StatisticColumn.StdDev } },
                { "operationpersecond", new [] { StatisticColumn.OperationsPerSecond } },
                { "min", new[] { StatisticColumn.Min } },
                { "q1", new[] { StatisticColumn.Q1 } },
                { "median", new[] { StatisticColumn.Median } },
                { "q3", new[] { StatisticColumn.Q3 } },
                { "max", new[] { StatisticColumn.Max } },
                { "allstatistics", StatisticColumn.AllStatistics  },
                { "place", new[] { PlaceColumn.ArabicNumber } }
            };

        public IConfig Parse(string[] args)
        {
            var config = new ManualConfig();
            foreach (var arg in args.Where(arg => arg.Contains("=")))
            {
                var split = arg.ToLowerInvariant().Split('=');
                var values = split[1].Split(',');
                // Delibrately allow both "jobs" and "job" to be specified, makes it easier for users!!
                var argument = split[0].EndsWith("s") ? split[0] : split[0] + "s";
                if (availableOptions.ContainsKey(argument))
                {
                    var action = availableOptions[argument];
                    foreach (var value in values)
                        action(config, value);
                }
                else
                {
                    throw new InvalidOperationException($"\"{split[0]}\" (from \"{arg}\") is an unrecognised Option");
                }
            }
            return config;
        }

        public bool ShouldDisplayOptions(string[] args)
        {
            return args.Select(a => a.ToLowerInvariant()).Any(a => a == "--help" || a == "-h");
        }

        public void PrintOptions(ILogger logger)
        {
            logger.WriteLineHeader($"{BenchmarkDotNetInfo.FullTitle}");
            logger.WriteLine();
            logger.WriteLineHeader("Options:");
            foreach (var option in availableOptions)
            {
                // TODO also consider allowing short version (i.e. '-d' and '--diagnosers')             
                var optionText = $"--{option.Key} <{option.Key.ToUpperInvariant()}>";
                var parameters = string.Empty;
                if (availableParameters.ContainsKey(option.Key))
                    parameters = string.Join(", ", availableParameters[option.Key].Value);
                var explanation = $"Allowed values: {parameters}";
                logger.WriteLineInfo($"  {optionText,-30} {explanation}");
            }
        }

        private static IJob[] ParseJobs(string value)
        {
            if (availableJobs.ContainsKey(value))
                return availableJobs[value];

            throw new InvalidOperationException($"\"{value}\" is an unrecognised Job");
        }

        private static IColumn[] ParseColumns(string value)
        {
            if (availableColumns.ContainsKey(value))
                return availableColumns[value];

            throw new InvalidOperationException($"\"{value}\" is an unrecognised Column");
        }

        private static IDiagnoser[] ParseDiagnosers(string value)
        {
            foreach (var diagnoser in DefaultConfig.LazyLoadedDiagnosers.Value)
            {
                if (value == diagnoser.GetType().Name.Replace("Diagnoser", "").ToLowerInvariant())
                    return new[] { diagnoser };
            }
            throw new InvalidOperationException($"{value} is an unrecognised Diagnoser");
        }
    }
}<commit_msg>Refactoring of ConfigParser code (part of #78)<commit_after>﻿using System;
using System.Linq;
using System.Collections.Generic;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Loggers;
using BenchmarkDotNet.Properties;

namespace BenchmarkDotNet.Configs
{
    public class ConfigParser
    {
        private class Options
        {
            public Action<ManualConfig, string> ProcessOption { get; set; } = (config, value) => { };
            public Lazy<IEnumerable<string>> GetAllOptions { get; set; } = new Lazy<IEnumerable<string>>(() => Enumerable.Empty<string>());
        }

        // NOTE: GetAllOptions needs to be Lazy<T>, because they call static variables (and initialisation order it tricky!!)
        private static Dictionary<string, Options> configuration = new Dictionary<string, Options>
        {
            { "jobs", new Options {
                ProcessOption = (config, value) => config.Add(ParseItem("Job", availableJobs, value)),
                GetAllOptions = new Lazy<IEnumerable<string>>(() => availableJobs.Keys)
            } },
            { "columns", new Options {
                ProcessOption = (config, value) => config.Add(ParseItem("Column", availableColumns, value)),
                GetAllOptions = new Lazy<IEnumerable<string>>(() => availableColumns.Keys)
            } },
            { "exporters", new Options {
                // TODO allow Exporters to be configured on the cmd line
                ProcessOption = (config, value) => { throw new InvalidOperationException($"{value} is an unrecognised Exporter"); },
            } },
            { "diagnosers", new Options {
                ProcessOption = (config, value) => config.Add(ParseDiagnosers(value)),
                // TODO these 2 should match the lookup in LoadDiagnosers() in DefaultConfig.cs
                GetAllOptions = new Lazy<IEnumerable<string>>(() => Enumerable.Empty<string>())
            } },
            { "analysers", new Options {
                // TODO allow Analysers to be configured on the cmd line
                ProcessOption = (config, value) => { throw new InvalidOperationException($"{value} is an unrecognised Analyser"); },
            } },
            { "loggers", new Options {
                // TODO does it make sense to allows Loggers to be configured on the cmd-line?
                ProcessOption = (config, value) => { throw new InvalidOperationException($"{value} is an unrecognised Logger"); },
            } },
        };

        private static Dictionary<string, IJob[]> availableJobs =
            new Dictionary<string, IJob[]>
            {
                { "default", new [] { Job.Default } },
                { "legacyjitx86", new[] { Job.LegacyJitX86 } },
                { "legacyjitx64", new[] { Job.LegacyJitX64 } } ,
                { "ryujitx64", new[] { Job.RyuJitX64 } },
                { "dry", new[] { Job.Dry } },
                { "alljits", Job.AllJits },
                { "clr", new[] { Job.Clr } },
                { "mono", new[] { Job.Mono } },
                { "longrun", new[] { Job.LongRun } }
            };

        private static Dictionary<string, IColumn[]> availableColumns =
            new Dictionary<string, IColumn[]>
            {
                { "mean", new [] { StatisticColumn.Mean } },
                { "stderror", new[] { StatisticColumn.StdError } },
                { "stddev", new[] { StatisticColumn.StdDev } },
                { "operationpersecond", new [] { StatisticColumn.OperationsPerSecond } },
                { "min", new[] { StatisticColumn.Min } },
                { "q1", new[] { StatisticColumn.Q1 } },
                { "median", new[] { StatisticColumn.Median } },
                { "q3", new[] { StatisticColumn.Q3 } },
                { "max", new[] { StatisticColumn.Max } },
                { "allstatistics", StatisticColumn.AllStatistics  },
                { "place", new[] { PlaceColumn.ArabicNumber } }
            };

        public IConfig Parse(string[] args)
        {
            var config = new ManualConfig();
            foreach (var arg in args.Where(arg => arg.Contains("=")))
            {
                var split = arg.ToLowerInvariant().Split('=');
                var values = split[1].Split(',');
                // Delibrately allow both "jobs" and "job" to be specified, makes it easier for users!!
                var argument = split[0].EndsWith("s") ? split[0] : split[0] + "s";

                if (configuration.ContainsKey(argument) == false)
                    throw new InvalidOperationException($"\"{split[0]}\" (from \"{arg}\") is an unrecognised Option");

                var action = configuration[argument].ProcessOption;
                    foreach (var value in values)
                        action(config, value);
            }
            return config;
        }

        public bool ShouldDisplayOptions(string[] args)
        {
            return args.Select(a => a.ToLowerInvariant()).Any(a => a == "--help" || a == "-h");
        }

        public void PrintOptions(ILogger logger)
        {
            logger.WriteLineHeader($"{BenchmarkDotNetInfo.FullTitle}");
            logger.WriteLine();
            logger.WriteLineHeader("Options:");
            foreach (var option in configuration)
            {
                // TODO also consider allowing short version (i.e. '-d' and '--diagnosers')             
                var optionText = $"--{option.Key} <{option.Key.ToUpperInvariant()}>";
                var parameters = string.Join(", ", option.Value.GetAllOptions.Value);
                var explanation = $"Allowed values: {parameters}";
                logger.WriteLineInfo($"  {optionText,-30} {explanation}");
            }
        }

        private static T[] ParseItem<T>(string itemName, Dictionary<string, T[]> itemLookup, string value)
        {
            if (itemLookup.ContainsKey(value))
                return itemLookup[value];

            throw new InvalidOperationException($"\"{value}\" is an unrecognised {itemName}");
        }

        private static IDiagnoser[] ParseDiagnosers(string value)
        {
            foreach (var diagnoser in DefaultConfig.LazyLoadedDiagnosers.Value)
            {
                if (value == diagnoser.GetType().Name.Replace("Diagnoser", "").ToLowerInvariant())
                    return new[] { diagnoser };
            }
            throw new InvalidOperationException($"{value} is an unrecognised Diagnoser");
        }
    }
}
{'fraction_non_alphanumeric': 0.13200401885771698, 'fraction_numerical': 0.003787000541000077, 'mean_word_length': 1.9111361079865017, 'pattern_counts': {'":': 0, '<': 58, '<?xml version=': 0, '>': 87, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1674488', 'n_tokens_mistral': 3528, 'n_tokens_neox': 3319, 'n_words': 795}