Skip to content

Commit 736554e

Browse files
authored
Fix for DebugAssertException detection during testing + VIntUtils length assertions (#1400)
1 parent 2cde8b5 commit 736554e

6 files changed

Lines changed: 196 additions & 12 deletions

File tree

src/Lucene.Net.TestFramework/Support/Util/LuceneTestFrameworkInitializer.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
using NUnit.Framework;
44
using System;
55
using System.Diagnostics.CodeAnalysis;
6-
using System.IO;
76

87
namespace Lucene.Net.Util
98
{
@@ -358,11 +357,13 @@ internal void InitializeStaticState()
358357
// Identify the Debug.Assert() exception so it can be excluded from being swallowed by catch blocks.
359358
// These types are internal, so we can identify them using Reflection.
360359
Lucene.ExceptionExtensions.DebugAssertExceptionType =
360+
// Microsoft.NET.Test.Sdk 16.6.0+ (used by Visual Studio Test Explorer)
361+
Type.GetType("Microsoft.VisualStudio.TestPlatform.TestHost.DebugAssertException, testhost")
361362
// .NET 5/.NET Core 3.x
362-
Type.GetType("System.Diagnostics.DebugProvider+DebugAssertException, System.Private.CoreLib")
363+
?? Type.GetType("System.Diagnostics.DebugProvider+DebugAssertException, System.Private.CoreLib")
363364
// .NET Core 2.x
364365
?? Type.GetType("System.Diagnostics.Debug+DebugAssertException, System.Private.CoreLib");
365-
// .NET Framework doesn't throw in this case
366+
// .NET Framework doesn't throw in this case
366367
}
367368

368369
/// <summary>

src/Lucene.Net.Tests.AllProjects/Support/AssemblyScanningTestCase.cs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,23 @@ public abstract class AssemblyScanningTestCase : LuceneTestCase
5959
};
6060

6161

62-
public static readonly Assembly[] DotNetAssemblies = new Assembly[]
62+
public static readonly Assembly[] DotNetAssemblies = LoadDotNetAssemblies();
63+
64+
private static Assembly[] LoadDotNetAssemblies()
6365
{
64-
typeof(Exception).Assembly
65-
};
66+
var list = new List<Assembly>()
67+
{
68+
typeof(Exception).Assembly
69+
};
70+
71+
Assembly testHostAssembly = Type.GetType("Microsoft.VisualStudio.TestPlatform.TestHost.DebugAssertException, testhost")?.Assembly;
72+
if (testHostAssembly is not null)
73+
{
74+
list.Add(testHostAssembly);
75+
}
76+
77+
return list.ToArray();
78+
}
6679

6780
public static readonly Assembly[] NUnitAssemblies = new Assembly[]
6881
{

src/Lucene.Net.Tests.AllProjects/Support/ExceptionHandling/ExceptionScanningTestCase.cs

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,21 @@ namespace Lucene.Net.Support.ExceptionHandling
3939
public abstract class ExceptionScanningTestCase : AssemblyScanningTestCase
4040
{
4141
// Internal types references
42-
private static readonly Type DebugAssertExceptionType =
42+
private static readonly Type ClrDebugAssertExceptionType =
4343
// .NET 5/.NET Core 3.x
4444
Type.GetType("System.Diagnostics.DebugProvider+DebugAssertException, System.Private.CoreLib")
4545
// .NET Core 2.x
4646
?? Type.GetType("System.Diagnostics.Debug+DebugAssertException, System.Private.CoreLib");
47+
48+
private static readonly Type TestHostDebugAssertExceptionType =
49+
// Microsoft.NET.Test.Sdk 16.6.0+ (used by Visual Studio Test Explorer)
50+
Type.GetType("Microsoft.VisualStudio.TestPlatform.TestHost.DebugAssertException, testhost");
51+
52+
private static readonly Type DebugAssertExceptionType =
53+
// Microsoft.NET.Test.Sdk 16.6.0+ (used by Visual Studio Test Explorer)
54+
TestHostDebugAssertExceptionType
55+
// .NET Core
56+
?? ClrDebugAssertExceptionType;
4757
// .NET Framework doesn't throw in this case
4858

4959
private static readonly Type MetadataExceptionType =
@@ -465,15 +475,28 @@ private static IDictionary<Type, Func<Type, string, object>> LoadNonStandardExce
465475
};
466476

467477
// Special case - this doesn't exist on .NET Framework, so we only add it if not null
468-
if (DebugAssertExceptionType is not null)
478+
if (ClrDebugAssertExceptionType is not null)
469479
{
470-
result[DebugAssertExceptionType] = (exceptionType, message) =>
480+
result[ClrDebugAssertExceptionType] = (exceptionType, message) =>
471481
{
472482
//private sealed class DebugAssertException : Exception
473483
//{
474484
// internal DebugAssertException(string? message, string? detailMessage, string? stackTrace)
475485
BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Instance;
476-
return Activator.CreateInstance(DebugAssertExceptionType, flags, null, new object[] { message, null, null }, CultureInfo.InvariantCulture);
486+
return Activator.CreateInstance(ClrDebugAssertExceptionType, flags, null, new object[] { message, null, null }, CultureInfo.InvariantCulture);
487+
};
488+
}
489+
490+
// Special case - Microsoft.NET.Test.Sdk 16.6.0+ contains its own Debug.Assert type that is thrown when debugging tests
491+
if (TestHostDebugAssertExceptionType is not null)
492+
{
493+
result[TestHostDebugAssertExceptionType] = (exceptionType, message) =>
494+
{
495+
//internal sealed class DebugAssertException : Exception
496+
//{
497+
//public DebugAssertException(string message, string stackTrace) : base(message)
498+
BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Instance;
499+
return Activator.CreateInstance(TestHostDebugAssertExceptionType, flags, null, new object[] { message, string.Empty }, CultureInfo.InvariantCulture);
477500
};
478501
}
479502

src/Lucene.Net.Tests.AllProjects/Support/ExceptionHandling/TestExceptionExtensions.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,29 @@ public void TestIsIllegalArgumentException_TestEnvironment(Type exceptionType, b
381381
}
382382
}
383383

