Skip to content

Commit

Permalink
Lucene.Net.Util: Added DisposableResourceInfo class to track scope, t…
Browse files Browse the repository at this point in the history
…hread name, and caller stack trace so this info can be included in the output if there is an exception during dispose.
  • Loading branch information
NightOwl888 committed Jan 16, 2025
1 parent 9209ed4 commit 5414333
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
#nullable enable

namespace Lucene.Net.Util
{
/// <summary>
/// Allocation information (Thread, allocation stack) for tracking disposable
/// resources.
/// </summary>
internal sealed class DisposableResourceInfo // From randomizedtesing
{
private readonly IDisposable resource;
private readonly LifecycleScope scope;
private readonly StackTrace stackTrace;
private readonly string? threadName;

public DisposableResourceInfo(IDisposable resource, LifecycleScope scope, string? threadName, StackTrace stackTrace)
{
Debug.Assert(resource != null);

this.resource = resource!;
this.scope = scope;
this.stackTrace = stackTrace;
this.threadName = threadName;
}

public IDisposable Resource => resource;

public StackTrace StackTrace => stackTrace;

public LifecycleScope Scope => scope;

public string? ThreadName => threadName;
}
}
114 changes: 104 additions & 10 deletions src/Lucene.Net.TestFramework/Support/Util/RandomizedContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@
using NUnit.Framework.Internal;
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.ExceptionServices;
using System.Text;
using System.Threading;
#nullable enable

