Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions TUnit.Core/Defaults.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
namespace TUnit.Core;

/// <summary>
/// Default values shared across TUnit.Core and TUnit.Engine.
/// Centralizes magic numbers so they can be tuned in a single place.
/// </summary>
public static class Defaults
{
/// <summary>
/// Default timeout applied to individual tests when no <c>[Timeout]</c> attribute is specified.
/// Can be overridden per-test via <see cref="TUnit.Core.TimeoutAttribute"/>.
/// </summary>
public static readonly TimeSpan TestTimeout = TimeSpan.FromMinutes(30);

/// <summary>
/// Default timeout applied to hook methods (Before/After at every level)
/// when no explicit timeout is configured.
/// </summary>
public static readonly TimeSpan HookTimeout = TimeSpan.FromMinutes(5);

/// <summary>
/// Time allowed for a graceful shutdown after a cancellation request (Ctrl+C / SIGTERM)
/// before the process is forcefully terminated.
/// </summary>
public static readonly TimeSpan ForcefulExitTimeout = TimeSpan.FromSeconds(30);

/// <summary>
/// Brief delay during process exit to allow After hooks registered via
/// <see cref="CancellationToken.Register"/> to execute before the process terminates.
/// </summary>
public static readonly TimeSpan ProcessExitHookDelay = TimeSpan.FromMilliseconds(500);
}
4 changes: 2 additions & 2 deletions TUnit.Core/EngineCancellationToken.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ private void Cancel()
_forcefulExitStarted = true;

