Skip to content

Commit

Permalink
Unify debug views of immutable dictionaries (#100745)
Browse files Browse the repository at this point in the history
* Unify debug views of immutable dictionaries

Fixes #94289

- Updates the way the debugger displays the remaining dictionaries (Frozen, Immutable, ImmutableSorted, Concurrent) to present their keys and values in separate columns.
- Fixes debugger views of Builder classes of immutable collections. Previous custom implementations incorrectly treated the Builder classes as immutable.

* Fixed tests of debugger attributes with immutable and concurrent generic dictionaries

* Removed tests superseded by DebugView.Tests.
* Fixed DebugView.Tests of cuncurrent and immutable generic dictionaries which failed on .Net Framework

* Fix ns2.0 build.

---------

Co-authored-by: Eirik Tsarpalis <eirik.tsarpalis@gmail.com>
  • Loading branch information
arturek and eiriktsarpalis authored Jul 5, 2024
1 parent 41d854f commit 8bcbe18
Show file tree
Hide file tree
Showing 14 changed files with 99 additions and 195 deletions.
62 changes: 57 additions & 5 deletions src/libraries/Common/tests/System/Collections/DebugView.Tests.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Concurrent;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
Expand Down Expand Up @@ -47,8 +50,8 @@ private static IEnumerable<object[]> TestDebuggerAttributes_GenericDictionaries(
new ("[\"Two\"]", "2"),
}
};
CustomKeyedCollection<string, int> collection = new ();
collection.GetKeyForItemHandler = value => (2 * value).ToString();
CustomKeyedCollection<string, int> collection = new();
collection.GetKeyForItemHandler = value => (2 * value).ToString();
collection.InsertItem(0, 1);
collection.InsertItem(1, 3);
yield return new object[] { collection,
Expand All @@ -58,6 +61,53 @@ private static IEnumerable<object[]> TestDebuggerAttributes_GenericDictionaries(
new ("[\"6\"]", "3"),
}
};

yield return new object[] { new ConcurrentDictionary<int, string>(new KeyValuePair<int, string>[] { new(1, "One"), new(2, "Two") }),
new KeyValuePair<string, string>[]
{
new ("[1]", "\"One\""),
new ("[2]", "\"Two\""),
}
};
}

private static IEnumerable<object[]> TestDebuggerAttributes_AdditionalGenericDictionaries()
{
yield return new object[] { new Dictionary<int, string> { { 1, "One" }, { 2, "Two" } }.ToFrozenDictionary(),
new KeyValuePair<string, string>[]
{
new ("[1]", "\"One\""),
new ("[2]", "\"Two\""),
}
};
yield return new object[] { new Dictionary<int, string> { { 1, "One" }, { 2, "Two" } }.ToImmutableDictionary(),
new KeyValuePair<string, string>[]
{
new ("[1]", "\"One\""),
new ("[2]", "\"Two\""),
}
};
yield return new object[] { new Dictionary<int, string> { { 1, "One" }, { 2, "Two" } }.ToImmutableDictionary().ToBuilder(),
new KeyValuePair<string, string>[]
{
new ("[1]", "\"One\""),
new ("[2]", "\"Two\""),
}
};
yield return new object[] { new Dictionary<int, string> { { 1, "One" }, { 2, "Two" } }.ToImmutableSortedDictionary(),
new KeyValuePair<string, string>[]
{
new ("[1]", "\"One\""),
new ("[2]", "\"Two\""),
}
};
yield return new object[] { new Dictionary<int, string> { { 1, "One" }, { 2, "Two" } }.ToImmutableSortedDictionary().ToBuilder(),
new KeyValuePair<string, string>[]
{
new ("[1]", "\"One\""),
new ("[2]", "\"Two\""),
}
};
}

private static IEnumerable<object[]> TestDebuggerAttributes_NonGenericDictionaries()
Expand Down Expand Up @@ -162,12 +212,14 @@ private static IEnumerable<object[]> TestDebuggerAttributes_ListInputs()

public static IEnumerable<object[]> TestDebuggerAttributes_InputsPresentedAsDictionary()
{
var testCases = TestDebuggerAttributes_NonGenericDictionaries()
.Concat(TestDebuggerAttributes_AdditionalGenericDictionaries());
#if !NETFRAMEWORK
return TestDebuggerAttributes_NonGenericDictionaries()
return testCases
.Concat(TestDebuggerAttributes_GenericDictionaries());
#else
// In .Net Framework only non-generic dictionaries are displayed in a dictionary format by the debugger.
return TestDebuggerAttributes_NonGenericDictionaries();
// In .Net Framework, the generic dictionaries that are part of the framework are displayed in a list format by the debugger.
return testCases;
#endif
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Reflection;
using System.Text;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
Link="System\Collections\HashHelpers.cs" />
<Compile Include="$(CoreLibSharedDir)System\Collections\Concurrent\IProducerConsumerCollectionDebugView.cs"
Link="System\Collections\Concurrent\IProducerConsumerCollectionDebugView.cs" />
<Compile Include="$(CoreLibSharedDir)System\Collections\Generic\DebugViewDictionaryItem.cs"
Link="Common\System\Collections\Generic\DebugViewDictionaryItem.cs" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2733,12 +2733,17 @@ public IDictionaryDebugView(IDictionary<TKey, TValue> dictionary)
}