Expand Down Expand Up @@ -33,14 +37,17 @@ internal class RandomizedContext
{
// LUCENENET NOTE: Using an underscore to prefix the name hides it from "traits" in Test Explorer
internal const string RandomizedContextPropertyName = "_RandomizedContext";
internal const string RandomizedContextScopeKeyName = "_RandomizedContext_Scope";
internal const string RandomizedContextThreadNameKeyName = "_RandomizedContext_ThreadName";
internal const string RandomizedContextStackTraceKeyName = "_RandomizedContext_StackTrace";

private readonly ThreadLocal<Random> randomGenerator;
private readonly Test currentTest;
private readonly Assembly currentTestAssembly;
private readonly long randomSeed;
private readonly string randomSeedAsHex;
private readonly long testSeed;
private Lazy<ConcurrentQueue<IDisposable>>? toDisposeAtEnd = null;
private Lazy<ConcurrentQueue<DisposableResourceInfo>>? disposableResources = null;

/// <summary>
/// Initializes the randomized context.
Expand Down Expand Up @@ -110,8 +117,8 @@ public static RandomizedContext? CurrentContext
/// <summary>
/// Gets a lazily-initialized concurrent queue to use for resources that will be disposed at the end of the test or suite.
/// </summary>
internal ConcurrentQueue<IDisposable> ToDisposeAtEnd
=> (toDisposeAtEnd ??= new Lazy<ConcurrentQueue<IDisposable>>(() => new ConcurrentQueue<IDisposable>())).Value;
internal ConcurrentQueue<DisposableResourceInfo> DisposableResources
=> (disposableResources ??= new Lazy<ConcurrentQueue<DisposableResourceInfo>>(() => new ConcurrentQueue<DisposableResourceInfo>())).Value;

/// <summary>
/// Registers the given <paramref name="resource"/> at the end of a given
Expand All @@ -133,19 +140,19 @@ public T DisposeAtEnd<T>(T resource, LifecycleScope scope) where T : IDisposable
{
if (scope == LifecycleScope.TEST)
{
AddDisposableAtEnd(resource);
AddDisposableAtEnd(resource, scope);
}
else // LifecycleScope.SUITE
{
var context = FindClassLevelTest(currentTest).GetRandomizedContext();
if (context is null)
throw new InvalidOperationException($"The provided {LifecycleScope.TEST} has no conceptual {LifecycleScope.SUITE} associated with it.");
context.AddDisposableAtEnd(resource);
context.AddDisposableAtEnd(resource, scope);
}
}
else if (currentTest.IsTestClass())
{
AddDisposableAtEnd(resource);
AddDisposableAtEnd(resource, scope);
}
else
{
Expand All @@ -156,12 +163,12 @@ public T DisposeAtEnd<T>(T resource, LifecycleScope scope) where T : IDisposable
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void AddDisposableAtEnd(IDisposable resource)
internal void AddDisposableAtEnd(IDisposable resource, LifecycleScope scope)
{
// LUCENENET: ConcurrentQueue handles thread-safety internally, so no explicit locking is needed.
// Note that if we port more of randomizedtesting later, we may need to change this to a List<T> and
// a lock, but for now it will suit our needs without locking.
ToDisposeAtEnd.Enqueue(resource);
DisposableResources.Enqueue(new DisposableResourceInfo(resource, scope, Thread.CurrentThread.Name, new StackTrace(skipFrames: 3)));
}

private Test? FindClassLevelTest(Test test)
Expand All @@ -184,11 +191,98 @@ internal void AddDisposableAtEnd(IDisposable resource)

internal void DisposeResources()
{
Lazy<ConcurrentQueue<IDisposable>>? toDispose = Interlocked.Exchange(ref toDisposeAtEnd, null);
Lazy<ConcurrentQueue<DisposableResourceInfo>>? toDispose = Interlocked.Exchange(ref disposableResources, null);
if (toDispose?.IsValueCreated == true) // Does null check on toDispose
{
IOUtils.Dispose(toDispose.Value);
Exception? th = null;

foreach (DisposableResourceInfo disposable in toDispose.Value)
{
try
{
disposable.Resource.Dispose();
}
catch (Exception t) when (t.IsThrowable())
{
// Add details about the source of the exception, so they can be printed out later.
t.Data[RandomizedContextScopeKeyName] = disposable.Scope.ToString(); // string
t.Data[RandomizedContextThreadNameKeyName] = disposable.ThreadName; // string
t.Data[RandomizedContextStackTraceKeyName] = disposable.StackTrace; // System.Diagnostics.StackTrace

if (th is not null)
{
th.AddSuppressed(t);
}
else
{
th = t;
}
}
}

if (th is not null)
{
ExceptionDispatchInfo.Capture(th).Throw(); // LUCENENET: Rethrow to preserve stack details from the original throw
}
}
} // toDispose goes out of scope here - no need to Clear().

/// <summary>
/// Prints a stack trace of the <paramref name="exception"/> to the destination <see cref="TextWriter"/>.
/// The message will show additional stack details relevant to <see cref="DisposeAtEnd{T}(T, LifecycleScope)"/>
/// to identify the calling method, <see cref="LifecycleScope"/>, and name of the calling thread.
/// </summary>
/// <param name="exception">The exception to print. This may contain additional details in <see cref="Exception.Data"/>.</param>
/// <param name="destination">A <see cref="TextWriter"/> to write the output to.</param>
internal static void PrintStackTrace(Exception exception, TextWriter destination)
{
destination.WriteLine(FormatStackTrace(exception));
}

private static string FormatStackTrace(Exception exception)
{
StringBuilder sb = new StringBuilder(256);
FormatException(exception, sb);

foreach (var suppressedException in exception.GetSuppressedAsList())
{
sb.AppendLine("Suppressed: ");
FormatException(suppressedException, sb);
}

return sb.ToString();
}

private static void FormatException(Exception exception, StringBuilder destination)
{
destination.AppendLine(exception.ToString());

string? scope = (string?)exception.Data[RandomizedContextScopeKeyName];
string? threadName = (string?)exception.Data[RandomizedContextThreadNameKeyName];
StackTrace? stackTrace = (StackTrace?)exception.Data[RandomizedContextStackTraceKeyName];

bool hasData = scope != null || threadName != null || stackTrace != null;
if (!hasData)
{
return;
}

destination.AppendLine("Caller Details:");
if (scope != null)
{
destination.Append("Scope: ");
destination.AppendLine(scope);
}
if (threadName != null)
{
destination.Append("Thread Name: ");
destination.AppendLine(threadName);
}
if (stackTrace != null)
{
destination.Append("Stack Trace:");
destination.AppendLine(stackTrace.ToString());
}
}
}
}
23 changes: 17 additions & 6 deletions src/Lucene.Net.TestFramework/Util/LuceneTestCase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.ExceptionServices;
using System.Text.RegularExpressions;
using System.Threading;
using static Lucene.Net.Search.FieldCache;
Expand Down Expand Up @@ -972,7 +973,16 @@ System Properties
}

