Skip to content

Add thorough tests for partially supported NRBF types #13490

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 3 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
3 changes: 2 additions & 1 deletion global.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
}
},
"sdk": {
"version": "10.0.100-preview.5.25265.106"
"version": "8.0.115",
"rollForward": "latestMajor"
},
"msbuild-sdks": {
"Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.25267.102",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
// Licensed to the .NET Foundation under one or more agreements.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Formats.Nrbf;
using System.Reflection.Metadata;
using System.Runtime.Serialization.Formatters.Binary;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;

namespace System.Private.Windows.Nrbf;

Expand All @@ -13,7 +16,9 @@ public class CoreNrbfSerializerTests
{
{ 123, true },
{ "test", true },
{ new object(), false }
{ new object(), false },
{ new Hashtable() { { "key", "value" } }, true },
{ new Hashtable(StringComparer.OrdinalIgnoreCase) { { "key", "value" } }, true }
};

[Theory]
Expand Down Expand Up @@ -94,4 +99,174 @@ public void IsSupportedType_ShouldReturnExpectedResult(Type type, bool expectedR
{
CoreNrbfSerializer.IsFullySupportedType(type).Should().Be(expectedResult);
}
}

[Fact]
public void HashtableType_IsNotFullySupportedType()
{
// Hashtable is not fully supported but can be round-tripped through TryWriteObject/TryGetObject
CoreNrbfSerializer.IsFullySupportedType(typeof(Hashtable)).Should().BeFalse();
}

[Theory]
[MemberData(nameof(HashtableTestData))]
public void Hashtable_TryWriteObject_TryGetObject_RoundTrip(Hashtable hashtable, bool expectSuccessfulDeserialization)
{
using MemoryStream stream = new();
bool result = CoreNrbfSerializer.TryWriteObject(stream, hashtable);
result.Should().BeTrue(); // All Hashtables should be serializable

stream.Position = 0;
SerializationRecord record = NrbfDecoder.Decode(stream);
bool deserializationResult = CoreNrbfSerializer.TryGetObject(record, out object? value);
deserializationResult.Should().Be(expectSuccessfulDeserialization);

// When deserialization is expected to succeed, verify the key-value pairs
if (expectSuccessfulDeserialization)
{
value.Should().BeOfType<Hashtable>();
Hashtable? deserializedTable = value as Hashtable;
deserializedTable!.Count.Should().Be(hashtable.Count);

foreach (DictionaryEntry entry in hashtable)
{
deserializedTable.Contains(entry.Key).Should().BeTrue();
deserializedTable[entry.Key].Should().Be(entry.Value);
}
}
}

[Theory]
[MemberData(nameof(HashtableTestData))]
public void BinaryFormatter_Hashtable_TryGetObject_RoundTrip(Hashtable hashtable, bool expectSuccessfulDeserialization)
{
using MemoryStream stream = new();
using (BinaryFormatterScope scope = new(enable: true))
{
#pragma warning disable SYSLIB0011 // Type or member is obsolete
new BinaryFormatter().Serialize(stream, hashtable);
#pragma warning restore SYSLIB0011
}

stream.Position = 0;
SerializationRecord record = NrbfDecoder.Decode(stream, leaveOpen: true);
bool deserializationResult = CoreNrbfSerializer.TryGetObject(record, out object? value);
deserializationResult.Should().Be(expectSuccessfulDeserialization);

// When deserialization is expected to succeed, verify the key-value pairs
if (expectSuccessfulDeserialization)
{
value.Should().BeOfType<Hashtable>();
Hashtable? deserializedTable = value as Hashtable;
deserializedTable!.Count.Should().Be(hashtable.Count);

foreach (DictionaryEntry entry in hashtable)
{
deserializedTable.Contains(entry.Key).Should().BeTrue();
deserializedTable[entry.Key].Should().Be(entry.Value);
}
}
}

public static TheoryData<Hashtable, bool> HashtableTestData => new()
{
// Standard hashtable should deserialize correctly
{ new Hashtable() { { "key", "value" } }, true },
{ new Hashtable() { { 1, 2 }, { "text", 42 } }, true },

// Hashtable with custom comparer won't be deserialized by TryGetPrimitiveHashtable
{ new Hashtable(StringComparer.OrdinalIgnoreCase) { { "key", "value" } }, false },
{ new Hashtable(StringComparer.CurrentCulture) { { "key", "value" } }, false },

// Hashtable with hash code provider won't be deserialized by TryGetPrimitiveHashtable
{ new Hashtable(new CustomHashCodeProvider()) { { "key", "value" } }, false },
{ new Hashtable(new CustomHashCodeProvider(), StringComparer.OrdinalIgnoreCase) { { "key", "value" } }, false }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This constructor overload does not exist. Use the one that takes an IComparer as a second argument (pass null for the second argument). Fix other instances in this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed the constructor overload in commit 7973629 by using the correct constructor that takes an IComparer as a second argument with null passed for the instances where only the hash code provider was needed.

};

[Fact]
public void HashtableWithCustomComparer_PreservesData_EvenWhenNotDeserialized()
{
// Create a hashtable with a custom comparer
Hashtable originalHashtable = new(StringComparer.OrdinalIgnoreCase)
{
{ "Key", "Value" }
};

// Serialize with BinaryFormatter
using MemoryStream stream = new();
using (BinaryFormatterScope scope = new(enable: true))
{
#pragma warning disable SYSLIB0011 // Type or member is obsolete
new BinaryFormatter().Serialize(stream, originalHashtable);
#pragma warning restore SYSLIB0011
}

// First verify that CoreNrbfSerializer can't deserialize it
stream.Position = 0;
SerializationRecord record = NrbfDecoder.Decode(stream, leaveOpen: true);
CoreNrbfSerializer.TryGetObject(record, out _).Should().BeFalse();

// Now verify that BinaryFormatter can deserialize it with all data intact
stream.Position = 0;
using (BinaryFormatterScope scope = new(enable: true))
{
#pragma warning disable SYSLIB0011 // Type or member is obsolete
Hashtable deserializedHashtable = (Hashtable)new BinaryFormatter().Deserialize(stream);
#pragma warning restore SYSLIB0011

// Verify key-value data
deserializedHashtable.Count.Should().Be(originalHashtable.Count);
deserializedHashtable["Key"].Should().Be("Value");

// Verify case-insensitivity was preserved (comparer was not lost)
deserializedHashtable["key"].Should().Be("Value");
}
}

[Fact]
public void HashtableWithHashCodeProvider_PreservesData_EvenWhenNotDeserialized()
{
// Create a hashtable with a custom hash code provider
Hashtable originalHashtable = new(new CustomHashCodeProvider())
{
{ "Key", "Value" }
};

// Serialize with BinaryFormatter
using MemoryStream stream = new();
using (BinaryFormatterScope scope = new(enable: true))
{
#pragma warning disable SYSLIB0011 // Type or member is obsolete
new BinaryFormatter().Serialize(stream, originalHashtable);
#pragma warning restore SYSLIB0011
}

// First verify that CoreNrbfSerializer can't deserialize it
stream.Position = 0;
SerializationRecord record = NrbfDecoder.Decode(stream, leaveOpen: true);
CoreNrbfSerializer.TryGetObject(record, out _).Should().BeFalse();

// Now verify that BinaryFormatter can deserialize it with all data intact
stream.Position = 0;
using (BinaryFormatterScope scope = new(enable: true))
{
#pragma warning disable SYSLIB0011 // Type or member is obsolete
Hashtable deserializedHashtable = (Hashtable)new BinaryFormatter().Deserialize(stream);
#pragma warning restore SYSLIB0011

// Verify key-value data
deserializedHashtable.Count.Should().Be(originalHashtable.Count);
deserializedHashtable["Key"].Should().Be("Value");

// Verify the hash code provider was preserved
deserializedHashtable.GetType().GetField("_keycomparer",
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)
.Should().NotBeNull();
}
}

[Serializable]
private class CustomHashCodeProvider : IHashCodeProvider
{
public int GetHashCode(object obj) => obj?.GetHashCode() ?? 0;
}
}
Loading