[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
public KeyValuePair<TKey, TValue>[] Items
public DebugViewDictionaryItem<TKey, TValue>[] Items
{
get
{
var items = new KeyValuePair<TKey, TValue>[_dictionary.Count];
_dictionary.CopyTo(items, 0);
var keyValuePairs = new KeyValuePair<TKey, TValue>[_dictionary.Count];
_dictionary.CopyTo(keyValuePairs, 0);
var items = new DebugViewDictionaryItem<TKey, TValue>[keyValuePairs.Length];
for (int i = 0; i < items.Length; i++)
{
items[i] = new DebugViewDictionaryItem<TKey, TValue>(keyValuePairs[i]);
}
return items;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -629,27 +629,6 @@ public static void TestConstructor_ConcurrencyLevel(int concurrencyLevel)
Assert.Equal(2, dictionary.Count);
}

[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsDebuggerTypeProxyAttributeSupported))]
public static void TestDebuggerAttributes()
{
DebuggerAttributes.ValidateDebuggerDisplayReferences(new ConcurrentDictionary<string, int>());
ConcurrentDictionary<string, int> dict = new ConcurrentDictionary<string, int>();
dict.TryAdd("One", 1);
dict.TryAdd("Two", 2);
DebuggerAttributeInfo info = DebuggerAttributes.ValidateDebuggerTypeProxyProperties(dict);
PropertyInfo itemProperty = info.Properties.Single(pr => pr.GetCustomAttribute<DebuggerBrowsableAttribute>().State == DebuggerBrowsableState.RootHidden);
KeyValuePair<string, int>[] items = itemProperty.GetValue(info.Instance) as KeyValuePair<string, int>[];
Assert.Equal(dict, items);
}

[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsDebuggerTypeProxyAttributeSupported))]
public static void TestDebuggerAttributes_Null()
{
Type proxyType = DebuggerAttributes.GetProxyType(new ConcurrentDictionary<string, int>());
TargetInvocationException tie = Assert.Throws<TargetInvocationException>(() => Activator.CreateInstance(proxyType, (object)null));
Assert.IsType<ArgumentNullException>(tie.InnerException);
}

[Fact]
public static void TestNullComparer()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ The System.Collections.Immutable library is built-in as part of the shared frame

<ItemGroup>
<Compile Include="Properties\InternalsVisibleTo.cs" />

<Compile Include="System\Polyfills.cs" />
<Compile Include="System\Collections\ThrowHelper.cs" />
<Compile Include="$(CoreLibSharedDir)System\Collections\HashHelpers.cs" Link="System\Collections\HashHelpers.cs" />

<Compile Include="$(CoreLibSharedDir)System\Collections\Generic\DebugViewDictionaryItem.cs" Link="Common\System\Collections\Generic\DebugViewDictionaryItem.cs" />
<Compile Include="$(CoreLibSharedDir)System\Collections\Generic\IDictionaryDebugView.cs" Link="Common\System\Collections\Generic\IDictionaryDebugView.cs" />
<Compile Include="System\Collections\Frozen\Constants.cs" />
<Compile Include="System\Collections\Frozen\DefaultFrozenDictionary.cs" />
<Compile Include="System\Collections\Frozen\DefaultFrozenSet.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public sealed partial class ImmutableDictionary<TKey, TValue>
/// </para>
/// </remarks>
[DebuggerDisplay("Count = {Count}")]
[DebuggerTypeProxy(typeof(ImmutableDictionaryBuilderDebuggerProxy<,>))]
[DebuggerTypeProxy(typeof(IDictionaryDebugView<,>))]
public sealed class Builder : IDictionary<TKey, TValue>, IReadOnlyDictionary<TKey, TValue>, IDictionary
{
/// <summary>
Expand Down Expand Up @@ -709,36 +709,4 @@ private bool Apply(MutationResult result)
}
}
}