// LUCENENET: DisposeAfterTest runs last
RandomizedContext.CurrentContext.DisposeResources();
try
{
RandomizedContext.CurrentContext.DisposeResources();
}
catch (Exception ex) when (ex.IsThrowable())
{
NUnit.Framework.TestContext.Error.WriteLine($"[ERROR] An exception occurred during TearDown:");
RandomizedContext.PrintStackTrace(ex, NUnit.Framework.TestContext.Error);
ExceptionDispatchInfo.Capture(ex).Throw();
}
}

/// <summary>
Expand All @@ -990,7 +1000,7 @@ public virtual void OneTimeSetUp()
{
ClassEnvRule.Before();
}
catch (Exception ex)
catch (Exception ex) when (ex.IsThrowable())
{
// This is a bug in the test framework that should be fixed and/or reported if it occurs.
if (FailOnTestFixtureOneTimeSetUpError)
Expand Down Expand Up @@ -1019,7 +1029,7 @@ public virtual void OneTimeTearDown()
{
ClassEnvRule.After();
}
catch (Exception ex)
catch (Exception ex) when (ex.IsThrowable())
{
// LUCENENET: Patch NUnit so it will report a failure in stderr if there was an exception during teardown.
NUnit.Framework.TestContext.Error.WriteLine($"[ERROR] OneTimeTearDown: An exception occurred during ClassEnvRule.After() in {GetType().FullName}:\n{ex}");
Expand All @@ -1028,7 +1038,7 @@ public virtual void OneTimeTearDown()
{
CleanupTemporaryFiles();
}
catch (Exception ex)
catch (Exception ex) when (ex.IsThrowable())
{
// LUCENENET: Patch NUnit so it will report a failure in stderr if there was an exception during teardown.
NUnit.Framework.TestContext.Error.WriteLine($"[ERROR] OneTimeTearDown: An exception occurred during CleanupTemporaryFiles() in {GetType().FullName}:\n{ex}");
Expand All @@ -1039,10 +1049,11 @@ public virtual void OneTimeTearDown()
{
RandomizedContext.CurrentContext.DisposeResources();
}
catch (Exception ex)
catch (Exception ex) when (ex.IsThrowable())
{
// LUCENENET: Patch NUnit so it will report a failure in stderr if there was an exception during teardown.
NUnit.Framework.TestContext.Error.WriteLine($"[ERROR] OneTimeTearDown: An exception occurred during RandomizedContext.DisposeResources() in {GetType().FullName}:\n{ex}");
NUnit.Framework.TestContext.Error.WriteLine($"[ERROR] OneTimeTearDown: An exception occurred during RandomizedContext.DisposeResources() in {GetType().FullName}:");
RandomizedContext.PrintStackTrace(ex, NUnit.Framework.TestContext.Error);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public override void TestProtectedFieldNames(Type typeFromTargetAssembly)
[TestCase(typeof(Lucene.Net.RandomExtensions))]
public override void TestPrivateFieldNames(Type typeFromTargetAssembly)
{
base.TestPrivateFieldNames(typeFromTargetAssembly, @"ApiScanTestBase|TestUtil\.MaxRecursionBound|Assert\.FailureFormat|Lucene\.Net\.Util\.RandomizedContext\.RandomizedContextPropertyName|Lucene\.Net\.Util\.DefaultNamespaceTypeWrapper\.AllMembers|^Lucene\.Net\.Store\.BaseDirectoryWrapper\.(?:True|False)");
base.TestPrivateFieldNames(typeFromTargetAssembly, @"ApiScanTestBase|TestUtil\.MaxRecursionBound|Assert\.FailureFormat|Lucene\.Net\.Util\.RandomizedContext|Lucene\.Net\.Util\.DefaultNamespaceTypeWrapper\.AllMembers|^Lucene\.Net\.Store\.BaseDirectoryWrapper\.(?:True|False)");
}

[Test, LuceneNetSpecific]
Expand Down Expand Up @@ -141,4 +141,4 @@ public override void TestForMembersAcceptingOrReturningListOrDictionary(Type typ
base.TestForMembersAcceptingOrReturningListOrDictionary(typeFromTargetAssembly);
}
}
}
}

0 comments on commit 5414333

Please sign in to comment.