Skip to content

Commit 95e811e

Browse files
committed
Expand code coverage for UDT serialization
1 parent 1058566 commit 95e811e

File tree

5 files changed

+614
-0
lines changed

5 files changed

+614
-0
lines changed

src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft.Data.SqlClient.UnitTests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
<PrivateAssets>all</PrivateAssets>
2626
</PackageReference>
2727
<PackageReference Include="Microsoft.DotNet.XUnitExtensions" />
28+
<ProjectReference Include="$(SqlServerSource)Microsoft.SqlServer.Server.csproj" />
2829
</ItemGroup>
2930
<!-- .NET Framework references -->
3031
<ItemGroup Condition="$(TargetGroup) == 'netfx'">
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.IO;
7+
using Microsoft.Data.SqlClient.Server;
8+
using Microsoft.Data.SqlClient.UnitTests.UdtSerialization.SerializedTypes;
9+
using Microsoft.SqlServer.Server;
10+
using Xunit;
11+
12+
namespace Microsoft.Data.SqlClient.UnitTests.UdtSerialization;
13+
14+
public class InvalidSerializationTest
15+
{
16+
[Fact]
17+
public void RequiresSqlUserDefinedTypeAttribute()
18+
{
19+
using MemoryStream stream = new MemoryStream();
20+
21+
InvalidUdtException exception = Assert.Throws<InvalidUdtException>(
22+
() => SerializationHelperSql9.Serialize(stream, new ClassMissingSqlUserDefinedTypeAttribute()));
23+
24+
Assert.Equal($"'{typeof(ClassMissingSqlUserDefinedTypeAttribute).FullName}' is an invalid user defined type, reason: no UDT attribute.", exception.Message);
25+
}
26+
27+
[Fact]
28+
public void CannotSerializeUnknownFormattedType()
29+
{
30+
using MemoryStream stream = new MemoryStream();
31+
32+
ArgumentOutOfRangeException exception = Assert.Throws<ArgumentOutOfRangeException>("Format",
33+
() => SerializationHelperSql9.Serialize(stream, new UnknownFormattedClass()));
34+
35+
#if NET
36+
Assert.Equal("The Format enumeration value, 0, is not supported by the format method. (Parameter 'Format')", exception.Message);
37+
#else
38+
Assert.Equal("The Format enumeration value, Unknown, is not supported by the format method.\r\nParameter name: Format", exception.Message);
39+
#endif
40+
}
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using Microsoft.Data.SqlClient.Server;
6+
using Microsoft.Data.SqlClient.UnitTests.UdtSerialization.SerializedTypes;
7+
using System;
8+
using System.Collections.Generic;
9+
using System.Data.SqlTypes;
10+
using System.IO;
11+
using Xunit;
12+
13+
namespace Microsoft.Data.SqlClient.UnitTests.UdtSerialization;
14+
15+
/// <summary>
16+
/// Tests the serialization method defined by MS-SSCLRT. Ensures that combinations of primitives and custom types round-trip.
17+
/// </summary>
18+
/// <seealso href="https://learn.microsoft.com/en-us/openspecs/sql_server_protocols/ms-ssclrt/77460aa9-8c2f-4449-a65e-1d649ebd77fa"/>
19+
public class NativeSerializationTest
20+
{
21+
public static IEnumerable<object[]> SerializedNonNullPrimitiveTypeValues()
22+
{
23+
yield return [new BoolWrapperStruct { Field1 = true },
24+
new byte[] { 0x01 }];
25+
yield return [new ByteWrapperStruct { Field1 = 0x20 },
26+
new byte[] { 0x20 }];
27+
yield return [new SByteWrapperStruct { Field1 = -0x1 },
28+
new byte[] { 0x7F }];
29+
yield return [new UShortWrapperStruct { Field1 = 0x8000 },
30+
new byte[] { 0x80, 0x00 }];
31+
yield return [new ShortWrapperStruct { Field1 = 0x1234 },
32+
new byte[] { 0x92, 0x34 }];
33+
yield return [new UIntWrapperStruct { Field1 = 0xFFFFFFFF },
34+
new byte[] { 0xFF, 0xFF, 0xFF, 0xFF }];
35+
yield return [new IntWrapperStruct { Field1 = -0x12345678 },
36+
new byte[] { 0x6D, 0xCB, 0xA9, 0x88 }];
37+
yield return [new ULongWrapperStruct { Field1 = ulong.MaxValue },
38+
new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }];
39+
yield return [new LongWrapperStruct { Field1 = long.MinValue },
40+
new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }];
41+
yield return [new FloatWrapperStruct { Field1 = -0 },
42+
new byte[] { 0x80, 0x00, 0x00, 0x00 }];
43+
yield return [new DoubleWrapperStruct { Field1 = Math.PI },
44+
new byte[] { 0xC0, 0x09, 0x21, 0xFB, 0x54, 0x44, 0x2D, 0x18 }];
45+
yield return [new SqlByteWrapperStruct { Field1 = 0x20 },
46+
new byte[] { 0x01, 0x20 }];
47+
yield return [new SqlInt16WrapperStruct { Field1 = 0x1234 },
48+
new byte[] { 0x01, 0x92, 0x34 }];
49+
yield return [new SqlInt32WrapperStruct { Field1 = -0x12345678 },
50+
new byte[] { 0x01, 0x6D, 0xCB, 0xA9, 0x88 }];
51+
yield return [new SqlInt64WrapperStruct { Field1 = long.MinValue },
52+
new byte[] { 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }];
53+
yield return [new SqlBooleanWrapperStruct { Field1 = false },
54+
new byte[] { 0x01 }];
55+
yield return [new SqlSingleWrapperStruct { Field1 = -1 },
56+
new byte[] { 0x01, 0x40, 0x7F, 0xFF, 0xFF }];
57+
yield return [new SqlDoubleWrapperStruct { Field1 = -Math.PI },
58+
new byte[] { 0x01, 0x3F, 0xF6, 0xDE, 0x04, 0xAB, 0xBB, 0xD2, 0xE7 }];
59+
yield return [new SqlDateTimeWrapperStruct { Field1 = new DateTime(2000, 1, 1, 12, 34, 56, 500) },
60+
new byte[] { 0x01, 0x80, 0x00, 0x8E, 0xAC, 0x80, 0xCF, 0x59, 0xD6 }];
61+
yield return [new SqlMoneyWrapperStruct { Field1 = 1.10m },
62+
new byte[] { 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2A, 0xF8 }];
63+
}
64+
65+
public static IEnumerable<object[]> SerializedNestedNonNullPrimitiveTypeValues()
66+
{
67+
yield return [new NestedBoolWrapperStruct { Field1 = true, Field2 = new BoolWrapperStruct { Field1 = false } },
68+
new byte[] { 0x01,
69+
0x00 }];
70+
yield return [new NestedByteWrapperStruct { Field1 = 0x20, Field2 = new ByteWrapperStruct { Field1 = 0x30 } },
71+
new byte[] { 0x20,
72+
0x30 }];
73+
yield return [new NestedSByteWrapperStruct { Field1 = -0x01, Field2 = new SByteWrapperStruct { Field1 = 0x01 } },
74+
new byte[] { 0x7F,
75+
0x81 }];
76+
yield return [new NestedUShortWrapperStruct { Field1 = 0x8000, Field2 = new UShortWrapperStruct { Field1 = 0x8014 } },
77+
new byte[] { 0x80, 0x00,
78+
0x80, 0x14 }];
79+
yield return [new NestedShortWrapperStruct { Field1 = 0x1234, Field2 = new ShortWrapperStruct { Field1 = 0x4321 } },
80+
new byte[] { 0x92, 0x34,
81+
0xC3, 0x21 }];
82+
yield return [new NestedUIntWrapperStruct { Field1 = 0xFFFFFFFF, Field2 = new UIntWrapperStruct { Field1 = 0x00000000 } },
83+
new byte[] { 0xFF, 0xFF, 0xFF, 0xFF,
84+
0x00, 0x00, 0x00, 0x00 }];
85+
yield return [new NestedIntWrapperStruct { Field1 = -0x12345678, Field2 = new IntWrapperStruct { Field1 = 0x12345678 } },
86+
new byte[] { 0x6D, 0xCB, 0xA9, 0x88,
87+
0x92, 0x34, 0x56, 0x78 }];
88+
yield return [new NestedULongWrapperStruct { Field1 = ulong.MaxValue, Field2 = new ULongWrapperStruct { Field1 = long.MaxValue } },
89+
new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
90+
0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }];
91+
yield return [new NestedLongWrapperStruct { Field1 = long.MinValue, Field2 = new LongWrapperStruct { Field1 = long.MaxValue } },
92+
new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
93+
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }];
94+
yield return [new NestedFloatWrapperStruct { Field1 = -0, Field2 = new FloatWrapperStruct { Field1 = +0 } },
95+
new byte[] { 0x80, 0x00, 0x00, 0x00,
96+
0x80, 0x00, 0x00, 0x00 }];
97+
yield return [new NestedDoubleWrapperStruct { Field1 = Math.PI, Field2 = new DoubleWrapperStruct { Field1 = Math.PI } },
98+
new byte[] { 0xC0, 0x09, 0x21, 0xFB, 0x54, 0x44, 0x2D, 0x18,
99+
0xC0, 0x09, 0x21, 0xFB, 0x54, 0x44, 0x2D, 0x18 }];
100+
yield return [new NestedSqlByteWrapperStruct { Field1 = 0x20, Field2 = new SqlByteWrapperStruct { Field1 = 0x30 } },
101+
new byte[] { 0x01, 0x20,
102+
0x01, 0x30 }];
103+
yield return [new NestedSqlInt16WrapperStruct { Field1 = 0x1234, Field2 = new SqlInt16WrapperStruct { Field1 = 0x4321 } },
104+
new byte[] { 0x01, 0x92, 0x34,
105+
0x01, 0xC3, 0x21 }];
106+
yield return [new NestedSqlInt32WrapperStruct { Field1 = -0x12345678, Field2 = new SqlInt32WrapperStruct { Field1 = 0x12345678 } },
107+
new byte[] { 0x01, 0x6D, 0xCB, 0xA9, 0x88,
108+
0x01, 0x92, 0x34, 0x56, 0x78 }];
109+
yield return [new NestedSqlInt64WrapperStruct { Field1 = long.MinValue, Field2 = new SqlInt64WrapperStruct { Field1 = long.MaxValue } },
110+
new byte[] { 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
111+
0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }];
112+
yield return [new NestedSqlBooleanWrapperStruct { Field1 = false, Field2 = new SqlBooleanWrapperStruct { Field1 = true } },
113+
new byte[] { 0x01,
114+
0x02 }];
115+
yield return [new NestedSqlSingleWrapperStruct { Field1 = -0, Field2 = new SqlSingleWrapperStruct { Field1 = +0 } },
116+
new byte[] { 0x01, 0x80, 0x00, 0x00, 0x00,
117+
0x01, 0x80, 0x00, 0x00, 0x00 }];
118+
yield return [new NestedSqlDoubleWrapperStruct { Field1 = Math.PI, Field2 = new SqlDoubleWrapperStruct { Field1 = Math.PI } },
119+
new byte[] { 0x01, 0xC0, 0x09, 0x21, 0xFB, 0x54, 0x44, 0x2D, 0x18,
120+
0x01, 0xC0, 0x09, 0x21, 0xFB, 0x54, 0x44, 0x2D, 0x18 }];
121+
yield return [new NestedSqlDateTimeWrapperStruct { Field1 = new DateTime(2000, 1, 1, 12, 34, 56, 500), Field2 = new SqlDateTimeWrapperStruct { Field1 = new DateTime(2000, 1, 1) } },
122+
new byte[] { 0x01, 0x80, 0x00, 0x8E, 0xAC, 0x80, 0xCF, 0x59, 0xD6,
123+
0x01, 0x80, 0x00, 0x8E, 0xAC, 0x80, 0x00, 0x00, 0x00 }];
124+
yield return [new NestedSqlMoneyWrapperStruct { Field1 = 1.10m, Field2 = new SqlMoneyWrapperStruct { Field1 = -2.55m } },
125+
new byte[] { 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2A, 0xF8,
126+
0x01, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x9C, 0x64 }];
127+
}
128+
129+
public static IEnumerable<object[]> SerializedNullPrimitiveTypeValues()
130+
{
131+
yield return [new SqlByteWrapperStruct { Field1 = SqlByte.Null },
132+
new byte[] { 0x00, 0x00 }];
133+
yield return [new SqlInt16WrapperStruct { Field1 = SqlInt16.Null },
134+
new byte[] { 0x00, 0x80, 0x00 }];
135+
yield return [new SqlInt32WrapperStruct { Field1 = SqlInt32.Null },
136+
new byte[] { 0x00, 0x80, 0x00, 0x00, 0x00 }];
137+
yield return [new SqlInt64WrapperStruct { Field1 = SqlInt64.Null },
138+
new byte[] { 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }];
139+
yield return [new SqlBooleanWrapperStruct { Field1 = SqlBoolean.Null },
140+
new byte[] { 0x00 }];
141+
yield return [new SqlSingleWrapperStruct { Field1 = SqlSingle.Null },
142+
new byte[] { 0x00, 0x80, 0x00, 0x00, 0x00 }];
143+
yield return [new SqlDoubleWrapperStruct { Field1 = SqlDouble.Null },
144+
new byte[] { 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }];
145+
yield return [new SqlDateTimeWrapperStruct { Field1 = SqlDateTime.Null },
146+
new byte[] { 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00 }];
147+
yield return [new SqlMoneyWrapperStruct { Field1 = SqlMoney.Null },
148+
new byte[] { 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }];
149+
}
150+
151+
/// <summary>
152+
/// Attempts to serialize various structs containing non-null primitive types.
153+
/// Verifies that the method does not throw, that serialized byte output is correct, and that the value round-trips.
154+
/// </summary>
155+
/// <param name="primitive">Primitive to serialize and to compare against.</param>
156+
/// <param name="expectedValue">Expected byte output.</param>
157+
[Theory]
158+
[MemberData(nameof(SerializedNonNullPrimitiveTypeValues))]
159+
public void SerializePrimitiveType(object primitive, byte[] expectedValue)
160+
=> RoundtripType(primitive, expectedValue);
161+
162+
/// <summary>
163+
/// Attempts to serialize a nested struct hierarchy containing non-null primitive types.
164+
/// Verifies that the method does not throw, that serialized byte output is correct, and that the value round-trips.
165+
/// </summary>
166+
/// <param name="primitive">Primitive to serialize and to compare against.</param>
167+
/// <param name="expectedValue">Expected byte output.</param>
168+
[Theory]
169+
[MemberData(nameof(SerializedNestedNonNullPrimitiveTypeValues))]
170+
public void SerializeNestedPrimitiveType(object primitive, byte[] expectedValue)
171+
=> RoundtripType(primitive, expectedValue);
172+
173+
/// <summary>
174+
/// Attempts to serialize various structs containing null-valued primitive types.
175+
/// Verifies that the method does not throw, that serialized byte output is correct, and that the value round-trips.
176+
/// </summary>
177+
/// <param name="primitive">Primitive to serialize and to compare against.</param>
178+
/// <param name="expectedValue">Expected byte output.</param>
179+
[Theory]
180+
[MemberData(nameof(SerializedNullPrimitiveTypeValues))]
181+
public void SerializeNullPrimitiveType(object primitive, byte[] expectedValue)
182+
=> RoundtripType(primitive, expectedValue);
183+
184+
/// <summary>
185+
/// Attempts to serializes an instance of a class.
186+
/// </summary>
187+
/// <seealso cref="CannotSerializeNestedClass"/>
188+
[Fact]
189+
public void CanSerializeTopLevelClass()
190+
{
191+
NestedBoolWrapperClass validWrapper = new NestedBoolWrapperClass()
192+
{
193+
Field1 = true,
194+
Field2 = new BoolWrapperStruct() { Field1 = true }
195+
};
196+
using MemoryStream stream = new MemoryStream();
197+
198+
SerializationHelperSql9.Serialize(stream, validWrapper);
199+
}
200+
201+
/// <summary>
202+
/// Attempts to serializes a field referring to an instance of a class.
203+
/// Verifies that this succeeds, and that Native format serialization only operates with primitive types
204+
/// and value types containing these.
205+
/// </summary>
206+
/// <seealso cref="CannotSerializeNestedClass"/>
207+
[Fact]
208+
public void CannotSerializeNestedClass()
209+
{
210+
InvalidNestedBoolWrapperClass invalidWrapper = new InvalidNestedBoolWrapperClass()
211+
{
212+
Field1 = true,
213+
Field2 = new BoolWrapperClass() { Field1 = true }
214+
};
215+
using MemoryStream stream = new MemoryStream();
216+
217+
Exception ex = Assert.Throws<Exception>(() => SerializationHelperSql9.Serialize(stream, invalidWrapper));
218+
string expectedException = StringsHelper.GetString(Strings.SQL_CannotCreateNormalizer, invalidWrapper.Field2.GetType().FullName);
219+
220+
Assert.Equal(expectedException, ex.Message);
221+
}
222+
223+
/// <summary>
224+
/// Attempts to serialize a struct containing non-primitive value types.
225+
/// Verifies that this fails.
226+
/// </summary>
227+
[Fact]
228+
public void CannotSerializeNonPrimitiveType()
229+
{
230+
InvalidIntPtrAndByteWrapperStruct invalidWrapper = new InvalidIntPtrAndByteWrapperStruct()
231+
{
232+
Field1 = 1,
233+
Field2 = IntPtr.Zero
234+
};
235+
using MemoryStream stream = new MemoryStream();
236+
237+
Exception ex = Assert.Throws<Exception>(() => SerializationHelperSql9.Serialize(stream, invalidWrapper));
238+
string expectedException = StringsHelper.GetString(Strings.SQL_CannotCreateNormalizer, invalidWrapper.Field2.GetType().FullName);
239+
240+
Assert.Equal(expectedException, ex.Message);
241+
}
242+
243+
/// <summary>
244+
/// Serializes an object, verifies the value and the size of the object, then roundtrips it and verifies the result is identical.
245+
/// </summary>
246+
/// <param name="inputValue">Object to serialize.</param>
247+
/// <param name="expectedValue">Expected serialization output.</param>
248+
private static void RoundtripType(object inputValue, byte[] expectedValue)
249+
{
250+
using MemoryStream stream = new MemoryStream();
251+
object readPrimitive;
252+
int typeSize = SerializationHelperSql9.SizeInBytes(inputValue.GetType());
253+
int objectSize = SerializationHelperSql9.SizeInBytes(inputValue);
254+
int maxTypeSize = SerializationHelperSql9.GetUdtMaxLength(inputValue.GetType());
255+
256+
SerializationHelperSql9.Serialize(stream, inputValue);
257+
stream.Seek(0, SeekOrigin.Begin);
258+
readPrimitive = SerializationHelperSql9.Deserialize(stream, inputValue.GetType());
259+
260+
// For native formatting, the type size, the object size and the maximum object size will always be identical
261+
Assert.Equal(typeSize, objectSize);
262+
Assert.Equal(expectedValue.Length, typeSize);
263+
Assert.Equal(typeSize, maxTypeSize);
264+
265+
Assert.Equal(expectedValue, stream.ToArray());
266+
Assert.Equal(inputValue, readPrimitive);
267+
}
268+
}

0 commit comments

Comments
 (0)