blob: 38bc0ef9eae38ec3584d3619706efa32f4dc976a [file] [log] [blame]
// Rougly similar to: https://github.com/randomizedtesting/randomizedtesting/blob/release/2.7.8/randomized-runner/src/main/java/com/carrotsearch/randomizedtesting/RandomizedContext.java
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
{
/*
* 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>
/// Extensions to NUnit's test context for randomized testing.
/// </summary>
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<J2N.Randomizer> randomGenerator;
private readonly Test currentTest;
private readonly Assembly currentTestAssembly;
private readonly long randomSeed;
private volatile string? randomSeedAsString;
private 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>
/// <param name="currentTest">The test or test fixture for the context.</param>
/// <param name="currentTestAssembly">The test assembly. This can be used in <see cref="LuceneTestFrameworkInitializer"/> to scan the test assembly for attributes.</param>
/// <param name="randomSeed">The initial random seed that can be used to regenerate this context.</param>
/// <param name="testSeed">The individual test's seed to regenerate the test.</param>
public RandomizedContext(Test currentTest, Assembly currentTestAssembly, long randomSeed, long testSeed)
{
this.currentTest = currentTest ?? throw new ArgumentNullException(nameof(currentTest));
this.currentTestAssembly = currentTestAssembly ?? throw new ArgumentNullException(nameof(currentTestAssembly));
this.randomSeed = randomSeed;
this.testSeed = testSeed;
this.randomGenerator = new ThreadLocal<J2N.Randomizer>(() => new J2N.Randomizer(this.testSeed));
}
/// <summary>
/// Gets the initial seed that was used to initialize the current context.
/// </summary>
public long RandomSeed => randomSeed;
/// <summary>
/// Gets the initial seed as a hexadecimal string for display/configuration purposes.
/// </summary>
public string RandomSeedAsString => randomSeedAsString ??= SeedUtils.FormatSeed(randomSeed, testSeed);
/// <summary>
/// The current test for this context.
/// </summary>
public Test CurrentTest => currentTest;
/// <summary>
/// The current test assembly, which may be used to scan the assembly for custom attributes.
/// </summary>
public Assembly CurrentTestAssembly => currentTestAssembly;
/// <summary>
/// The random seed for this test's <see cref="RandomGenerator"/>.
/// </summary>
public long TestSeed => Interlocked.Read(ref this.testSeed);
/// <summary>
/// Gets the RandomGenerator specific to this Test and thread. This random generator implementatation
/// is not platform specific, so random numbers generated on one operating system will work on another.
/// <para/>
/// NOTE: NUnit doesn't currently set the <see cref="Test.Seed"/> property for the test fixtures
/// when using their built-in <see cref="NUnit.Framework.TestFixtureAttribute"/> or
/// <see cref="NUnit.Framework.SetUpFixtureAttribute"/>. This means the seed will always be 0
/// when using this property from <see cref="NUnit.Framework.OneTimeSetUpAttribute"/> or
/// <see cref="NUnit.Framework.OneTimeTearDownAttribute"/>, so it cannot be relied upon to create
/// 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
{
get
{
var random = randomGenerator.Value!;
UninterruptableMonitor.Enter(random.SyncRoot);
try
{
// Ensure the current thread is using the latest test seed.
if (random.Seed != testSeed)
random.Seed = testSeed;
}
finally
{
UninterruptableMonitor.Exit(random.SyncRoot);
}
return random;
}
}
/// <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>
/// <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());
}
}
/// <summary>
/// Resets the local <see cref="testSeed"/> field so it can be
/// used to update each thread that uses <see cref="randomGenerator"/> if
/// the seed doesn't match.
/// <para/>
/// Although calling this method is threadsafe, it should not be
/// called in the middle of a test. Instead, it should be called
/// between test runs to prevent non-repeatable random conditions
/// from occurring during a test.
/// </summary>
/// <param name="testSeed">The new test seed. This value will be
/// used to initialize the random generator for the test run.</param>
internal void ResetSeed(long testSeed)
{
var random = this.randomGenerator.Value!;
UninterruptableMonitor.Enter(random.SyncRoot);
try
{
this.randomSeedAsString = null;
this.testSeed = testSeed;
// Note that this resets the current thread only.
// That is why we have to check in the RandomGenerator
// property getter that the Seed is up-to-date.
random.Seed = testSeed;
}
finally
{
UninterruptableMonitor.Exit(random.SyncRoot);
}
}
}
}