Skip to content

Commit

Permalink
Added DisposeAfterSuite/DisposeAfterTest feature to test framework (a…
Browse files Browse the repository at this point in the history
…pache#1096)

* Lucene.Net.TestFramework: Implemented functionality of DisposeAfterTest() and DisposeAfterSuite() and removed TestMarkerFailure and SuiteMarkerFailure, since this info is already available through NUnit's TestExecutionContext.

* Lucene.Net.Util.TestRuleMarkFailure: Eliminated the class and added comments to indicate why we don't need this class in .NET. It is not sensible to have it because result state is already tracked by NUnit and is context sensitive.

* BUG: Lucene.Net.Tests.Grouping/TestGroupingExtra.cs: Directory, IndexWriter, and IndexReader were not being disposed in any of these tests. Not closing the wrapped reader was being detected by the test framework as a problem and casing it to fail the test run, so added using statements to each of these instances so they will be cleaned up at the end of each test.

* Lucene.Net.Util.LuceneTestCase::OneTimeTearDown(): Log the class that caused any failure, so we can debug more easily.

* Lucene.Net.Util.Fst.TestFSTs: Added SuppressTempFileChecks attribute to ignore the fact that the LineDocsFile is not being deleted.

* Lucene.Net.Util.LifecycleScope: Marked internal - this is part of randomizedtesting and is not exposed publicly by Lucene.

* Lucene.Net.Util.RandomizedContext: Changed to use Lazy<ConcurrentQueue<IDisposable>> to eliminate locking

* Lucene.Net.Util.LifecycleScope: Added license header

* Lucene.Net.Util: Added DisposableResourceInfo class to track scope, thread name, and caller stack trace so this info can be included in the output if there is an exception during dispose.

* Lucene.Net.Util.DisposableResourceInfo: Added license header

* Lucene.Net.Util.RandomizedContext: Reverted to using conextLock and regular List<T> to stay more closely aligned with the upstream randomizedtesting code.

* Lucene.Net.Util.LuceneTestCase (TearDown() + OneTimeTearDown()): use empty throw statement to preserve stack details, since we are still in the finally block.

* Lucene.Net.Util.RandomizedContext: Lazily-load random seed string only if it is required

* Lucene.Net.Util.RandomizedContext: Updated comments
  • Loading branch information
NightOwl888 authored Jan 17, 2025
1 parent 84b7c60 commit dcfa0e2
Show file tree
Hide file tree
Showing 10 changed files with 532 additions and 278 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
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
{
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/// <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;
}
}
41 changes: 41 additions & 0 deletions src/Lucene.Net.TestFramework/Support/Util/LifecycleScope.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Lucene.Net.Util
{
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/// <summary>
/// Lifecycle stages for tracking resources.
/// </summary>
internal enum LifecycleScope // From randomizedtesing
{
/// <summary>
/// A single test case.
/// </summary>
TEST,

/// <summary>
/// A single suite (class).
/// </summary>
SUITE
}
}
217 changes: 206 additions & 11 deletions src/Lucene.Net.TestFramework/Support/Util/RandomizedContext.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
using J2N;
using Lucene.Net.Support.Threading;
using NUnit.Framework.Interfaces;
using NUnit.Framework.Internal;
using System;
using System.Collections.Generic;
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

