diff --git a/samples/BenchmarkDotNet.Samples/IntroRenameTest.cs b/samples/BenchmarkDotNet.Samples/IntroRenameTest.cs new file mode 100644 index 0000000000..525e16f1dc --- /dev/null +++ b/samples/BenchmarkDotNet.Samples/IntroRenameTest.cs @@ -0,0 +1,22 @@ +using BenchmarkDotNet.Attributes; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace BenchmarkDotNet.Samples +{ + [BenchmarkDescription("Used to be 'IntroRenameTest', now is 'My Renamed Test'")] + public class IntroRenameTest + { + // And define a method with the Benchmark attribute + [Benchmark] + public void Sleep() => Thread.Sleep(10); + + // You can write a description for your method. + [Benchmark(Description = "Thread.Sleep(10)")] + public void SleepWithDescription() => Thread.Sleep(10); + } +} diff --git a/src/BenchmarkDotNet.Annotations/Attributes/BenchmarkDescriptionAttribute.cs b/src/BenchmarkDotNet.Annotations/Attributes/BenchmarkDescriptionAttribute.cs new file mode 100644 index 0000000000..1b3aad7f07 --- /dev/null +++ b/src/BenchmarkDotNet.Annotations/Attributes/BenchmarkDescriptionAttribute.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace BenchmarkDotNet.Attributes +{ + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] + public class BenchmarkDescriptionAttribute : Attribute + { + public BenchmarkDescriptionAttribute(){ } + public BenchmarkDescriptionAttribute(string description) + => Description = description; + + public string Description { get; set; } + } +} diff --git a/src/BenchmarkDotNet/Columns/TargetMethodColumn.cs b/src/BenchmarkDotNet/Columns/TargetMethodColumn.cs index f6bcb3ef31..d0b9da6da7 100644 --- a/src/BenchmarkDotNet/Columns/TargetMethodColumn.cs +++ b/src/BenchmarkDotNet/Columns/TargetMethodColumn.cs @@ -8,7 +8,7 @@ namespace BenchmarkDotNet.Columns public class TargetMethodColumn : IColumn { public static readonly IColumn Namespace = new TargetMethodColumn(Column.Namespace, benchmark => benchmark.Descriptor.Type.Namespace); - public static readonly IColumn Type = new TargetMethodColumn(Column.Type, benchmark => benchmark.Descriptor.Type.GetDisplayName()); + public static readonly IColumn Type = new TargetMethodColumn(Column.Type, benchmark => benchmark.Descriptor.TypeInfo); public static readonly IColumn Method = new TargetMethodColumn(Column.Method, benchmark => benchmark.Descriptor.WorkloadMethodDisplayInfo, true); private readonly Func valueProvider; diff --git a/src/BenchmarkDotNet/Extensions/ReflectionExtensions.cs b/src/BenchmarkDotNet/Extensions/ReflectionExtensions.cs index cf1f71a166..5ce60cbe85 100644 --- a/src/BenchmarkDotNet/Extensions/ReflectionExtensions.cs +++ b/src/BenchmarkDotNet/Extensions/ReflectionExtensions.cs @@ -102,6 +102,10 @@ private static IEnumerable GetNestedTypeNames(Type type, bool includeGen /// private static string GetDisplayName(this TypeInfo typeInfo) { + var customAttr = typeInfo.GetCustomAttribute(); + if (customAttr != null) + return customAttr.Description; + if (!typeInfo.IsGenericType) return typeInfo.Name; diff --git a/src/BenchmarkDotNet/Running/BenchmarkConverter.cs b/src/BenchmarkDotNet/Running/BenchmarkConverter.cs index dbe350b2c8..fa2523e1c8 100644 --- a/src/BenchmarkDotNet/Running/BenchmarkConverter.cs +++ b/src/BenchmarkDotNet/Running/BenchmarkConverter.cs @@ -127,6 +127,7 @@ private static IEnumerable GetTargets( GetTargetedMatchingMethod(methodInfo, iterationSetupMethods), GetTargetedMatchingMethod(methodInfo, iterationCleanupMethods), methodInfo.ResolveAttribute(), + methodInfo.ResolveAttribute(), targetMethods, config)); } @@ -155,10 +156,15 @@ private static Descriptor CreateDescriptor( MethodInfo iterationSetupMethod, MethodInfo iterationCleanupMethod, BenchmarkAttribute attr, + BenchmarkDescriptionAttribute? methodDescription, MethodInfo[] targetMethods, IConfig config) { var categoryDiscoverer = config.CategoryDiscoverer ?? DefaultCategoryDiscoverer.Instance; + if (attr?.Description != null && methodDescription?.Description != null) + throw new InvalidOperationException($"Benchmark {methodInfo.Name} has 2 descriptions from different attributes"); + string description = attr?.Description; + description ??= methodDescription?.Description; var target = new Descriptor( type, methodInfo, @@ -166,7 +172,7 @@ private static Descriptor CreateDescriptor( globalCleanupMethod, iterationSetupMethod, iterationCleanupMethod, - attr.Description, + description, baseline: attr.Baseline, categories: categoryDiscoverer.GetCategories(methodInfo), operationsPerInvoke: attr.OperationsPerInvoke, diff --git a/src/BenchmarkDotNet/Running/Descriptor.cs b/src/BenchmarkDotNet/Running/Descriptor.cs index a7fd0cd7ac..b72db09084 100644 --- a/src/BenchmarkDotNet/Running/Descriptor.cs +++ b/src/BenchmarkDotNet/Running/Descriptor.cs @@ -1,6 +1,8 @@ using System; +using System.Data; using System.Linq; using System.Reflection; +using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Extensions; using BenchmarkDotNet.Helpers; using BenchmarkDotNet.Portability; diff --git a/tests/BenchmarkDotNet.Tests/Exporters/JobBaseline_WithAttribute.cs b/tests/BenchmarkDotNet.Tests/Exporters/JobBaseline_WithAttribute.cs new file mode 100644 index 0000000000..0b1aaaf959 --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Exporters/JobBaseline_WithAttribute.cs @@ -0,0 +1,14 @@ +using BenchmarkDotNet.Attributes; + +namespace BenchmarkDotNet.Tests.Exporters +{ + [RankColumn, LogicalGroupColumn, BaselineColumn] + [SimpleJob(id: "Job1"), SimpleJob(id: "Job2")] + [BenchmarkDescription(Description = "MyRenamedTestCase")] + public class JobBaseline_MethodsJobs_WithAttribute + { + [Benchmark] public void Base() { } + [Benchmark] public void Foo() { } + [Benchmark] public void Bar() { } + } +} diff --git a/tests/BenchmarkDotNet.Tests/Exporters/MarkdownExporterVerifyTests.cs b/tests/BenchmarkDotNet.Tests/Exporters/MarkdownExporterVerifyTests.cs index 950f8498f8..23a7daf465 100644 --- a/tests/BenchmarkDotNet.Tests/Exporters/MarkdownExporterVerifyTests.cs +++ b/tests/BenchmarkDotNet.Tests/Exporters/MarkdownExporterVerifyTests.cs @@ -15,6 +15,8 @@ using VerifyXunit; using Xunit; +using static BenchmarkDotNet.Tests.Exporters.MarkdownExporterVerifyTests.BaselinesBenchmarks; + namespace BenchmarkDotNet.Tests.Exporters { [Collection("VerifyTests")] @@ -57,8 +59,31 @@ public Task GroupExporterTest(Type benchmarkType) var settings = VerifySettingsFactory.Create(); settings.UseTextForParameters(benchmarkType.Name); return Verifier.Verify(logger.GetLog(), settings); - } - + } + + [Fact] + public Task GroupExporterMultipleTypesTest() + { + Type[] benchmarkTypes = new Type[] { typeof(JobBaseline_MethodsJobs_WithAttribute), typeof(JobBaseline_MethodsJobs) }; + Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; + var logger = new AccumulationLogger(); + logger.WriteLine("=== " + benchmarkTypes + " ==="); + + var exporter = MarkdownExporter.Mock; + var summary = MockFactory.CreateSummary(benchmarkTypes); + exporter.ExportToLog(summary, logger); + + var validator = BaselineValidator.FailOnError; + var errors = validator.Validate(new ValidationParameters(summary.BenchmarksCases, summary.BenchmarksCases.First().Config)).ToList(); + logger.WriteLine(); + logger.WriteLine("Errors: " + errors.Count); + foreach (var error in errors) + logger.WriteLineError("* " + error.Message); + + var settings = VerifySettingsFactory.Create(); + return Verifier.Verify(logger.GetLog(), settings); + } + public void Dispose() => Thread.CurrentThread.CurrentCulture = initCulture; [SuppressMessage("ReSharper", "InconsistentNaming")] diff --git a/tests/BenchmarkDotNet.Tests/Exporters/VerifiedFiles/MarkdownExporterApprovalTests.GroupExporterTest.JobBaseline_RenameJob_MethodsJobs.approved.txt b/tests/BenchmarkDotNet.Tests/Exporters/VerifiedFiles/MarkdownExporterApprovalTests.GroupExporterTest.JobBaseline_RenameJob_MethodsJobs.approved.txt new file mode 100644 index 0000000000..e8edaa833f --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Exporters/VerifiedFiles/MarkdownExporterApprovalTests.GroupExporterTest.JobBaseline_RenameJob_MethodsJobs.approved.txt @@ -0,0 +1,20 @@ +=== JobBaseline_RenameJob_MethodsJobs === + +BenchmarkDotNet=v0.10.x-mock, OS=Microsoft Windows NT 10.0.x.mock, VM=Hyper-V +MockIntel Core i7-6700HQ CPU 2.60GHz (Max: 3.10GHz), 1 CPU, 8 logical and 4 physical cores +Frequency=2531248 Hz, Resolution=395.0620 ns, Timer=TSC + [Host] : Clr 4.0.x.mock, 64mock RyuJIT-v4.6.x.mock CONFIGURATION + Job1 : extra output line + Job2 : extra output line + + + Method | Job | Mean | Error | StdDev | Rank | LogicalGroup | Baseline | +------- |----- |---------:|--------:|--------:|-----:|------------- |--------- | + Base | Job1 | 102.0 ns | 6.09 ns | 1.58 ns | 1 | * | No | + Foo | Job1 | 202.0 ns | 6.09 ns | 1.58 ns | 2 | * | No | + Bar | Job1 | 302.0 ns | 6.09 ns | 1.58 ns | 3 | * | No | + Base | Job2 | 402.0 ns | 6.09 ns | 1.58 ns | 4 | * | No | + Foo | Job2 | 502.0 ns | 6.09 ns | 1.58 ns | 5 | * | No | + Bar | Job2 | 602.0 ns | 6.09 ns | 1.58 ns | 6 | * | No | + +Errors: 0 diff --git a/tests/BenchmarkDotNet.Tests/Exporters/VerifiedFiles/MarkdownExporterVerifyTests.GroupExporterMultipleTypesTest.verified.txt b/tests/BenchmarkDotNet.Tests/Exporters/VerifiedFiles/MarkdownExporterVerifyTests.GroupExporterMultipleTypesTest.verified.txt new file mode 100644 index 0000000000..2e5dcab0a0 --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Exporters/VerifiedFiles/MarkdownExporterVerifyTests.GroupExporterMultipleTypesTest.verified.txt @@ -0,0 +1,31 @@ +=== System.Type[] === + +BenchmarkDotNet v0.10.x-mock, Microsoft Windows NT 10.0.x.mock (Hyper-V) +MockIntel Core i7-6700HQ CPU 2.60GHz (Max: 3.10GHz), 1 CPU, 8 logical and 4 physical cores +Frequency: 2531248 Hz, Resolution: 395.0620 ns, Timer: TSC + [Host] : Clr 4.0.x.mock, 64mock RyuJIT-v4.6.x.mock CONFIGURATION + Job1 : extra output line + Job2 : extra output line + + + Type | Method | Job | Mean | Error | StdDev | Ratio | RatioSD | Rank | LogicalGroup | Baseline | +------------------------ |------- |----- |-----------:|--------:|--------:|------:|--------:|-----:|----------------------------- |--------- | + MyRenamedTestCase | Base | Job1 | 102.0 ns | 6.09 ns | 1.58 ns | ? | ? | 1 | MyRenamedTestCase.Base | No | + MyRenamedTestCase | Base | Job2 | 402.0 ns | 6.09 ns | 1.58 ns | ? | ? | 2 | MyRenamedTestCase.Base | No | + | | | | | | | | | | | + MyRenamedTestCase | Foo | Job1 | 202.0 ns | 6.09 ns | 1.58 ns | ? | ? | 1 | MyRenamedTestCase.Foo | No | + MyRenamedTestCase | Foo | Job2 | 502.0 ns | 6.09 ns | 1.58 ns | ? | ? | 2 | MyRenamedTestCase.Foo | No | + | | | | | | | | | | | + MyRenamedTestCase | Bar | Job1 | 302.0 ns | 6.09 ns | 1.58 ns | ? | ? | 1 | MyRenamedTestCase.Bar | No | + MyRenamedTestCase | Bar | Job2 | 602.0 ns | 6.09 ns | 1.58 ns | ? | ? | 2 | MyRenamedTestCase.Bar | No | + | | | | | | | | | | | + JobBaseline_MethodsJobs | Base | Job1 | 702.0 ns | 6.09 ns | 1.58 ns | 1.00 | 0.00 | 1 | JobBaseline_MethodsJobs.Base | Yes | + JobBaseline_MethodsJobs | Base | Job2 | 1,002.0 ns | 6.09 ns | 1.58 ns | 1.43 | 0.00 | 2 | JobBaseline_MethodsJobs.Base | No | + | | | | | | | | | | | + JobBaseline_MethodsJobs | Foo | Job1 | 802.0 ns | 6.09 ns | 1.58 ns | 1.00 | 0.00 | 1 | JobBaseline_MethodsJobs.Foo | Yes | + JobBaseline_MethodsJobs | Foo | Job2 | 1,102.0 ns | 6.09 ns | 1.58 ns | 1.37 | 0.00 | 2 | JobBaseline_MethodsJobs.Foo | No | + | | | | | | | | | | | + JobBaseline_MethodsJobs | Bar | Job1 | 902.0 ns | 6.09 ns | 1.58 ns | 1.00 | 0.00 | 1 | JobBaseline_MethodsJobs.Bar | Yes | + JobBaseline_MethodsJobs | Bar | Job2 | 1,202.0 ns | 6.09 ns | 1.58 ns | 1.33 | 0.00 | 2 | JobBaseline_MethodsJobs.Bar | No | + +Errors: 0 diff --git a/tests/BenchmarkDotNet.Tests/Mocks/MockFactory.cs b/tests/BenchmarkDotNet.Tests/Mocks/MockFactory.cs index b51f978edf..159fb00cb0 100644 --- a/tests/BenchmarkDotNet.Tests/Mocks/MockFactory.cs +++ b/tests/BenchmarkDotNet.Tests/Mocks/MockFactory.cs @@ -16,22 +16,23 @@ namespace BenchmarkDotNet.Tests.Mocks { public static class MockFactory - { - public static Summary CreateSummary(Type benchmarkType) - { - var runInfo = BenchmarkConverter.TypeToBenchmarks(benchmarkType); - return new Summary( - "MockSummary", - runInfo.BenchmarksCases.Select((benchmark, index) => CreateReport(benchmark, 5, (index + 1) * 100)).ToImmutableArray(), - new HostEnvironmentInfoBuilder().WithoutDotNetSdkVersion().Build(), - string.Empty, - string.Empty, - TimeSpan.FromMinutes(1), - TestCultureInfo.Instance, - ImmutableArray.Empty, - ImmutableArray.Empty); - } - + { + public static Summary CreateSummary(Type benchmarkType) => CreateSummary(new Type[] { benchmarkType }); + public static Summary CreateSummary(Type[] benchmarkTypes) + { + var runInfos = benchmarkTypes.Select(benchmarkType => BenchmarkConverter.TypeToBenchmarks(benchmarkType)); + return new Summary( + "MockSummary", + runInfos.SelectMany(runInfo => runInfo.BenchmarksCases).Select((benchmark, index) => CreateReport(benchmark, 5, (index + 1) * 100)).ToImmutableArray(), + new HostEnvironmentInfoBuilder().WithoutDotNetSdkVersion().Build(), + string.Empty, + string.Empty, + TimeSpan.FromMinutes(1), + TestCultureInfo.Instance, + ImmutableArray.Empty, + ImmutableArray.Empty); + } + public static Summary CreateSummary(IConfig config) => new Summary( "MockSummary", CreateReports(config), diff --git a/tests/BenchmarkDotNet.Tests/Running/BenchmarkConverterTests.cs b/tests/BenchmarkDotNet.Tests/Running/BenchmarkConverterTests.cs index 4eb72b5d59..5875b48629 100644 --- a/tests/BenchmarkDotNet.Tests/Running/BenchmarkConverterTests.cs +++ b/tests/BenchmarkDotNet.Tests/Running/BenchmarkConverterTests.cs @@ -212,6 +212,29 @@ public void MethodDeclarationOrderIsPreserved() Assert.Equal(nameof(BAC.C), info.BenchmarksCases[2].Descriptor.WorkloadMethod.Name); } } + [Fact] + public void OnlyOneOfAttributeDescriptionIsUsed() + { + Assert.Throws(() => BenchmarkConverter.TypeToBenchmarks(typeof(BothAttributeDescriptionTests))); + } + [Fact] + public void DescriptorDescriptionNameOverride() + { + var description = BenchmarkConverter.TypeToBenchmarks(typeof(MethodDescriptionOverrideTests)); + + Assert.Equal("VoidTest", description.BenchmarksCases[0].Descriptor.WorkloadMethodDisplayInfo); + Assert.Equal("\'from Benchmark\'", description.BenchmarksCases[1].Descriptor.WorkloadMethodDisplayInfo); + Assert.Equal("OverrideFromAttribute", description.BenchmarksCases[2].Descriptor.WorkloadMethodDisplayInfo); + } + [Fact] + public void ClassDescriptorDescriptionNameOverride() + { + var description = BenchmarkConverter.TypeToBenchmarks(typeof(ClassDescriptionOverrideTests)); + + Assert.Equal("FromClassDescription", description.BenchmarksCases[0].Descriptor.WorkloadMethodDisplayInfo); + Assert.Equal("\'from Benchmark\'", description.BenchmarksCases[1].Descriptor.WorkloadMethodDisplayInfo); + Assert.Equal("OverrideFromAttribute", description.BenchmarksCases[2].Descriptor.WorkloadMethodDisplayInfo); + } public class BAC { @@ -278,5 +301,36 @@ public class PrivateIterationCleanup [IterationCleanup] private void X() { } [Benchmark] public void A() { } } + public class BothAttributeDescriptionTests + { + [Benchmark(Description = "BenchmarkAttributeDescription")] + [BenchmarkDescription("BenchmarkDescriptionAttributeDescription")] + public void BothDescriptionsUsed() { } + } + public class MethodDescriptionOverrideTests + { + [Benchmark] + public void VoidTest() { } + + [Benchmark(Description = "from Benchmark")] + public void BenchmarkAttributeOverride() {} + + [Benchmark] + [BenchmarkDescription("OverrideFromAttribute")] + public void BenchmarkDescriptionAttributeOverride() { } + } + [BenchmarkDescription("FromClassDescription")] + public class ClassDescriptionOverrideTests + { + [Benchmark] + public void VoidTest() { } + + [Benchmark(Description = "from Benchmark")] + public void ClassBenchmarkAttributeOverride() { } + + [Benchmark] + [BenchmarkDescription("OverrideFromAttribute")] + public void ClassBenchmarkDescriptionAttributeOverride() { } + } } }