// Start a new forceful exit timer
_ = Task.Delay(TimeSpan.FromSeconds(30), CancellationToken.None).ContinueWith(t =>
_ = Task.Delay(Defaults.ForcefulExitTimeout, CancellationToken.None).ContinueWith(t =>
{
if (!t.IsCanceled)
{
Expand All @@ -86,7 +86,7 @@ private void OnProcessExit(object? sender, EventArgs e)
// ProcessExit has limited time (~3s on Windows), so we can only wait briefly.
// Thread.Sleep is appropriate here: we're on a synchronous event handler thread
// and just need a simple delay — no need to involve the task scheduler.
Thread.Sleep(500);
Thread.Sleep(Defaults.ProcessExitHookDelay);
}
}

Expand Down
4 changes: 2 additions & 2 deletions TUnit.Core/Executors/DedicatedThreadExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -349,12 +349,12 @@ public override void Send(SendOrPostCallback d, object? state)
var waitTask = Task.Run(async () =>
{
// For .NET Standard 2.0 compatibility, use Task.Delay for timeout
var timeoutTask = Task.Delay(TimeSpan.FromMinutes(30));
var timeoutTask = Task.Delay(Defaults.TestTimeout);
var completedTask = await Task.WhenAny(tcs.Task, timeoutTask).ConfigureAwait(false);

if (completedTask == timeoutTask)
{
throw new TimeoutException("Synchronous operation on dedicated thread timed out after 30 minutes");
throw new TimeoutException($"Synchronous operation on dedicated thread timed out after {Defaults.TestTimeout.TotalMinutes} minutes");
}

// Await the actual task to get its result or exception
Expand Down
2 changes: 1 addition & 1 deletion TUnit.Core/Hooks/HookMethod.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public abstract record HookMethod
/// Gets the timeout for this hook method. This will be set during hook registration
/// by the event receiver infrastructure, falling back to the default 5-minute timeout.
/// </summary>
public TimeSpan? Timeout { get; internal set; } = TimeSpan.FromMinutes(5);
public TimeSpan? Timeout { get; internal set; } = Defaults.HookTimeout;

public required IHookExecutor HookExecutor { get; init; }

Expand Down
4 changes: 2 additions & 2 deletions TUnit.Engine/Building/TestBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1040,7 +1040,7 @@ private async ValueTask<TestContext> CreateTestContextAsync(string testId, TestM
AttributesByType = attributes.ToAttributeDictionary(),
MethodGenericArguments = testData.ResolvedMethodGenericArguments,
ClassGenericArguments = testData.ResolvedClassGenericArguments,
Timeout = TimeSpan.FromMinutes(30) // Default 30-minute timeout (can be overridden by TimeoutAttribute)
Timeout = Core.Defaults.TestTimeout // Default timeout (can be overridden by TimeoutAttribute)
// Don't set RetryLimit here - let discovery event receivers set it
};

Expand Down Expand Up @@ -1133,7 +1133,7 @@ private TestDetails CreateFailedTestDetails(TestMetadata metadata, string testId
ReturnType = typeof(Task),
MethodMetadata = metadata.MethodMetadata,
AttributesByType = AttributeDictionaryHelper.Empty,
Timeout = TimeSpan.FromMinutes(30)
Timeout = Core.Defaults.TestTimeout
};
}

Expand Down
8 changes: 4 additions & 4 deletions TUnit.Engine/Building/TestBuilderPipeline.cs
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ private async Task<AbstractExecutableTest[]> GenerateDynamicTests(TestMetadata m
ReturnType = typeof(Task),
MethodMetadata = metadata.MethodMetadata,
AttributesByType = attributes.ToAttributeDictionary(),
Timeout = TimeSpan.FromMinutes(30) // Default 30-minute timeout (can be overridden by TimeoutAttribute)
Timeout = Core.Defaults.TestTimeout // Default timeout (can be overridden by TimeoutAttribute)
// Don't set RetryLimit here - let discovery event receivers set it
};

Expand Down Expand Up @@ -391,7 +391,7 @@ private async IAsyncEnumerable<AbstractExecutableTest> BuildTestsFromSingleMetad
ReturnType = typeof(Task),
MethodMetadata = resolvedMetadata.MethodMetadata,
AttributesByType = attributes.ToAttributeDictionary(),
Timeout = TimeSpan.FromMinutes(30) // Default 30-minute timeout (can be overridden by TimeoutAttribute)
Timeout = Core.Defaults.TestTimeout // Default timeout (can be overridden by TimeoutAttribute)
// Don't set Timeout and RetryLimit here - let discovery event receivers set them
};

Expand Down Expand Up @@ -471,7 +471,7 @@ private AbstractExecutableTest CreateFailedTestForDataGenerationError(TestMetada
ReturnType = typeof(Task),
MethodMetadata = metadata.MethodMetadata,
AttributesByType = AttributeDictionaryHelper.Empty,
Timeout = TimeSpan.FromMinutes(30) // Default 30-minute timeout
Timeout = Core.Defaults.TestTimeout // Default timeout
};

var context = _contextProvider.CreateTestContext(
Expand Down Expand Up @@ -523,7 +523,7 @@ private AbstractExecutableTest CreateFailedTestForGenericResolutionError(TestMet
ReturnType = typeof(Task),
MethodMetadata = metadata.MethodMetadata,
AttributesByType = AttributeDictionaryHelper.Empty,
Timeout = TimeSpan.FromMinutes(30) // Default 30-minute timeout
Timeout = Core.Defaults.TestTimeout // Default timeout
};

var context = _contextProvider.CreateTestContext(
Expand Down
97 changes: 97 additions & 0 deletions TUnit.Engine/Constants/EngineDefaults.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
namespace TUnit.Engine.Constants;

/// <summary>
/// Default configuration values for the TUnit test engine.
/// Centralizes magic numbers so they can be tuned in a single place.
/// </summary>
internal static class EngineDefaults
{
// ── Discovery ───────────────────────────────────────────────────────

/// <summary>
/// Maximum time allowed for test discovery before the operation is cancelled.
/// </summary>
public static readonly TimeSpan DiscoveryTimeout = TimeSpan.FromMinutes(5);

/// <summary>
/// Maximum time allowed for test data generation in the discovery circuit breaker
/// before it trips.
/// </summary>
public static readonly TimeSpan MaxGenerationTime = TimeSpan.FromMinutes(2);

/// <summary>
/// Maximum proportion of available memory that test data generation may consume
/// before the discovery circuit breaker trips.
/// </summary>
public const double MaxMemoryPercentage = 0.7;

/// <summary>
/// Conservative fallback for total available memory when the runtime cannot determine
/// the actual value (e.g., on older .NET runtimes).
/// </summary>
public const long FallbackAvailableMemoryBytes = 1024L * 1024L * 1024L; // 1 GB

/// <summary>
/// Maximum number of retry passes when resolving test dependencies.
/// </summary>
public const int DependencyResolutionMaxRetries = 3;

// ── Event Batching ──────────────────────────────────────────────────

/// <summary>
/// Default number of events collected before a batch is flushed.
/// </summary>
public const int DefaultEventBatchSize = 100;

/// <summary>
/// Minimum batch delay used when the caller specifies <see cref="TimeSpan.Zero"/>.
/// Prevents a tight spin loop in the batching consumer.
/// </summary>
public static readonly TimeSpan MinBatchDelay = TimeSpan.FromMilliseconds(10);

/// <summary>
/// Maximum time to wait for the background processing task to complete
/// during dispose / shutdown.
/// </summary>
public static readonly TimeSpan ShutdownTimeout = TimeSpan.FromSeconds(5);

// ── Timeout Handling ────────────────────────────────────────────────

/// <summary>
/// Grace period given to a timed-out task to handle cancellation
/// before a <see cref="TimeoutException"/> is thrown.
/// </summary>
public static readonly TimeSpan TimeoutGracePeriod = TimeSpan.FromSeconds(1);

// ── IDE Streaming ───────────────────────────────────────────────────

/// <summary>
/// Interval at which cumulative test output snapshots are streamed to the IDE.
/// </summary>
public static readonly TimeSpan IdeStreamingThrottleInterval = TimeSpan.FromSeconds(1);

// ── File-Write Retry (Reporters) ────────────────────────────────────

/// <summary>
/// Maximum number of attempts when writing a report file that may be locked
/// by another process.
/// </summary>
public const int FileWriteMaxAttempts = 5;

/// <summary>
/// Base delay in milliseconds for exponential back-off when retrying a locked file write.
/// Actual delay = <c>BaseRetryDelayMs * 2^(attempt-1) + jitter</c>.
/// </summary>
public const int BaseRetryDelayMs = 50;

/// <summary>
/// Maximum random jitter in milliseconds added to retry delays to prevent thundering-herd effects.
/// </summary>
public const int MaxRetryJitterMs = 50;

/// <summary>
/// Maximum file size in bytes for the GitHub Step Summary file.
/// GitHub imposes a 1 MB limit on step summary files.
/// </summary>
public const long GitHubSummaryMaxFileSizeBytes = 1L * 1024L * 1024L; // 1 MB
}
13 changes: 7 additions & 6 deletions TUnit.Engine/Events/EventBatcher.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Threading.Channels;
using TUnit.Engine.Constants;

namespace TUnit.Engine.Events;

Expand All @@ -14,7 +15,7 @@ internal sealed class EventBatcher<TEvent> : IAsyncDisposable, IDisposable where

public EventBatcher(
Func<IReadOnlyList<TEvent>, ValueTask> batchProcessor,
int batchSize = 100,
int batchSize = EngineDefaults.DefaultEventBatchSize,
TimeSpan maxBatchDelay = default)
{
_batchProcessor = batchProcessor;
Expand All @@ -23,10 +24,10 @@ public EventBatcher(
SingleReader = true,
SingleWriter = false
});

_processingTask = ProcessBatchesAsync(
batchSize,
maxBatchDelay == TimeSpan.Zero ? TimeSpan.FromMilliseconds(10) : maxBatchDelay,
batchSize,
maxBatchDelay == TimeSpan.Zero ? EngineDefaults.MinBatchDelay : maxBatchDelay,
_shutdownCts.Token);
}

Expand Down Expand Up @@ -164,10 +165,10 @@ public async ValueTask DisposeAsync()
{
// Properly await the task with timeout
#if NET6_0_OR_GREATER
await _processingTask.WaitAsync(TimeSpan.FromSeconds(5), CancellationToken.None);
await _processingTask.WaitAsync(EngineDefaults.ShutdownTimeout, CancellationToken.None);
#else
// For .NET Framework, use Task.WhenAny to implement timeout
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
using var cts = new CancellationTokenSource(EngineDefaults.ShutdownTimeout);
var completedTask = await Task.WhenAny(_processingTask, Task.Delay(Timeout.Infinite, cts.Token)).ConfigureAwait(false);
if (completedTask == _processingTask)
{
Expand Down
4 changes: 3 additions & 1 deletion TUnit.Engine/Helpers/TimeoutHelper.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using TUnit.Engine.Constants;

namespace TUnit.Engine.Helpers;

/// <summary>
Expand All @@ -8,7 +10,7 @@ internal static class TimeoutHelper
/// <summary>
/// Grace period to allow tasks to handle cancellation before throwing timeout exception.
/// </summary>
private static readonly TimeSpan GracePeriod = TimeSpan.FromSeconds(1);
private static readonly TimeSpan GracePeriod = EngineDefaults.TimeoutGracePeriod;

/// <summary>
/// Executes a ValueTask-returning operation with an optional timeout.
Expand Down
3 changes: 2 additions & 1 deletion TUnit.Engine/Logging/IdeStreamingSink.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Microsoft.Testing.Platform.Extensions.Messages;
using TUnit.Core;
using TUnit.Core.Logging;
using TUnit.Engine.Constants;

#pragma warning disable TPEXP

Expand Down Expand Up @@ -33,7 +34,7 @@ internal sealed class IdeStreamingSink : ILogSink, IAsyncDisposable
{
private readonly TUnitMessageBus _messageBus;
private readonly ConcurrentDictionary<string, TestStreamingState> _activeTests = new();
private readonly TimeSpan _throttleInterval = TimeSpan.FromSeconds(1);
private readonly TimeSpan _throttleInterval = EngineDefaults.IdeStreamingThrottleInterval;

public IdeStreamingSink(TUnitMessageBus messageBus)
{
Expand Down
11 changes: 6 additions & 5 deletions TUnit.Engine/Reporters/GitHubReporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Microsoft.Testing.Platform.Extensions.Messages;
using Microsoft.Testing.Platform.Extensions.TestHost;
using TUnit.Engine.Configuration;
using TUnit.Engine.Constants;
using TUnit.Engine.Framework;

namespace TUnit.Engine.Reporters;
Expand All @@ -19,7 +20,7 @@ public enum GitHubReporterStyle

public class GitHubReporter(IExtension extension) : IDataConsumer, ITestHostApplicationLifetime, IFilterReceiver
{
private const long MaxFileSizeInBytes = 1 * 1024 * 1024; // 1MB
private const long MaxFileSizeInBytes = EngineDefaults.GitHubSummaryMaxFileSizeBytes;
private string _outputSummaryFilePath = null!;
private GitHubReporterStyle _reporterStyle = GitHubReporterStyle.Collapsible;

Expand Down Expand Up @@ -248,9 +249,9 @@ private async Task WriteFile(string contents)
return;
}

const int maxAttempts = 5;
const int maxAttempts = EngineDefaults.FileWriteMaxAttempts;
var random = new Random();

for (int attempt = 1; attempt <= maxAttempts; attempt++)
{
try
Expand All @@ -264,8 +265,8 @@ private async Task WriteFile(string contents)
}
catch (IOException ex) when (attempt < maxAttempts && IsFileLocked(ex))
{
var baseDelay = 50 * Math.Pow(2, attempt - 1);
var jitter = random.Next(0, 50);
var baseDelay = EngineDefaults.BaseRetryDelayMs * Math.Pow(2, attempt - 1);
var jitter = random.Next(0, EngineDefaults.MaxRetryJitterMs);
var delay = (int)(baseDelay + jitter);

Console.WriteLine($"GitHub Summary file is locked, retrying in {delay}ms (attempt {attempt}/{maxAttempts})");
Expand Down
7 changes: 4 additions & 3 deletions TUnit.Engine/Reporters/JUnitReporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Microsoft.Testing.Platform.Extensions.Messages;
using Microsoft.Testing.Platform.Extensions.TestHost;
using TUnit.Engine.Configuration;
using TUnit.Engine.Constants;
using TUnit.Engine.Framework;
using TUnit.Engine.Xml;

Expand Down Expand Up @@ -123,7 +124,7 @@ private static async Task WriteXmlFileAsync(string path, string content, Cancell
Directory.CreateDirectory(directory);
}

const int maxAttempts = 5;
const int maxAttempts = EngineDefaults.FileWriteMaxAttempts;

for (int attempt = 1; attempt <= maxAttempts; attempt++)
{
Expand All @@ -139,8 +140,8 @@ private static async Task WriteXmlFileAsync(string path, string content, Cancell
}
catch (IOException ex) when (attempt < maxAttempts && IsFileLocked(ex))
{
var baseDelay = 50 * Math.Pow(2, attempt - 1);
var jitter = Random.Shared.Next(0, 50);
var baseDelay = EngineDefaults.BaseRetryDelayMs * Math.Pow(2, attempt - 1);
var jitter = Random.Shared.Next(0, EngineDefaults.MaxRetryJitterMs);
var delay = (int)(baseDelay + jitter);

Console.WriteLine($"JUnit XML file is locked, retrying in {delay}ms (attempt {attempt}/{maxAttempts})");
Expand Down
9 changes: 5 additions & 4 deletions TUnit.Engine/Services/DiscoveryCircuitBreaker.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics;
using TUnit.Engine.Constants;

namespace TUnit.Engine.Services;

Expand All @@ -17,11 +18,11 @@ public sealed class DiscoveryCircuitBreaker
/// Creates a new discovery circuit breaker with intelligent limits
/// </summary>
public DiscoveryCircuitBreaker(
double maxMemoryPercentage = 0.7, // Use up to 70% of available memory
TimeSpan? maxGenerationTime = null) // Default 2 minutes
double maxMemoryPercentage = EngineDefaults.MaxMemoryPercentage,
TimeSpan? maxGenerationTime = null)
{
_maxMemoryBytes = (long)(GetAvailableMemoryBytes() * maxMemoryPercentage);
_maxGenerationTime = maxGenerationTime ?? TimeSpan.FromMinutes(2);
_maxGenerationTime = maxGenerationTime ?? EngineDefaults.MaxGenerationTime;
_stopwatch = Stopwatch.StartNew();

// Track initial memory to calculate growth
Expand Down Expand Up @@ -104,7 +105,7 @@ private static long GetAvailableMemoryBytes()
}

// Conservative fallback: assume 1GB available
return 1024L * 1024L * 1024L;
return EngineDefaults.FallbackAvailableMemoryBytes;
}

public void Dispose()
Expand Down
Loading
Loading