diff --git a/src/BenchmarkDotNet/Extensions/ReflectionExtensions.cs b/src/BenchmarkDotNet/Extensions/ReflectionExtensions.cs index 4f2eaf0dc4..1bfaf346ce 100644 --- a/src/BenchmarkDotNet/Extensions/ReflectionExtensions.cs +++ b/src/BenchmarkDotNet/Extensions/ReflectionExtensions.cs @@ -112,6 +112,34 @@ private static IEnumerable GetNestedTypeNames(Type type, bool includeGen /// internal static string GetDisplayName(this Type type) => GetDisplayName(type.GetTypeInfo()); + /// + /// Returns a display name per type, aligned with . When more than one + /// type shares the same simple (e.g. same class name in + /// different namespaces), the colliding names are qualified with their namespace so they can be + /// told apart; unambiguous names are left as-is. + /// + internal static string[] GetDisambiguatedDisplayNames(this IReadOnlyList types) + { + var simpleNames = new string[types.Count]; + for (int i = 0; i < types.Count; i++) + simpleNames[i] = types[i].GetDisplayName(); + + var ambiguousNames = new HashSet( + simpleNames.GroupBy(name => name).Where(group => group.Count() > 1).Select(group => group.Key), + StringComparer.Ordinal); + + var displayNames = new string[types.Count]; + for (int i = 0; i < types.Count; i++) + { + string? fullName = types[i].FullName; + displayNames[i] = ambiguousNames.Contains(simpleNames[i]) && !string.IsNullOrEmpty(fullName) + ? fullName + : simpleNames[i]; + } + + return displayNames; + } + /// /// returns simple, human friendly display name /// diff --git a/src/BenchmarkDotNet/Running/UserInteraction.cs b/src/BenchmarkDotNet/Running/UserInteraction.cs index d706110142..0bdc99314c 100644 --- a/src/BenchmarkDotNet/Running/UserInteraction.cs +++ b/src/BenchmarkDotNet/Running/UserInteraction.cs @@ -21,11 +21,12 @@ public void PrintNoBenchmarksError(ILogger logger) public IReadOnlyList AskUser(IReadOnlyList allTypes, ILogger logger) { var selectedTypes = new List(); - string benchmarkCaptionExample = allTypes.First().GetDisplayName(); + var displayNames = allTypes.GetDisambiguatedDisplayNames(); + string benchmarkCaptionExample = displayNames[0]; while (selectedTypes.Count == 0 && !consoleCancelKeyPressed) { - PrintAvailable(allTypes, logger); + PrintAvailable(allTypes, displayNames, logger); if (consoleCancelKeyPressed) break; @@ -42,7 +43,7 @@ public IReadOnlyList AskUser(IReadOnlyList allTypes, ILogger logger) break; } - selectedTypes.AddRange(GetMatching(allTypes, userInput.Split([' '], StringSplitOptions.RemoveEmptyEntries))); + selectedTypes.AddRange(GetMatching(allTypes, displayNames, userInput.Split([' '], StringSplitOptions.RemoveEmptyEntries))); logger.WriteLine(); } @@ -81,7 +82,7 @@ public void PrintWrongFilterInfo(IReadOnlyList allTypes, ILogger logger, s logger.WriteLineInfo("To learn more about filtering use `--help`."); } - private static IEnumerable GetMatching(IReadOnlyList allTypes, string[] userInput) + private static IEnumerable GetMatching(IReadOnlyList allTypes, string[] displayNames, string[] userInput) { if (userInput.IsEmpty()) yield break; @@ -93,7 +94,7 @@ private static IEnumerable GetMatching(IReadOnlyList allTypes, strin { var type = allTypes[i]; - if (stringInput.Any(arg => type.GetDisplayName().ContainsWithIgnoreCase(arg)) + if (stringInput.Any(arg => displayNames[i].ContainsWithIgnoreCase(arg)) || stringInput.Contains($"#{i}") || integerInput.Contains($"{i}") || stringInput.Contains("*")) @@ -105,13 +106,13 @@ private static IEnumerable GetMatching(IReadOnlyList allTypes, strin static bool IsInteger(string str) => int.TryParse(str, out _); } - private static void PrintAvailable(IReadOnlyList allTypes, ILogger logger) + private static void PrintAvailable(IReadOnlyList allTypes, string[] displayNames, ILogger logger) { logger.WriteLineHelp($"Available Benchmark{(allTypes.Count > 1 ? "s" : "")}:"); int numberWidth = allTypes.Count.ToString().Length; for (int i = 0; i < allTypes.Count && !consoleCancelKeyPressed; i++) - logger.WriteLineHelp(string.Format(CultureInfo.InvariantCulture, " #{0} {1}", i.ToString().PadRight(numberWidth), allTypes[i].GetDisplayName())); + logger.WriteLineHelp(string.Format(CultureInfo.InvariantCulture, " #{0} {1}", i.ToString().PadRight(numberWidth), displayNames[i])); if (!consoleCancelKeyPressed) { diff --git a/tests/BenchmarkDotNet.Tests/ReflectionTests.cs b/tests/BenchmarkDotNet.Tests/ReflectionTests.cs index 17b6a78d9a..e39c71af82 100644 --- a/tests/BenchmarkDotNet.Tests/ReflectionTests.cs +++ b/tests/BenchmarkDotNet.Tests/ReflectionTests.cs @@ -125,6 +125,34 @@ private static void CheckCorrectDisplayName(string expectedName, Type type) Assert.Equal(expectedName, type.GetDisplayName()); } + [Fact] + public void GetDisambiguatedDisplayNamesQualifiesCollidingNamesWithNamespace() + { + var types = new[] + { + typeof(Foo.Fixture), + typeof(Bar.Fixture), + typeof(ReflectionTests), + }; + + var displayNames = types.GetDisambiguatedDisplayNames(); + + Assert.Equal("BenchmarkDotNet.Tests.Foo.Fixture", displayNames[0]); + Assert.Equal("BenchmarkDotNet.Tests.Bar.Fixture", displayNames[1]); + Assert.Equal("ReflectionTests", displayNames[2]); + } + + [Fact] + public void GetDisambiguatedDisplayNamesLeavesUniqueNamesUnqualified() + { + var types = new[] { typeof(Foo.Fixture), typeof(ReflectionTests) }; + + var displayNames = types.GetDisambiguatedDisplayNames(); + + Assert.Equal("Fixture", displayNames[0]); + Assert.Equal("ReflectionTests", displayNames[1]); + } + [Fact] public void OnlyClosedGenericsWithPublicParameterlessCtorsAreSupported() { @@ -175,4 +203,14 @@ public static implicit operator StackOnlyStruct(WithImplicitCastToStackOnlySt => new StackOnlyStruct { Span = instance.Array }; } } +} + +namespace BenchmarkDotNet.Tests.Foo +{ + public class Fixture { } +} + +namespace BenchmarkDotNet.Tests.Bar +{ + public class Fixture { } } \ No newline at end of file