namespace Lucene.Net.Util
{
Expand Down Expand Up @@ -30,14 +38,27 @@ 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 volatile string? randomSeedAsString;
private readonly long testSeed;

/// <summary>
/// Disposable resources.
/// </summary>
private List<DisposableResourceInfo>? disposableResources = null;

/// <summary>
/// Coordination at context level.
/// </summary>
private readonly object contextLock = new object();

/// <summary>
/// Initializes the randomized context.
/// </summary>
Expand All @@ -50,7 +71,6 @@ public RandomizedContext(Test currentTest, Assembly currentTestAssembly, long ra
this.currentTest = currentTest ?? throw new ArgumentNullException(nameof(currentTest));
this.currentTestAssembly = currentTestAssembly ?? throw new ArgumentNullException(nameof(currentTestAssembly));
this.randomSeed = randomSeed;
this.randomSeedAsHex = SeedUtils.FormatSeed(randomSeed);
this.testSeed = testSeed;
this.randomGenerator = new ThreadLocal<Random>(() => new J2N.Randomizer(this.testSeed));
}
Expand All @@ -63,7 +83,7 @@ public RandomizedContext(Test currentTest, Assembly currentTestAssembly, long ra
/// <summary>
/// Gets the initial seed as a hexadecimal string for display/configuration purposes.
/// </summary>
public string RandomSeedAsHex => randomSeedAsHex;
public string RandomSeedAsString => randomSeedAsString ??= SeedUtils.FormatSeed(randomSeed);

/// <summary>
/// The current test for this context.
Expand Down Expand Up @@ -92,21 +112,196 @@ public RandomizedContext(Test currentTest, Assembly currentTestAssembly, long ra
/// random test data in these cases. Using the <see cref="LuceneTestCase.TestFixtureAttribute"/>
/// will set the seed properly and make it possible to repeat the result.
/// </summary>
public Random RandomGenerator => randomGenerator.Value;
public Random RandomGenerator => randomGenerator.Value!;

/// <summary>
/// Gets the randomized context for the current test or test fixture.
/// <para/>
/// If <c>null</c>, the call is being made out of context and the random test behavior
/// will not be repeatable.
/// </summary>
public static RandomizedContext? CurrentContext
=> TestExecutionContext.CurrentContext.CurrentTest.GetRandomizedContext();

/// <summary>
/// Registers the given <paramref name="resource"/> at the end of a given
/// lifecycle <paramref name="scope"/>.
/// </summary>
/// <typeparam name="T">Type of <see cref="IDisposable"/>.</typeparam>
/// <param name="resource">A resource to dispose.</param>
/// <param name="scope">The scope to dispose the resource in.</param>
/// <returns>The <paramref name="resource"/> (for chaining).</returns>
/// <remarks>
/// Due to limitations of NUnit, any exceptions or assertions raised
/// from the <paramref name="resource"/> will not be respected. However, if
/// you want to detect a failure, do note that the message from either one
/// will be printed to StdOut.
/// </remarks>
public T DisposeAtEnd<T>(T resource, LifecycleScope scope) where T : IDisposable
{
if (currentTest.IsTest())
{
if (scope == LifecycleScope.TEST)
{
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, scope);
}
}
else if (currentTest.IsTestClass())
{
AddDisposableAtEnd(resource, scope);
}
else
{
throw new NotSupportedException("Only runnable tests and test classes are supported.");
}

return resource;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void AddDisposableAtEnd(IDisposable resource, LifecycleScope scope)
{
UninterruptableMonitor.Enter(contextLock);
try
{
disposableResources ??= new List<DisposableResourceInfo>();
disposableResources.Add(new DisposableResourceInfo(resource, scope, Thread.CurrentThread.Name, new StackTrace(skipFrames: 3)));
}
finally
{
UninterruptableMonitor.Exit(contextLock);
}
}

private Test? FindClassLevelTest(Test test)
{
ITest? current = test;

while (current != null)
{
// Check if this test is at the class level
if (current.IsTestClass() && current is Test t)
{
return t;
}

current = current.Parent;
}

return null;
}

internal void DisposeResources()
{
List<DisposableResourceInfo>? resources;
UninterruptableMonitor.Enter(contextLock);
try
{
resources = disposableResources; // Set the resources to a local variable
disposableResources = null; // Set disposableResources field to null so our local list will go out of scope when we are done
}
finally
{
UninterruptableMonitor.Exit(contextLock);
}

if (resources is not null)
{
Exception? th = null;

foreach (DisposableResourceInfo disposable in resources)
{
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
}
}
} // resources 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>
public static RandomizedContext CurrentContext
/// <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)
{
get
destination.WriteLine(FormatStackTrace(exception));
}

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

foreach (var suppressedException in exception.GetSuppressedAsList())
{
var currentTest = TestExecutionContext.CurrentContext.CurrentTest;
sb.AppendLine("Suppressed: ");
FormatException(suppressedException, sb);
}

return sb.ToString();
}

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

if (currentTest.Properties.ContainsKey(RandomizedContextPropertyName))
return (RandomizedContext)currentTest.Properties.Get(RandomizedContextPropertyName);
string? scope = (string?)exception.Data[RandomizedContextScopeKeyName];
string? threadName = (string?)exception.Data[RandomizedContextThreadNameKeyName];
StackTrace? stackTrace = (StackTrace?)exception.Data[RandomizedContextStackTraceKeyName];

return null; // We are out of random context and cannot respond with results that are repeatable.
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());
}
}
}
Expand Down
Loading

0 comments on commit dcfa0e2

Please sign in to comment.