/// <summary>
/// A simple view of the immutable collection that the debugger can show to the developer.
/// </summary>
internal sealed class ImmutableDictionaryBuilderDebuggerProxy<TKey, TValue> where TKey : notnull
{
/// <summary>
/// The collection to be enumerated.
/// </summary>
private readonly ImmutableDictionary<TKey, TValue>.Builder _map;

/// <summary>
/// The simple view of the collection.
/// </summary>
private KeyValuePair<TKey, TValue>[]? _contents;

/// <summary>
/// Initializes a new instance of the <see cref="ImmutableDictionaryBuilderDebuggerProxy{TKey, TValue}"/> class.
/// </summary>
/// <param name="map">The collection to display in the debugger</param>
public ImmutableDictionaryBuilderDebuggerProxy(ImmutableDictionary<TKey, TValue>.Builder map)
{
Requires.NotNull(map, nameof(map));
_map = map;
}

/// <summary>
/// Gets a simple debugger-viewable collection.
/// </summary>
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
public KeyValuePair<TKey, TValue>[] Contents => _contents ??= _map.ToArray(_map.Count);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,38 @@ namespace System.Collections.Immutable
/// </summary>
/// <typeparam name="TKey">The type of the dictionary's keys.</typeparam>
/// <typeparam name="TValue">The type of the dictionary's values.</typeparam>
internal sealed class ImmutableDictionaryDebuggerProxy<TKey, TValue> : ImmutableEnumerableDebuggerProxy<KeyValuePair<TKey, TValue>> where TKey : notnull
/// <remarks>
/// This class should only be used with immutable dictionaries, since it
/// caches the dictionary into an array for display in the debugger.
/// </remarks>
internal sealed class ImmutableDictionaryDebuggerProxy<TKey, TValue> where TKey : notnull
{
/// <summary>
/// The dictionary to show to the debugger.
/// </summary>
private readonly IReadOnlyDictionary<TKey, TValue> _dictionary;

/// <summary>
/// The contents of the dictionary, cached into an array.
/// </summary>
private DebugViewDictionaryItem<TKey, TValue>[]? _cachedContents;

/// <summary>
/// Initializes a new instance of the <see cref="ImmutableDictionaryDebuggerProxy{TKey, TValue}"/> class.
/// </summary>
/// <param name="dictionary">The enumerable to show in the debugger.</param>
/// <param name="dictionary">The dictionary to show in the debugger.</param>
public ImmutableDictionaryDebuggerProxy(IReadOnlyDictionary<TKey, TValue> dictionary)
: base(enumerable: dictionary)
{
Requires.NotNull(dictionary, nameof(dictionary));
_dictionary = dictionary;
}

/// <summary>
/// Gets the contents of the dictionary for display in the debugger.
/// </summary>
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
public DebugViewDictionaryItem<TKey, TValue>[] Contents => _cachedContents
??= _dictionary.Select(kv => new DebugViewDictionaryItem<TKey, TValue>(kv)).ToArray(_dictionary.Count);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public sealed partial class ImmutableSortedDictionary<TKey, TValue> where TKey :
/// </para>
/// </remarks>
[DebuggerDisplay("Count = {Count}")]
[DebuggerTypeProxy(typeof(ImmutableSortedDictionaryBuilderDebuggerProxy<,>))]
[DebuggerTypeProxy(typeof(IDictionaryDebugView<,>))]
public sealed class Builder : IDictionary<TKey, TValue>, IReadOnlyDictionary<TKey, TValue>, IDictionary
{
/// <summary>
Expand Down Expand Up @@ -645,35 +645,4 @@ public ImmutableSortedDictionary<TKey, TValue> ToImmutable()
#endregion
}
}
/// <summary>
/// A simple view of the immutable collection that the debugger can show to the developer.
/// </summary>
internal sealed class ImmutableSortedDictionaryBuilderDebuggerProxy<TKey, TValue> where TKey : notnull
{
/// <summary>
/// The collection to be enumerated.
/// </summary>
private readonly ImmutableSortedDictionary<TKey, TValue>.Builder _map;

/// <summary>
/// The simple view of the collection.
/// </summary>
private KeyValuePair<TKey, TValue>[]? _contents;

/// <summary>
/// Initializes a new instance of the <see cref="ImmutableSortedDictionaryBuilderDebuggerProxy{TKey, TValue}"/> class.
/// </summary>
/// <param name="map">The collection to display in the debugger</param>
public ImmutableSortedDictionaryBuilderDebuggerProxy(ImmutableSortedDictionary<TKey, TValue>.Builder map)
{
Requires.NotNull(map, nameof(map));
_map = map;
}

/// <summary>
/// Gets a simple debugger-viewable collection.
/// </summary>
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
public KeyValuePair<TKey, TValue>[] Contents => _contents ??= _map.ToArray(_map.Count);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -255,27 +255,6 @@ public void GetValueOrDefaultOfConcreteType()
Assert.Equal(5, populated.GetValueOrDefault("a", 1));
}

[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsDebuggerTypeProxyAttributeSupported))]
public void DebuggerAttributesValid()
{
DebuggerAttributes.ValidateDebuggerDisplayReferences(ImmutableDictionary.CreateBuilder<string, int>());
ImmutableDictionary<int, string>.Builder builder = ImmutableDictionary.CreateBuilder<int, string>();
builder.Add(1, "One");
builder.Add(2, "Two");
DebuggerAttributeInfo info = DebuggerAttributes.ValidateDebuggerTypeProxyProperties(builder);
PropertyInfo itemProperty = info.Properties.Single(pr => pr.GetCustomAttribute<DebuggerBrowsableAttribute>().State == DebuggerBrowsableState.RootHidden);
KeyValuePair<int, string>[] items = itemProperty.GetValue(info.Instance) as KeyValuePair<int, string>[];
Assert.Equal(builder, items);
}

