Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions src/BenchmarkDotNet/Extensions/ReflectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,34 @@ private static IEnumerable<string> GetNestedTypeNames(Type type, bool includeGen
/// </summary>
internal static string GetDisplayName(this Type type) => GetDisplayName(type.GetTypeInfo());

/// <summary>
/// Returns a display name per type, aligned with <paramref name="types"/>. When more than one
/// type shares the same simple <see cref="GetDisplayName(Type)"/> (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.
/// </summary>
internal static string[] GetDisambiguatedDisplayNames(this IReadOnlyList<Type> types)
{
var simpleNames = new string[types.Count];
for (int i = 0; i < types.Count; i++)
simpleNames[i] = types[i].GetDisplayName();

var ambiguousNames = new HashSet<string>(
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;
}

/// <summary>
/// returns simple, human friendly display name
/// </summary>
Expand Down
15 changes: 8 additions & 7 deletions src/BenchmarkDotNet/Running/UserInteraction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@ public void PrintNoBenchmarksError(ILogger logger)
public IReadOnlyList<Type> AskUser(IReadOnlyList<Type> allTypes, ILogger logger)
{
var selectedTypes = new List<Type>();
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;
Expand All @@ -42,7 +43,7 @@ public IReadOnlyList<Type> AskUser(IReadOnlyList<Type> allTypes, ILogger logger)
break;
}

selectedTypes.AddRange(GetMatching(allTypes, userInput.Split([' '], StringSplitOptions.RemoveEmptyEntries)));
selectedTypes.AddRange(GetMatching(allTypes, displayNames, userInput.Split([' '], StringSplitOptions.RemoveEmptyEntries)));
logger.WriteLine();
}

Expand Down Expand Up @@ -81,7 +82,7 @@ public void PrintWrongFilterInfo(IReadOnlyList<Type> allTypes, ILogger logger, s
logger.WriteLineInfo("To learn more about filtering use `--help`.");
}

private static IEnumerable<Type> GetMatching(IReadOnlyList<Type> allTypes, string[] userInput)
private static IEnumerable<Type> GetMatching(IReadOnlyList<Type> allTypes, string[] displayNames, string[] userInput)
{
if (userInput.IsEmpty())
yield break;
Expand All @@ -93,7 +94,7 @@ private static IEnumerable<Type> GetMatching(IReadOnlyList<Type> 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("*"))
Expand All @@ -105,13 +106,13 @@ private static IEnumerable<Type> GetMatching(IReadOnlyList<Type> allTypes, strin
static bool IsInteger(string str) => int.TryParse(str, out _);
}

private static void PrintAvailable(IReadOnlyList<Type> allTypes, ILogger logger)
private static void PrintAvailable(IReadOnlyList<Type> 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)
{
Expand Down
38 changes: 38 additions & 0 deletions tests/BenchmarkDotNet.Tests/ReflectionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down Expand Up @@ -175,4 +203,14 @@ public static implicit operator StackOnlyStruct<T>(WithImplicitCastToStackOnlySt
=> new StackOnlyStruct<T> { Span = instance.Array };
}
}
}

namespace BenchmarkDotNet.Tests.Foo
{
public class Fixture { }
}

namespace BenchmarkDotNet.Tests.Bar
{
public class Fixture { }
}