384+
#if DEBUG
385+
/// <summary>
386+
/// Tests to ensure that the DebugAssertException type string is correct for the current test environment.
387+
/// If this test fails, it means that the DebugAssertException has changed location and the
388+
/// <see cref="LuceneTestFrameworkInitializer.InitializeStaticState()"/> method needs to be updated with
389+
/// the new type string.
390+
/// <para/>
391+
/// Microsoft controls this location, it is internal, and changes from time to time. Currently, it is in
392+
/// Microsoft.NET.Test.Sdk as of version 16.6.0.
393+
/// <para/>
394+
/// Note that this exception only applies when the test SDK is attached. If the test SDK is not attached,
395+
/// then this exception falls back to a different exception type in the current .NET SDK. This can happen
396+
/// in DEBUG mode when running as a console application, for example.
397+
/// </summary>
398+
[Test]
399+
public void TestDebugAssertExceptionTypeString()
400+
{
401+
Assert.IsNotNull(
402+
Type.GetType("Microsoft.VisualStudio.TestPlatform.TestHost.DebugAssertException, testhost"),
403+
"DebugAssertException type not found for the current test enviornment. This means that the DebugAssertException has changed location and the LuceneTestFrameworkInitializer.InitializeStaticState() method needs to be updated with the new type string.");
404+
}
405+
#endif
406+
384407
private class MyException : Exception
385408
{
386409
public MyException(string message) : base(message)

src/Lucene.Net.Tests/Support/TestVIntUtils.cs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,5 +171,93 @@ public void TestMaxLengths()
171171
assertEquals(VIntUtils.MaxVInt32Length, EncodeVInt32(-1).Length);
172172
assertEquals(VIntUtils.MaxVInt64Length, EncodeVInt64(long.MaxValue).Length);
173173
}
174+
175+
#if DEBUG
176+
177+
[TestCase(0)]
178+
[TestCase(1)]
179+
[TestCase(127)]
180+
[TestCase(128)]
181+
[TestCase(16383)]
182+
[TestCase(16384)]
183+
[TestCase(2097151)]
184+
[TestCase(2097152)]
185+
[TestCase(268435455)]
186+
[TestCase(int.MaxValue)]
187+
[TestCase(-1)]
188+
[LuceneNetSpecific]
189+
public void TestReadVInt32_DebugAssertRequiresCompleteSpan(int value)
190+
{
191+
byte[] encoded = EncodeVInt32(value);
192+
193+
// Every prefix shorter than the encoded value should assert.
194+
for (int length = 0; length < encoded.Length; length++)
195+
{
196+
Assert.Throws(Lucene.ExceptionExtensions.DebugAssertExceptionType, () =>
197+
{
198+
VIntUtils.TryReadVInt32(encoded.AsSpan(0, length), out _, out _);
199+
}, $"Expected {Lucene.ExceptionExtensions.DebugAssertExceptionType.AssemblyQualifiedName} for value {value} with span length {length}.");
200+
}
201+
202+
// The exact encoded length should succeed.
203+
{
204+
Assert.DoesNotThrow(() =>
205+
{
206+
bool ok = VIntUtils.TryReadVInt32(encoded, out int result, out int count);
207+
208+
assertTrue(ok);
209+
assertEquals(value, result);
210+
assertEquals(encoded.Length, count);
211+
});
212+
}
213+
}
214+
215+
[TestCase(0L)]
216+
[TestCase(1L)]
217+
[TestCase(127L)]
218+
[TestCase(128L)]
219+
[TestCase(16383L)]
220+
[TestCase(16384L)]
221+
[TestCase(2097151L)]
222+
[TestCase(2097152L)]
223+
[TestCase(268435455L)]
224+
[TestCase(268435456L)]
225+
[TestCase(34359738367L)]
226+
[TestCase(34359738368L)]
227+
[TestCase(4398046511103L)]
228+
[TestCase(4398046511104L)]
229+
[TestCase(562949953421311L)]
230+
[TestCase(562949953421312L)]
231+
[TestCase(72057594037927935L)]
232+
[TestCase(72057594037927936L)]
233+
[TestCase(long.MaxValue)]
234+
[LuceneNetSpecific]
235+
public void TestReadVInt64_DebugAssertRequiresCompleteSpan(long value)
236+
{
237+
byte[] encoded = EncodeVInt64(value);
238+
239+
// Every prefix shorter than the encoded value should assert.
240+
for (int length = 0; length < encoded.Length; length++)
241+
{
242+
Assert.Throws(Lucene.ExceptionExtensions.DebugAssertExceptionType, () =>
243+
{
244+
VIntUtils.TryReadVInt64(encoded.AsSpan(0, length), out _, out _);
245+
}, $"Expected {Lucene.ExceptionExtensions.DebugAssertExceptionType.AssemblyQualifiedName} for value {value} with span length {length}.");
246+
}
247+
248+
// The exact encoded length should succeed.
249+
{
250+
Assert.DoesNotThrow(() =>
251+
{
252+
bool ok = VIntUtils.TryReadVInt64(encoded, out long result, out int count);
253+
254+
assertTrue(ok);
255+
assertEquals(value, result);
256+
assertEquals(encoded.Length, count);
257+
});
258+
}
259+
}
260+
261+
#endif
174262
}
175263
}

