You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucenenet.apache.org by ni...@apache.org on 2026/07/04 15:03:50 UTC
(lucenenet) branch master updated: Fix for `DebugAssertException` detection during testing + `VIntUtils` length assertions (#1400)
This is an automated email from the ASF dual-hosted git repository.
NightOwl888 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/lucenenet.git
The following commit(s) were added to refs/heads/master by this push:
new 736554ef5 Fix for `DebugAssertException` detection during testing + `VIntUtils` length assertions (#1400)
736554ef5 is described below
commit 736554ef592c0e1daf59c9ec87d512216186456a
Author: Shad Storhaug <sh...@shadstorhaug.com>
AuthorDate: Sat Jul 4 22:03:45 2026 +0700
Fix for `DebugAssertException` detection during testing + `VIntUtils` length assertions (#1400)
---
.../Support/Util/LuceneTestFrameworkInitializer.cs | 7 +-
.../Support/AssemblyScanningTestCase.cs | 19 ++++-
.../ExceptionHandling/ExceptionScanningTestCase.cs | 31 +++++++-
.../ExceptionHandling/TestExceptionExtensions.cs | 23 ++++++
src/Lucene.Net.Tests/Support/TestVIntUtils.cs | 88 ++++++++++++++++++++++
src/Lucene.Net/Support/VIntUtils.cs | 40 +++++++++-
6 files changed, 196 insertions(+), 12 deletions(-)
diff --git a/src/Lucene.Net.TestFramework/Support/Util/LuceneTestFrameworkInitializer.cs b/src/Lucene.Net.TestFramework/Support/Util/LuceneTestFrameworkInitializer.cs
index fbdb0f8d0..50fbb2180 100644
--- a/src/Lucene.Net.TestFramework/Support/Util/LuceneTestFrameworkInitializer.cs
+++ b/src/Lucene.Net.TestFramework/Support/Util/LuceneTestFrameworkInitializer.cs
@@ -3,7 +3,6 @@
using NUnit.Framework;
using System;
using System.Diagnostics.CodeAnalysis;
-using System.IO;
namespace Lucene.Net.Util
{
@@ -358,11 +357,13 @@ internal void InitializeStaticState()
// Identify the Debug.Assert() exception so it can be excluded from being swallowed by catch blocks.
// These types are internal, so we can identify them using Reflection.
Lucene.ExceptionExtensions.DebugAssertExceptionType =
+ // Microsoft.NET.Test.Sdk 16.6.0+ (used by Visual Studio Test Explorer)
+ Type.GetType("Microsoft.VisualStudio.TestPlatform.TestHost.DebugAssertException, testhost")
// .NET 5/.NET Core 3.x
- Type.GetType("System.Diagnostics.DebugProvider+DebugAssertException, System.Private.CoreLib")
+ ?? Type.GetType("System.Diagnostics.DebugProvider+DebugAssertException, System.Private.CoreLib")
// .NET Core 2.x
?? Type.GetType("System.Diagnostics.Debug+DebugAssertException, System.Private.CoreLib");
- // .NET Framework doesn't throw in this case
+ // .NET Framework doesn't throw in this case
}
/// <summary>
diff --git a/src/Lucene.Net.Tests.AllProjects/Support/AssemblyScanningTestCase.cs b/src/Lucene.Net.Tests.AllProjects/Support/AssemblyScanningTestCase.cs
index 135843c01..bdf9278e4 100644
--- a/src/Lucene.Net.Tests.AllProjects/Support/AssemblyScanningTestCase.cs
+++ b/src/Lucene.Net.Tests.AllProjects/Support/AssemblyScanningTestCase.cs
@@ -59,10 +59,23 @@ public abstract class AssemblyScanningTestCase : LuceneTestCase
};
- public static readonly Assembly[] DotNetAssemblies = new Assembly[]
+ public static readonly Assembly[] DotNetAssemblies = LoadDotNetAssemblies();
+
+ private static Assembly[] LoadDotNetAssemblies()
{
- typeof(Exception).Assembly
- };
+ var list = new List<Assembly>()
+ {
+ typeof(Exception).Assembly
+ };
+
+ Assembly testHostAssembly = Type.GetType("Microsoft.VisualStudio.TestPlatform.TestHost.DebugAssertException, testhost")?.Assembly;
+ if (testHostAssembly is not null)
+ {
+ list.Add(testHostAssembly);
+ }
+
+ return list.ToArray();
+ }
public static readonly Assembly[] NUnitAssemblies = new Assembly[]
{
diff --git a/src/Lucene.Net.Tests.AllProjects/Support/ExceptionHandling/ExceptionScanningTestCase.cs b/src/Lucene.Net.Tests.AllProjects/Support/ExceptionHandling/ExceptionScanningTestCase.cs
index 4c5ffc4ef..399da4c55 100644
--- a/src/Lucene.Net.Tests.AllProjects/Support/ExceptionHandling/ExceptionScanningTestCase.cs
+++ b/src/Lucene.Net.Tests.AllProjects/Support/ExceptionHandling/ExceptionScanningTestCase.cs
@@ -39,11 +39,21 @@ namespace Lucene.Net.Support.ExceptionHandling
public abstract class ExceptionScanningTestCase : AssemblyScanningTestCase
{
// Internal types references
- private static readonly Type DebugAssertExceptionType =
+ private static readonly Type ClrDebugAssertExceptionType =
// .NET 5/.NET Core 3.x
Type.GetType("System.Diagnostics.DebugProvider+DebugAssertException, System.Private.CoreLib")
// .NET Core 2.x
?? Type.GetType("System.Diagnostics.Debug+DebugAssertException, System.Private.CoreLib");
+
+ private static readonly Type TestHostDebugAssertExceptionType =
+ // Microsoft.NET.Test.Sdk 16.6.0+ (used by Visual Studio Test Explorer)
+ Type.GetType("Microsoft.VisualStudio.TestPlatform.TestHost.DebugAssertException, testhost");
+
+ private static readonly Type DebugAssertExceptionType =
+ // Microsoft.NET.Test.Sdk 16.6.0+ (used by Visual Studio Test Explorer)
+ TestHostDebugAssertExceptionType
+ // .NET Core
+ ?? ClrDebugAssertExceptionType;
// .NET Framework doesn't throw in this case
private static readonly Type MetadataExceptionType =
@@ -465,15 +475,28 @@ private static IEnumerable<Type> LoadKnownRuntimeExceptionTypes()
};
// Special case - this doesn't exist on .NET Framework, so we only add it if not null
- if (DebugAssertExceptionType is not null)
+ if (ClrDebugAssertExceptionType is not null)
{
- result[DebugAssertExceptionType] = (exceptionType, message) =>
+ result[ClrDebugAssertExceptionType] = (exceptionType, message) =>
{
//private sealed class DebugAssertException : Exception
//{
// internal DebugAssertException(string? message, string? detailMessage, string? stackTrace)
BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Instance;
- return Activator.CreateInstance(DebugAssertExceptionType, flags, null, new object[] { message, null, null }, CultureInfo.InvariantCulture);
+ return Activator.CreateInstance(ClrDebugAssertExceptionType, flags, null, new object[] { message, null, null }, CultureInfo.InvariantCulture);
+ };
+ }
+
+ // Special case - Microsoft.NET.Test.Sdk 16.6.0+ contains its own Debug.Assert type that is thrown when debugging tests
+ if (TestHostDebugAssertExceptionType is not null)
+ {
+ result[TestHostDebugAssertExceptionType] = (exceptionType, message) =>
+ {
+ //internal sealed class DebugAssertException : Exception
+ //{
+ //public DebugAssertException(string message, string stackTrace) : base(message)
+ BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Instance;
+ return Activator.CreateInstance(TestHostDebugAssertExceptionType, flags, null, new object[] { message, string.Empty }, CultureInfo.InvariantCulture);
};
}
diff --git a/src/Lucene.Net.Tests.AllProjects/Support/ExceptionHandling/TestExceptionExtensions.cs b/src/Lucene.Net.Tests.AllProjects/Support/ExceptionHandling/TestExceptionExtensions.cs
index 224f0cf77..584a895de 100644
--- a/src/Lucene.Net.Tests.AllProjects/Support/ExceptionHandling/TestExceptionExtensions.cs
+++ b/src/Lucene.Net.Tests.AllProjects/Support/ExceptionHandling/TestExceptionExtensions.cs
@@ -381,6 +381,29 @@ private static void ThrowException(Type exceptionType)
}
}
+#if DEBUG
+ /// <summary>
+ /// Tests to ensure that the DebugAssertException type string is correct for the current test environment.
+ /// If this test fails, it means that the DebugAssertException has changed location and the
+ /// <see cref="LuceneTestFrameworkInitializer.InitializeStaticState()"/> method needs to be updated with
+ /// the new type string.
+ /// <para/>
+ /// Microsoft controls this location, it is internal, and changes from time to time. Currently, it is in
+ /// Microsoft.NET.Test.Sdk as of version 16.6.0.
+ /// <para/>
+ /// Note that this exception only applies when the test SDK is attached. If the test SDK is not attached,
+ /// then this exception falls back to a different exception type in the current .NET SDK. This can happen
+ /// in DEBUG mode when running as a console application, for example.
+ /// </summary>
+ [Test]
+ public void TestDebugAssertExceptionTypeString()
+ {
+ Assert.IsNotNull(
+ Type.GetType("Microsoft.VisualStudio.TestPlatform.TestHost.DebugAssertException, testhost"),
+ "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.");
+ }
+#endif
+
private class MyException : Exception
{
public MyException(string message) : base(message)
diff --git a/src/Lucene.Net.Tests/Support/TestVIntUtils.cs b/src/Lucene.Net.Tests/Support/TestVIntUtils.cs
index 220b5acfe..f53d5f425 100644
--- a/src/Lucene.Net.Tests/Support/TestVIntUtils.cs
+++ b/src/Lucene.Net.Tests/Support/TestVIntUtils.cs
@@ -171,5 +171,93 @@ public void TestMaxLengths()
assertEquals(VIntUtils.MaxVInt32Length, EncodeVInt32(-1).Length);
assertEquals(VIntUtils.MaxVInt64Length, EncodeVInt64(long.MaxValue).Length);
}
+
+#if DEBUG
+
+ [TestCase(0)]
+ [TestCase(1)]
+ [TestCase(127)]
+ [TestCase(128)]
+ [TestCase(16383)]
+ [TestCase(16384)]
+ [TestCase(2097151)]
+ [TestCase(2097152)]
+ [TestCase(268435455)]
+ [TestCase(int.MaxValue)]
+ [TestCase(-1)]
+ [LuceneNetSpecific]
+ public void TestReadVInt32_DebugAssertRequiresCompleteSpan(int value)
+ {
+ byte[] encoded = EncodeVInt32(value);
+
+ // Every prefix shorter than the encoded value should assert.
+ for (int length = 0; length < encoded.Length; length++)
+ {
+ Assert.Throws(Lucene.ExceptionExtensions.DebugAssertExceptionType, () =>
+ {
+ VIntUtils.TryReadVInt32(encoded.AsSpan(0, length), out _, out _);
+ }, $"Expected {Lucene.ExceptionExtensions.DebugAssertExceptionType.AssemblyQualifiedName} for value {value} with span length {length}.");
+ }
+
+ // The exact encoded length should succeed.
+ {
+ Assert.DoesNotThrow(() =>
+ {
+ bool ok = VIntUtils.TryReadVInt32(encoded, out int result, out int count);
+
+ assertTrue(ok);
+ assertEquals(value, result);
+ assertEquals(encoded.Length, count);
+ });
+ }
+ }
+
+ [TestCase(0L)]
+ [TestCase(1L)]
+ [TestCase(127L)]
+ [TestCase(128L)]
+ [TestCase(16383L)]
+ [TestCase(16384L)]
+ [TestCase(2097151L)]
+ [TestCase(2097152L)]
+ [TestCase(268435455L)]
+ [TestCase(268435456L)]
+ [TestCase(34359738367L)]
+ [TestCase(34359738368L)]
+ [TestCase(4398046511103L)]
+ [TestCase(4398046511104L)]
+ [TestCase(562949953421311L)]
+ [TestCase(562949953421312L)]
+ [TestCase(72057594037927935L)]
+ [TestCase(72057594037927936L)]
+ [TestCase(long.MaxValue)]
+ [LuceneNetSpecific]
+ public void TestReadVInt64_DebugAssertRequiresCompleteSpan(long value)
+ {
+ byte[] encoded = EncodeVInt64(value);
+
+ // Every prefix shorter than the encoded value should assert.
+ for (int length = 0; length < encoded.Length; length++)
+ {
+ Assert.Throws(Lucene.ExceptionExtensions.DebugAssertExceptionType, () =>
+ {
+ VIntUtils.TryReadVInt64(encoded.AsSpan(0, length), out _, out _);
+ }, $"Expected {Lucene.ExceptionExtensions.DebugAssertExceptionType.AssemblyQualifiedName} for value {value} with span length {length}.");
+ }
+
+ // The exact encoded length should succeed.
+ {
+ Assert.DoesNotThrow(() =>
+ {
+ bool ok = VIntUtils.TryReadVInt64(encoded, out long result, out int count);
+
+ assertTrue(ok);
+ assertEquals(value, result);
+ assertEquals(encoded.Length, count);
+ });
+ }
+ }
+
+#endif
}
}
diff --git a/src/Lucene.Net/Support/VIntUtils.cs b/src/Lucene.Net/Support/VIntUtils.cs
index b678dfef8..1bebd7705 100644
--- a/src/Lucene.Net/Support/VIntUtils.cs
+++ b/src/Lucene.Net/Support/VIntUtils.cs
@@ -55,7 +55,7 @@ internal static class VIntUtils
/// extra high bits set.</returns>
public static bool TryReadVInt32(ReadOnlySpan<byte> source, out int value, out int count)
{
- Debug.Assert(source.Length >= 1);
+ AssertCanContainVInt32(source);
byte b = source[0];
if (b <= sbyte.MaxValue) // LUCENENET: Optimized equivalent of "if ((sbyte)b >= 0)"
{
@@ -109,7 +109,7 @@ public static bool TryReadVInt32(ReadOnlySpan<byte> source, out int value, out i
/// the continuation bit set (which would indicate a negative value, disallowed).</returns>
public static bool TryReadVInt64(ReadOnlySpan<byte> source, out long value, out int count)
{
- Debug.Assert(source.Length >= 1);
+ AssertCanContainVInt64(source);
byte b = source[0];
if (b <= sbyte.MaxValue)
{
@@ -181,6 +181,42 @@ public static bool TryReadVInt64(ReadOnlySpan<byte> source, out long value, out
return b <= sbyte.MaxValue;
}
+ [Conditional("DEBUG")]
+ private static void AssertCanContainVInt32(ReadOnlySpan<byte> source)
+ {
+ int length = source.Length;
+ if (length >= MaxVInt32Length) return;
+
+ int limit = Math.Min(length, MaxVInt32Length);
+
+ for (int i = 0; i < limit; i++)
+ {
+ if ((source[i] & 0x80) == 0)
+ return;
+ }
+
+ Debug.Assert(length >= MaxVInt32Length,
+ "The span is too short to contain a complete VInt32.");
+ }
+
+ [Conditional("DEBUG")]
+ private static void AssertCanContainVInt64(ReadOnlySpan<byte> source)
+ {
+ int length = source.Length;
+ if (length >= MaxVInt64Length) return;
+
+ int limit = Math.Min(length, MaxVInt64Length);
+
+ for (int i = 0; i < limit; i++)
+ {
+ if ((source[i] & 0x80) == 0)
+ return;
+ }
+
+ Debug.Assert(length >= MaxVInt64Length,
+ "The span is too short to contain a complete VInt64.");
+ }
+
// LUCENENET: The throw sites below are factored into dedicated, non-inlined helpers so the
// hot Read*VInt* overrides that call them stay small enough for the JIT to inline. Keeping
// the `throw` + message string out of the caller body avoids bloating its IL (the inliner