diff --git a/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs b/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs index 0a125900f4..7acfc89392 100644 --- a/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs +++ b/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs @@ -51,7 +51,7 @@ public static class ConfigParser [SuppressMessage("ReSharper", "StringLiteralTypo")] [SuppressMessage("ReSharper", "CoVariantArrayConversion")] - private static readonly IReadOnlyDictionary AvailableExporters = + private static readonly IDictionary AvailableExporters = new Dictionary(StringComparer.InvariantCultureIgnoreCase) { { "csv", new[] { CsvExporter.Default } }, @@ -72,6 +72,21 @@ public static class ConfigParser { "fullxml", new[] { XmlExporter.Full } } }; + + private static bool TryCreateCustomExporter(string customExporterName) + { + try + { + var customExporter = Activator.CreateInstance(Type.GetType(customExporterName)); + AvailableExporters.Add(customExporterName, new IExporter[] { (IExporter)customExporter }); + return true; + } + catch (Exception) + { + return false; + } + } + public static (bool isSuccess, IConfig config, CommandLineOptions options) Parse(string[] args, ILogger logger, IConfig? globalConfig = null) { (bool isSuccess, IConfig config, CommandLineOptions options) result = default; @@ -253,11 +268,17 @@ private static bool Validate(CommandLineOptions options, ILogger logger) } foreach (string exporter in options.Exporters) - if (!AvailableExporters.ContainsKey(exporter)) + { + if (AvailableExporters.ContainsKey(exporter) || TryCreateCustomExporter(exporter)) + { + continue; + } + else { - logger.WriteLineError($"The provided exporter \"{exporter}\" is invalid. Available options are: {string.Join(", ", AvailableExporters.Keys)}."); + logger.WriteLineError($"The provided exporter \"{exporter}\" is invalid. Available options are: {string.Join(", ", AvailableExporters.Keys)} or custom exporter by assembly-qualified name."); return false; } + } if (options.CliPath.IsNotNullButDoesNotExist()) { diff --git a/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs b/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs index ec9b85021f..283135c661 100644 --- a/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs +++ b/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs @@ -25,15 +25,37 @@ using Perfolizer.Horology; using Perfolizer.Mathematics.SignificanceTesting; using Perfolizer.Mathematics.Thresholds; +using BenchmarkDotNet.Exporters.Json; +using BenchmarkDotNet.Exporters.Xml; namespace BenchmarkDotNet.Tests { + public class CustomExporterTestClass : JsonExporterBase { } public class ConfigParserTests { public ITestOutputHelper Output { get; } public ConfigParserTests(ITestOutputHelper output) => Output = output; + [Theory] + [InlineData("--exporters", "BenchmarkDotNet.Tests.CustomExporterTestClass, BenchmarkDotNet.Tests", "html", "xml")] + [InlineData("--exporters", "html", "BenchmarkDotNet.Tests.CustomExporterTestClass, BenchmarkDotNet.Tests", "xml")] + [InlineData("--exporters", "html", "xml", "BenchmarkDotNet.Tests.CustomExporterTestClass, BenchmarkDotNet.Tests")] + public void CustomExporterConfigParsedCorrectly(params string[] args) + { + var config = ConfigParser.Parse(args, new OutputLogger(Output)).config; + + Assert.Equal(3, config.GetExporters().Count()); + Assert.Contains(typeof(CustomExporterTestClass).Name, config.GetExporters().Select(e => e.Name)); + Assert.Contains(HtmlExporter.Default, config.GetExporters()); + Assert.Contains(XmlExporter.Default, config.GetExporters()); + + Assert.Empty(config.GetColumnProviders()); + Assert.Empty(config.GetDiagnosers()); + Assert.Empty(config.GetAnalysers()); + Assert.Empty(config.GetLoggers()); + } + [Theory] [InlineData("--job=dry", "--exporters", "html", "rplot")] [InlineData("--JOB=dry", "--EXPORTERS", "html", "rplot")] // case insensitive