diff --git a/src/BenchmarkDotNet/Analysers/LoggerAnalyzer.cs b/src/BenchmarkDotNet/Analysers/LoggerAnalyzer.cs new file mode 100644 index 0000000000..0f4328a8e8 --- /dev/null +++ b/src/BenchmarkDotNet/Analysers/LoggerAnalyzer.cs @@ -0,0 +1,56 @@ +using BenchmarkDotNet.Loggers; +using BenchmarkDotNet.Reports; +using System; +using System.Collections.Generic; + +namespace BenchmarkDotNet.Analysers +{ + public class LoggerAnalyzer : IAnalyser, ILogger + { + private int errorCount; + private int warningCount; + public static readonly LoggerAnalyzer Instance = new LoggerAnalyzer(); + + private LoggerAnalyzer() + { + + } + + public string Id => nameof(LoggerAnalyzer); + + public int Priority => throw new NotImplementedException(); + + public IEnumerable Analyse(Summary summary) + { + if (errorCount > 0) + { + yield return Conclusion.CreateError(Id, "There is one or more errors. See log for dettails."); + } + if (warningCount > 0) + { + yield return Conclusion.CreateWarning(Id, "There is one or more warning. See log for details."); + } + } + + public void Flush() { } + + public void Write(LogKind logKind, string text) + { + switch (logKind) + { + case LogKind.Error: + errorCount++; + break; + case LogKind.Warning: + warningCount++; + break; + default: + break; + } + } + + public void WriteLine() { } + + public void WriteLine(LogKind logKind, string text) => Write(logKind, text); + } +} diff --git a/src/BenchmarkDotNet/Configs/DefaultConfig.cs b/src/BenchmarkDotNet/Configs/DefaultConfig.cs index eb3fb6e80a..c3fc6e9d87 100644 --- a/src/BenchmarkDotNet/Configs/DefaultConfig.cs +++ b/src/BenchmarkDotNet/Configs/DefaultConfig.cs @@ -42,6 +42,7 @@ public IEnumerable GetLoggers() yield return LinqPadLogger.Instance; else yield return ConsoleLogger.Default; + yield return LoggerAnalyzer.Instance; } public IEnumerable GetAnalysers() @@ -53,6 +54,7 @@ public IEnumerable GetAnalysers() yield return RuntimeErrorAnalyser.Default; yield return ZeroMeasurementAnalyser.Default; yield return BaselineCustomAnalyzer.Default; + yield return LoggerAnalyzer.Instance; } public IEnumerable GetValidators() diff --git a/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs b/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs index d89e71868d..4343709f72 100644 --- a/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs +++ b/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs @@ -38,7 +38,7 @@ public static ImmutableConfig Create(IConfig source) var uniqueHardwareCounters = source.GetHardwareCounters().ToImmutableHashSet(); var uniqueDiagnosers = GetDiagnosers(source.GetDiagnosers(), uniqueHardwareCounters); - var uniqueExporters = GetExporters(source.GetExporters(), uniqueDiagnosers); + var uniqueExporters = GetExporters(source.GetExporters(), uniqueDiagnosers, uniqueLoggers); var uniqueAnalyzers = GetAnalysers(source.GetAnalysers(), uniqueDiagnosers); var uniqueValidators = GetValidators(source.GetValidators(), MandatoryValidators, source.Options); @@ -88,18 +88,49 @@ private static ImmutableHashSet GetDiagnosers(IEnumerable GetExporters(IEnumerable exporters, ImmutableHashSet uniqueDiagnosers) + private static ImmutableArray GetExporters(IEnumerable exporters, + ImmutableHashSet uniqueDiagnosers, + IEnumerable loggers = null) { - var result = new List(); + + void PrintWarning(string message) + { + if (loggers?.Any() == true) + { + foreach (var logger in loggers) + { + logger.WriteLine(Loggers.LogKind.Warning, + message); + } + } + } + + var mergeDictionary = new Dictionary(); foreach (var exporter in exporters) - if (!result.Contains(exporter)) - result.Add(exporter); + { + var exporterType = exporter.GetType(); + if (mergeDictionary.ContainsKey(exporterType)) + { + PrintWarning($"The exporter {exporterType} is already present in configuration. There may be unexpected results."); + } + mergeDictionary[exporterType] = exporter; + } + foreach (var diagnoser in uniqueDiagnosers) - foreach (var exporter in diagnoser.Exporters) - if (!result.Contains(exporter)) - result.Add(exporter); + foreach (var exporter in diagnoser.Exporters) + { + var exporterType = exporter.GetType(); + if (mergeDictionary.ContainsKey(exporterType)) + { + PrintWarning($"The exporter {exporterType} of {diagnoser.GetType().Name} is already present in configuration. There may be unexpected results."); + } + mergeDictionary[exporterType] = exporter; + }; + + var result = mergeDictionary.Values.ToList(); + var hardwareCounterDiagnoser = uniqueDiagnosers.OfType().SingleOrDefault(); var disassemblyDiagnoser = uniqueDiagnosers.OfType().SingleOrDefault(); @@ -111,8 +142,31 @@ private static ImmutableArray GetExporters(IEnumerable exp for (int i = result.Count - 1; i >=0; i--) if (result[i] is IExporterDependencies exporterDependencies) foreach (var dependency in exporterDependencies.Dependencies) - if (!result.Contains(dependency)) + /* + * When exporter that depends on an other already present in the configuration print warning. + * + * Example: + * + * // Global Current Culture separator is Semicolon; + * [CsvMeasurementsExporter(CsvSeparator.Comma)] // force use Comma + * [RPlotExporter] + * public class MyBanch + * { + * + * } + * + * RPlotExporter is depend from CsvMeasurementsExporter.Default + * + * On active logger will by print: + * "The CsvMeasurementsExporter is already present in the configuration. There may be unexpected results of RPlotExporter. + * + */ + if (!result.Any(exporter=> exporter.GetType() == dependency.GetType())) result.Insert(i, dependency); // All the exporter dependencies should be added before the exporter + else + { + PrintWarning($"The {dependency.Name} is already present in the configuration. There may be unexpected results of {result[i].GetType().Name}."); + } result.Sort((left, right) => (left is IExporterDependencies).CompareTo(right is IExporterDependencies)); // the case when they were defined by user in wrong order ;) diff --git a/src/BenchmarkDotNet/Loggers/ConsoleLogger.cs b/src/BenchmarkDotNet/Loggers/ConsoleLogger.cs index 8d9a96e939..0a9520f964 100644 --- a/src/BenchmarkDotNet/Loggers/ConsoleLogger.cs +++ b/src/BenchmarkDotNet/Loggers/ConsoleLogger.cs @@ -71,7 +71,8 @@ private static Dictionary CreateColorfulScheme() => { LogKind.Statistic, ConsoleColor.Cyan }, { LogKind.Info, ConsoleColor.DarkYellow }, { LogKind.Error, ConsoleColor.Red }, - { LogKind.Hint, ConsoleColor.DarkCyan } + { LogKind.Hint, ConsoleColor.DarkCyan }, + { LogKind.Warning, ConsoleColor.Yellow } }; [PublicAPI] diff --git a/src/BenchmarkDotNet/Loggers/LinqPadLogger.cs b/src/BenchmarkDotNet/Loggers/LinqPadLogger.cs index 930d19d4ca..d4bb2f873f 100644 --- a/src/BenchmarkDotNet/Loggers/LinqPadLogger.cs +++ b/src/BenchmarkDotNet/Loggers/LinqPadLogger.cs @@ -82,7 +82,8 @@ private static IReadOnlyDictionary CreateDarkScheme() => { LogKind.Statistic, "#00FFFF" }, { LogKind.Info, "#808000" }, { LogKind.Error, "#FF0000" }, - { LogKind.Hint, "#008080" } + { LogKind.Hint, "#008080" }, + { LogKind.Warning, "#FFFF00" }, }; private static IReadOnlyDictionary CreateLightScheme() => @@ -95,7 +96,8 @@ private static IReadOnlyDictionary CreateLightScheme() => { LogKind.Statistic, "#008080" }, { LogKind.Info, "#808000" }, { LogKind.Error, "#FF0000" }, - { LogKind.Hint, "#008080" } + { LogKind.Hint, "#008080" }, + { LogKind.Warning, "#FFFF00" }, }; } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Loggers/LogKind.cs b/src/BenchmarkDotNet/Loggers/LogKind.cs index 8f3dff72a8..8fa4660aa8 100644 --- a/src/BenchmarkDotNet/Loggers/LogKind.cs +++ b/src/BenchmarkDotNet/Loggers/LogKind.cs @@ -2,6 +2,6 @@ { public enum LogKind { - Default, Help, Header, Result, Statistic, Info, Error, Hint + Default, Help, Header, Result, Statistic, Info, Error, Hint, Warning } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Running/BenchmarkConverter.cs b/src/BenchmarkDotNet/Running/BenchmarkConverter.cs index fdca381fc8..2ed65be581 100644 --- a/src/BenchmarkDotNet/Running/BenchmarkConverter.cs +++ b/src/BenchmarkDotNet/Running/BenchmarkConverter.cs @@ -79,7 +79,7 @@ private static ImmutableConfig GetFullTypeConfig(Type type, IConfig config) var typeAttributes = type.GetCustomAttributes(true).OfType(); var assemblyAttributes = type.Assembly.GetCustomAttributes().OfType(); - foreach (var configFromAttribute in typeAttributes.Concat(assemblyAttributes)) + foreach (var configFromAttribute in assemblyAttributes.Concat(typeAttributes)) config = ManualConfig.Union(config, configFromAttribute.Config); return ImmutableConfigBuilder.Create(config); diff --git a/tests/BenchmarkDotNet.Tests/Configs/ImmutableConfigTests.cs b/tests/BenchmarkDotNet.Tests/Configs/ImmutableConfigTests.cs index df068ac504..566618e180 100644 --- a/tests/BenchmarkDotNet.Tests/Configs/ImmutableConfigTests.cs +++ b/tests/BenchmarkDotNet.Tests/Configs/ImmutableConfigTests.cs @@ -331,5 +331,47 @@ public class TestExporterDependency : IExporter public string Name => nameof(TestExporterDependency); public void ExportToLog(Summary summary, ILogger logger) { } } + + [Fact] + public void GenerateWarningWhenExporterDependencyAlreadyExistInConfig() + { + System.Globalization.CultureInfo currentCulture = default; + System.Globalization.CultureInfo currentUICulture = default; + { + var ct = System.Threading.Thread.CurrentThread; + currentCulture = ct.CurrentCulture; + currentUICulture = ct.CurrentUICulture; + ct.CurrentCulture = System.Globalization.CultureInfo.InvariantCulture; + ct.CurrentUICulture = System.Globalization.CultureInfo.InvariantCulture; + } + try + { + var countWarning = 0; + var logger = new Loggers.DelegateLogger((kind, text) => + { + if (kind == LogKind.Warning) + { + countWarning++; + } + }); + + var mutable = ManualConfig.CreateEmpty(); + mutable.AddLogger(logger); + mutable.AddExporter(new BenchmarkDotNet.Exporters.Csv.CsvMeasurementsExporter(BenchmarkDotNet.Exporters.Csv.CsvSeparator.Comma)); + mutable.AddExporter(RPlotExporter.Default); + + var final = ImmutableConfigBuilder.Create(mutable); + + Assert.Equal(1, countWarning); + } + finally + { + var ct = System.Threading.Thread.CurrentThread; + ct.CurrentCulture = currentCulture; + ct.CurrentUICulture = currentUICulture; + + } + + } } } diff --git a/tests/BenchmarkDotNet.Tests/Loggers/DelegateLogger.cs b/tests/BenchmarkDotNet.Tests/Loggers/DelegateLogger.cs new file mode 100644 index 0000000000..5d48c982a9 --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Loggers/DelegateLogger.cs @@ -0,0 +1,28 @@ +using BenchmarkDotNet.Loggers; +using System; + +namespace BenchmarkDotNet.Tests.Loggers +{ + internal class DelegateLogger : ILogger + { + private readonly Action writeAction; + + public DelegateLogger(Action writeAction) + { + this.writeAction = writeAction; + } + + public string Id { get; } + public int Priority => 0; + + public virtual void Write(LogKind logKind, string text) + => writeAction?.Invoke(logKind, text); + + public virtual void WriteLine() { } + + public virtual void WriteLine(LogKind logKind, string text) + => writeAction?.Invoke(logKind, string.Empty); + + public void Flush() { } + } +}