src/Lucene.Net/Support/VIntUtils.cs

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ internal static class VIntUtils
5555
/// extra high bits set.</returns>
5656
public static bool TryReadVInt32(ReadOnlySpan<byte> source, out int value, out int count)
5757
{
58-
Debug.Assert(source.Length >= 1);
58+
AssertCanContainVInt32(source);
5959
byte b = source[0];
6060
if (b <= sbyte.MaxValue) // LUCENENET: Optimized equivalent of "if ((sbyte)b >= 0)"
6161
{
@@ -109,7 +109,7 @@ public static bool TryReadVInt32(ReadOnlySpan<byte> source, out int value, out i
109109
/// the continuation bit set (which would indicate a negative value, disallowed).</returns>
110110
public static bool TryReadVInt64(ReadOnlySpan<byte> source, out long value, out int count)
111111
{
112-
Debug.Assert(source.Length >= 1);
112+
AssertCanContainVInt64(source);
113113
byte b = source[0];
114114
if (b <= sbyte.MaxValue)
115115
{
@@ -181,6 +181,42 @@ public static bool TryReadVInt64(ReadOnlySpan<byte> source, out long value, out
181181
return b <= sbyte.MaxValue;
182182
}
183183

184+
[Conditional("DEBUG")]
185+
private static void AssertCanContainVInt32(ReadOnlySpan<byte> source)
186+
{
187+
int length = source.Length;
188+
if (length >= MaxVInt32Length) return;
189+
190+
int limit = Math.Min(length, MaxVInt32Length);
191+
192+
for (int i = 0; i < limit; i++)
193+
{
194+
if ((source[i] & 0x80) == 0)
195+
return;
196+
}
197+
198+
Debug.Assert(length >= MaxVInt32Length,
199+
"The span is too short to contain a complete VInt32.");
200+
}
201+
202+
[Conditional("DEBUG")]
203+
private static void AssertCanContainVInt64(ReadOnlySpan<byte> source)
204+
{
205+
int length = source.Length;
206+
if (length >= MaxVInt64Length) return;
207+
208+
int limit = Math.Min(length, MaxVInt64Length);
209+
210+
for (int i = 0; i < limit; i++)
211+
{
212+
if ((source[i] & 0x80) == 0)
213+
return;
214+
}
215+
216+
Debug.Assert(length >= MaxVInt64Length,
217+
"The span is too short to contain a complete VInt64.");
218+
}
219+
184220
// LUCENENET: The throw sites below are factored into dedicated, non-inlined helpers so the
185221
// hot Read*VInt* overrides that call them stay small enough for the JIT to inline. Keeping
186222
// the `throw` + message string out of the caller body avoids bloating its IL (the inliner

0 commit comments

Comments
 (0)