[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsDebuggerTypeProxyAttributeSupported))]
public static void TestDebuggerAttributes_Null()
{
Type proxyType = DebuggerAttributes.GetProxyType(ImmutableHashSet.Create<string>());
TargetInvocationException tie = Assert.Throws<TargetInvocationException>(() => Activator.CreateInstance(proxyType, (object)null));
Assert.IsType<ArgumentNullException>(tie.InnerException);
}

[Fact]
public void ToImmutableDictionary()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -349,28 +349,6 @@ public void EnumeratorRecyclingMisuse()
enumerator.Dispose();
}

[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsDebuggerTypeProxyAttributeSupported))]
public void DebuggerAttributesValid()
{
DebuggerAttributes.ValidateDebuggerDisplayReferences(ImmutableDictionary.Create<int, int>());
ImmutableDictionary<string, int> dict = ImmutableDictionary.Create<string, int>().Add("One", 1).Add("Two", 2);
DebuggerAttributeInfo info = DebuggerAttributes.ValidateDebuggerTypeProxyProperties(dict);

object rootNode = DebuggerAttributes.GetFieldValue(ImmutableDictionary.Create<string, string>(), "_root");
DebuggerAttributes.ValidateDebuggerDisplayReferences(rootNode);
PropertyInfo itemProperty = info.Properties.Single(pr => pr.GetCustomAttribute<DebuggerBrowsableAttribute>().State == DebuggerBrowsableState.RootHidden);
KeyValuePair<string, int>[] items = itemProperty.GetValue(info.Instance) as KeyValuePair<string, int>[];
Assert.Equal(dict, items);
}

[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsDebuggerTypeProxyAttributeSupported))]
public static void TestDebuggerAttributes_Null()
{
Type proxyType = DebuggerAttributes.GetProxyType(ImmutableHashSet.Create<string>());
TargetInvocationException tie = Assert.Throws<TargetInvocationException>(() => Activator.CreateInstance(proxyType, (object)null));
Assert.IsType<ArgumentNullException>(tie.InnerException);
}

[Fact]
public void Clear_NoComparer_ReturnsEmptyWithoutComparer()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,27 +255,6 @@ public void GetValueOrDefaultOfConcreteType()
Assert.Equal(5, populated.GetValueOrDefault("a", 1));
}

[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsDebuggerTypeProxyAttributeSupported))]
public void DebuggerAttributesValid()
{
DebuggerAttributes.ValidateDebuggerDisplayReferences(ImmutableSortedDictionary.CreateBuilder<string, int>());
ImmutableSortedDictionary<int, string>.Builder builder = ImmutableSortedDictionary.CreateBuilder<int, string>();
builder.Add(1, "One");
builder.Add(2, "Two");
DebuggerAttributeInfo info = DebuggerAttributes.ValidateDebuggerTypeProxyProperties(builder);
PropertyInfo itemProperty = info.Properties.Single(pr => pr.GetCustomAttribute<DebuggerBrowsableAttribute>().State == DebuggerBrowsableState.RootHidden);
KeyValuePair<int, string>[] items = itemProperty.GetValue(info.Instance) as KeyValuePair<int, string>[];
Assert.Equal(builder, items);
}

[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsDebuggerTypeProxyAttributeSupported))]
public static void TestDebuggerAttributes_Null()
{
Type proxyType = DebuggerAttributes.GetProxyType(ImmutableSortedDictionary.CreateBuilder<int, string>());
TargetInvocationException tie = Assert.Throws<TargetInvocationException>(() => Activator.CreateInstance(proxyType, (object)null));
Assert.IsType<ArgumentNullException>(tie.InnerException);
}

[Fact]
public void ValueRef()
{
Expand Down
Loading

0 comments on commit 8bcbe18

Please sign in to comment.