diff --git a/TUnit.Core/Defaults.cs b/TUnit.Core/Defaults.cs
new file mode 100644
index 0000000000..893376f766
--- /dev/null
+++ b/TUnit.Core/Defaults.cs
@@ -0,0 +1,32 @@
+namespace TUnit.Core;
+
+///
+/// Default values shared across TUnit.Core and TUnit.Engine.
+/// Centralizes magic numbers so they can be tuned in a single place.
+///
+public static class Defaults
+{
+ ///
+ /// Default timeout applied to individual tests when no [Timeout] attribute is specified.
+ /// Can be overridden per-test via .
+ ///
+ public static readonly TimeSpan TestTimeout = TimeSpan.FromMinutes(30);
+
+ ///
+ /// Default timeout applied to hook methods (Before/After at every level)
+ /// when no explicit timeout is configured.
+ ///
+ public static readonly TimeSpan HookTimeout = TimeSpan.FromMinutes(5);
+
+ ///
+ /// Time allowed for a graceful shutdown after a cancellation request (Ctrl+C / SIGTERM)
+ /// before the process is forcefully terminated.
+ ///
+ public static readonly TimeSpan ForcefulExitTimeout = TimeSpan.FromSeconds(30);
+
+ ///
+ /// Brief delay during process exit to allow After hooks registered via
+ /// to execute before the process terminates.
+ ///
+ public static readonly TimeSpan ProcessExitHookDelay = TimeSpan.FromMilliseconds(500);
+}
diff --git a/TUnit.Core/EngineCancellationToken.cs b/TUnit.Core/EngineCancellationToken.cs
index 7cdb43edad..4236d62080 100644
--- a/TUnit.Core/EngineCancellationToken.cs
+++ b/TUnit.Core/EngineCancellationToken.cs
@@ -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)
{
@@ -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);
}
}
diff --git a/TUnit.Core/Executors/DedicatedThreadExecutor.cs b/TUnit.Core/Executors/DedicatedThreadExecutor.cs
index 96b0fe1246..d12c6f80c5 100644
--- a/TUnit.Core/Executors/DedicatedThreadExecutor.cs
+++ b/TUnit.Core/Executors/DedicatedThreadExecutor.cs
@@ -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
diff --git a/TUnit.Core/Hooks/HookMethod.cs b/TUnit.Core/Hooks/HookMethod.cs
index ab73dc6236..9de33ba695 100644
--- a/TUnit.Core/Hooks/HookMethod.cs
+++ b/TUnit.Core/Hooks/HookMethod.cs
@@ -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.
///
- public TimeSpan? Timeout { get; internal set; } = TimeSpan.FromMinutes(5);
+ public TimeSpan? Timeout { get; internal set; } = Defaults.HookTimeout;
public required IHookExecutor HookExecutor { get; init; }
diff --git a/TUnit.Engine/Building/TestBuilder.cs b/TUnit.Engine/Building/TestBuilder.cs
index 6d308f8d8f..d34cb77a91 100644
--- a/TUnit.Engine/Building/TestBuilder.cs
+++ b/TUnit.Engine/Building/TestBuilder.cs
@@ -1040,7 +1040,7 @@ private async ValueTask 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
};
@@ -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
};
}
diff --git a/TUnit.Engine/Building/TestBuilderPipeline.cs b/TUnit.Engine/Building/TestBuilderPipeline.cs
index 2e0b78536a..879384ed8d 100644
--- a/TUnit.Engine/Building/TestBuilderPipeline.cs
+++ b/TUnit.Engine/Building/TestBuilderPipeline.cs
@@ -263,7 +263,7 @@ private async Task 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
};
@@ -391,7 +391,7 @@ private async IAsyncEnumerable 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
};
@@ -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(
@@ -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(
diff --git a/TUnit.Engine/Constants/EngineDefaults.cs b/TUnit.Engine/Constants/EngineDefaults.cs
new file mode 100644
index 0000000000..25ab46670b
--- /dev/null
+++ b/TUnit.Engine/Constants/EngineDefaults.cs
@@ -0,0 +1,97 @@
+namespace TUnit.Engine.Constants;
+
+///
+/// Default configuration values for the TUnit test engine.
+/// Centralizes magic numbers so they can be tuned in a single place.
+///
+internal static class EngineDefaults
+{
+ // ── Discovery ───────────────────────────────────────────────────────
+
+ ///
+ /// Maximum time allowed for test discovery before the operation is cancelled.
+ ///
+ public static readonly TimeSpan DiscoveryTimeout = TimeSpan.FromMinutes(5);
+
+ ///
+ /// Maximum time allowed for test data generation in the discovery circuit breaker
+ /// before it trips.
+ ///
+ public static readonly TimeSpan MaxGenerationTime = TimeSpan.FromMinutes(2);
+
+ ///
+ /// Maximum proportion of available memory that test data generation may consume
+ /// before the discovery circuit breaker trips.
+ ///
+ public const double MaxMemoryPercentage = 0.7;
+
+ ///
+ /// Conservative fallback for total available memory when the runtime cannot determine
+ /// the actual value (e.g., on older .NET runtimes).
+ ///
+ public const long FallbackAvailableMemoryBytes = 1024L * 1024L * 1024L; // 1 GB
+
+ ///
+ /// Maximum number of retry passes when resolving test dependencies.
+ ///
+ public const int DependencyResolutionMaxRetries = 3;
+
+ // ── Event Batching ──────────────────────────────────────────────────
+
+ ///
+ /// Default number of events collected before a batch is flushed.
+ ///
+ public const int DefaultEventBatchSize = 100;
+
+ ///
+ /// Minimum batch delay used when the caller specifies .
+ /// Prevents a tight spin loop in the batching consumer.
+ ///
+ public static readonly TimeSpan MinBatchDelay = TimeSpan.FromMilliseconds(10);
+
+ ///
+ /// Maximum time to wait for the background processing task to complete
+ /// during dispose / shutdown.
+ ///
+ public static readonly TimeSpan ShutdownTimeout = TimeSpan.FromSeconds(5);
+
+ // ── Timeout Handling ────────────────────────────────────────────────
+
+ ///
+ /// Grace period given to a timed-out task to handle cancellation
+ /// before a is thrown.
+ ///
+ public static readonly TimeSpan TimeoutGracePeriod = TimeSpan.FromSeconds(1);
+
+ // ── IDE Streaming ───────────────────────────────────────────────────
+
+ ///
+ /// Interval at which cumulative test output snapshots are streamed to the IDE.
+ ///
+ public static readonly TimeSpan IdeStreamingThrottleInterval = TimeSpan.FromSeconds(1);
+
+ // ── File-Write Retry (Reporters) ────────────────────────────────────
+
+ ///
+ /// Maximum number of attempts when writing a report file that may be locked
+ /// by another process.
+ ///
+ public const int FileWriteMaxAttempts = 5;
+
+ ///
+ /// Base delay in milliseconds for exponential back-off when retrying a locked file write.
+ /// Actual delay = BaseRetryDelayMs * 2^(attempt-1) + jitter.
+ ///
+ public const int BaseRetryDelayMs = 50;
+
+ ///
+ /// Maximum random jitter in milliseconds added to retry delays to prevent thundering-herd effects.
+ ///
+ public const int MaxRetryJitterMs = 50;
+
+ ///
+ /// Maximum file size in bytes for the GitHub Step Summary file.
+ /// GitHub imposes a 1 MB limit on step summary files.
+ ///
+ public const long GitHubSummaryMaxFileSizeBytes = 1L * 1024L * 1024L; // 1 MB
+}
diff --git a/TUnit.Engine/Events/EventBatcher.cs b/TUnit.Engine/Events/EventBatcher.cs
index fe48107938..e1a35cbf9e 100644
--- a/TUnit.Engine/Events/EventBatcher.cs
+++ b/TUnit.Engine/Events/EventBatcher.cs
@@ -1,4 +1,5 @@
using System.Threading.Channels;
+using TUnit.Engine.Constants;
namespace TUnit.Engine.Events;
@@ -14,7 +15,7 @@ internal sealed class EventBatcher : IAsyncDisposable, IDisposable where
public EventBatcher(
Func, ValueTask> batchProcessor,
- int batchSize = 100,
+ int batchSize = EngineDefaults.DefaultEventBatchSize,
TimeSpan maxBatchDelay = default)
{
_batchProcessor = batchProcessor;
@@ -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);
}
@@ -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)
{
diff --git a/TUnit.Engine/Helpers/TimeoutHelper.cs b/TUnit.Engine/Helpers/TimeoutHelper.cs
index 2e21204d9f..81ed952d31 100644
--- a/TUnit.Engine/Helpers/TimeoutHelper.cs
+++ b/TUnit.Engine/Helpers/TimeoutHelper.cs
@@ -1,3 +1,5 @@
+using TUnit.Engine.Constants;
+
namespace TUnit.Engine.Helpers;
///
@@ -8,7 +10,7 @@ internal static class TimeoutHelper
///
/// Grace period to allow tasks to handle cancellation before throwing timeout exception.
///
- private static readonly TimeSpan GracePeriod = TimeSpan.FromSeconds(1);
+ private static readonly TimeSpan GracePeriod = EngineDefaults.TimeoutGracePeriod;
///
/// Executes a ValueTask-returning operation with an optional timeout.
diff --git a/TUnit.Engine/Logging/IdeStreamingSink.cs b/TUnit.Engine/Logging/IdeStreamingSink.cs
index fc729dab10..0d2e82d6cf 100644
--- a/TUnit.Engine/Logging/IdeStreamingSink.cs
+++ b/TUnit.Engine/Logging/IdeStreamingSink.cs
@@ -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
@@ -33,7 +34,7 @@ internal sealed class IdeStreamingSink : ILogSink, IAsyncDisposable
{
private readonly TUnitMessageBus _messageBus;
private readonly ConcurrentDictionary _activeTests = new();
- private readonly TimeSpan _throttleInterval = TimeSpan.FromSeconds(1);
+ private readonly TimeSpan _throttleInterval = EngineDefaults.IdeStreamingThrottleInterval;
public IdeStreamingSink(TUnitMessageBus messageBus)
{
diff --git a/TUnit.Engine/Reporters/GitHubReporter.cs b/TUnit.Engine/Reporters/GitHubReporter.cs
index eedd2dbc6e..e49c7f9d68 100644
--- a/TUnit.Engine/Reporters/GitHubReporter.cs
+++ b/TUnit.Engine/Reporters/GitHubReporter.cs
@@ -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;
@@ -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;
@@ -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
@@ -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})");
diff --git a/TUnit.Engine/Reporters/JUnitReporter.cs b/TUnit.Engine/Reporters/JUnitReporter.cs
index 070b1d4c12..0fda21bca1 100644
--- a/TUnit.Engine/Reporters/JUnitReporter.cs
+++ b/TUnit.Engine/Reporters/JUnitReporter.cs
@@ -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;
@@ -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++)
{
@@ -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})");
diff --git a/TUnit.Engine/Services/DiscoveryCircuitBreaker.cs b/TUnit.Engine/Services/DiscoveryCircuitBreaker.cs
index 67f30787b8..133e9c4a84 100644
--- a/TUnit.Engine/Services/DiscoveryCircuitBreaker.cs
+++ b/TUnit.Engine/Services/DiscoveryCircuitBreaker.cs
@@ -1,4 +1,5 @@
using System.Diagnostics;
+using TUnit.Engine.Constants;
namespace TUnit.Engine.Services;
@@ -17,11 +18,11 @@ public sealed class DiscoveryCircuitBreaker
/// Creates a new discovery circuit breaker with intelligent limits
///
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
@@ -104,7 +105,7 @@ private static long GetAvailableMemoryBytes()
}
// Conservative fallback: assume 1GB available
- return 1024L * 1024L * 1024L;
+ return EngineDefaults.FallbackAvailableMemoryBytes;
}
public void Dispose()
diff --git a/TUnit.Engine/Services/TestDependencyResolver.cs b/TUnit.Engine/Services/TestDependencyResolver.cs
index a8f477affe..e08476a887 100644
--- a/TUnit.Engine/Services/TestDependencyResolver.cs
+++ b/TUnit.Engine/Services/TestDependencyResolver.cs
@@ -1,5 +1,6 @@
using TUnit.Core;
using TUnit.Core.Helpers;
+using TUnit.Engine.Constants;
namespace TUnit.Engine.Services;
@@ -272,7 +273,7 @@ public void ResolveAllDependencies()
}
}
- var maxRetries = 3;
+ var maxRetries = EngineDefaults.DependencyResolutionMaxRetries;
for (var retry = 0; retry < maxRetries && _testsWithPendingDependencies.Count > 0; retry++)
{
ResolvePendingDependencies();
diff --git a/TUnit.Engine/TestDiscoveryService.cs b/TUnit.Engine/TestDiscoveryService.cs
index 14d4f3aff3..0d7121abe4 100644
--- a/TUnit.Engine/TestDiscoveryService.cs
+++ b/TUnit.Engine/TestDiscoveryService.cs
@@ -6,6 +6,7 @@
using TUnit.Core;
using TUnit.Engine.Building;
using TUnit.Engine.Building.Interfaces;
+using TUnit.Engine.Constants;
using TUnit.Engine.Services;
namespace TUnit.Engine;
@@ -83,7 +84,7 @@ public async Task DiscoverTests(string testSessionId, ITest
// Build tests directly from the pre-collected metadata (avoid re-collecting)
// Apply 5-minute discovery timeout matching the streaming path (#4715)
using var filterCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
- filterCts.CancelAfter(TimeSpan.FromMinutes(5));
+ filterCts.CancelAfter(EngineDefaults.DiscoveryTimeout);
var buildingContext = new Building.TestBuildingContext(isForExecution, Filter: null);
var tests = await _testBuilderPipeline.BuildTestsFromMetadataAsync(
@@ -160,7 +161,7 @@ private async IAsyncEnumerable DiscoverTestsStreamAsync(
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
- cts.CancelAfter(TimeSpan.FromMinutes(5));
+ cts.CancelAfter(EngineDefaults.DiscoveryTimeout);
var tests = await _testBuilderPipeline.BuildTestsStreamingAsync(testSessionId, buildingContext, metadataFilter: null, cts.Token).ConfigureAwait(false);