diff --git a/docs/api/resource_reuse.md b/docs/api/resource_reuse.md
index f99f4c06b..2165add78 100644
--- a/docs/api/resource_reuse.md
+++ b/docs/api/resource_reuse.md
@@ -17,8 +17,9 @@ _ = new ContainerBuilder()
The current implementation considers the following resource configurations and their corresponding builder APIs when calculating the hash value.
-> [!NOTE]
-> Version 3.8.0 did not include the container configuration's name in the hash value.
+!!!note
+
+ Version 3.8.0 did not include the container configuration's name in the hash value.
- [ContainerConfiguration](https://github.com/testcontainers/testcontainers-dotnet/blob/develop/src/Testcontainers/Configurations/Containers/ContainerConfiguration.cs)
- Image
diff --git a/docs/custom_configuration/index.md b/docs/custom_configuration/index.md
index 81eae7bb7..5c3f60634 100644
--- a/docs/custom_configuration/index.md
+++ b/docs/custom_configuration/index.md
@@ -16,6 +16,11 @@ Testcontainers supports various configurations to set up your test environment.
| `ryuk.container.privileged` | `TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED` | Runs Ryuk (resource reaper) in privileged mode. | `false` |
| `ryuk.container.image` | `TESTCONTAINERS_RYUK_CONTAINER_IMAGE` | The Ryuk (resource reaper) Docker image. | `testcontainers/ryuk:0.5.1` |
| `hub.image.name.prefix` | `TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX` | The name to use for substituting the Docker Hub registry part of the image name. | - |
+
## Configure remote container runtime
diff --git a/src/Testcontainers/Builders/DockerDesktopEndpointAuthenticationProvider.cs b/src/Testcontainers/Builders/DockerDesktopEndpointAuthenticationProvider.cs
index 362558579..5ec60e648 100644
--- a/src/Testcontainers/Builders/DockerDesktopEndpointAuthenticationProvider.cs
+++ b/src/Testcontainers/Builders/DockerDesktopEndpointAuthenticationProvider.cs
@@ -101,5 +101,23 @@ public string GetHubImageNamePrefix()
{
return null;
}
+
+ ///
+ public ushort? GetWaitStrategyRetries()
+ {
+ return null;
+ }
+
+ ///
+ public TimeSpan? GetWaitStrategyInterval()
+ {
+ return null;
+ }
+
+ ///
+ public TimeSpan? GetWaitStrategyTimeout()
+ {
+ return null;
+ }
}
}
diff --git a/src/Testcontainers/Builders/TestcontainersEndpointAuthenticationProvider.cs b/src/Testcontainers/Builders/TestcontainersEndpointAuthenticationProvider.cs
index e3078055c..a043d2e4e 100644
--- a/src/Testcontainers/Builders/TestcontainersEndpointAuthenticationProvider.cs
+++ b/src/Testcontainers/Builders/TestcontainersEndpointAuthenticationProvider.cs
@@ -129,6 +129,24 @@ public string GetHubImageNamePrefix()
return _customConfiguration.GetHubImageNamePrefix();
}
+ ///
+ public ushort? GetWaitStrategyRetries()
+ {
+ return _customConfiguration.GetWaitStrategyRetries();
+ }
+
+ ///
+ public TimeSpan? GetWaitStrategyInterval()
+ {
+ return _customConfiguration.GetWaitStrategyInterval();
+ }
+
+ ///
+ public TimeSpan? GetWaitStrategyTimeout()
+ {
+ return _customConfiguration.GetWaitStrategyTimeout();
+ }
+
private sealed class TestcontainersConfiguration : PropertiesFileConfiguration
{
public TestcontainersConfiguration()
diff --git a/src/Testcontainers/Configurations/CustomConfiguration.cs b/src/Testcontainers/Configurations/CustomConfiguration.cs
index 155e2dbe2..e5a45b361 100644
--- a/src/Testcontainers/Configurations/CustomConfiguration.cs
+++ b/src/Testcontainers/Configurations/CustomConfiguration.cs
@@ -102,15 +102,37 @@ protected virtual string GetHubImageNamePrefix(string propertyName)
return GetPropertyValue(propertyName);
}
+ protected virtual ushort? GetWaitStrategyRetries(string propertyName)
+ {
+ return GetPropertyValue(propertyName);
+ }
+
+ protected virtual TimeSpan? GetWaitStrategyInterval(string propertyName)
+ {
+ return _properties.TryGetValue(propertyName, out var propertyValue) && TimeSpan.TryParse(propertyValue, out var result) && result > TimeSpan.Zero ? result : (TimeSpan?)null;
+ }
+
+ protected virtual TimeSpan? GetWaitStrategyTimeout(string propertyName)
+ {
+ return _properties.TryGetValue(propertyName, out var propertyValue) && TimeSpan.TryParse(propertyValue, out var result) && result > TimeSpan.Zero ? result : (TimeSpan?)null;
+ }
+
private T GetPropertyValue(string propertyName)
{
- switch (Type.GetTypeCode(typeof(T)))
+ var type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
+
+ switch (Type.GetTypeCode(type))
{
case TypeCode.Boolean:
{
return (T)(object)(_properties.TryGetValue(propertyName, out var propertyValue) && ("1".Equals(propertyValue, StringComparison.Ordinal) || (bool.TryParse(propertyValue, out var result) && result)));
}
+ case TypeCode.UInt16:
+ {
+ return (T)(object)(_properties.TryGetValue(propertyName, out var propertyValue) && ushort.TryParse(propertyValue, out var result) ? result : (ushort?)null);
+ }
+
case TypeCode.String:
{
_ = _properties.TryGetValue(propertyName, out var propertyValue);
diff --git a/src/Testcontainers/Configurations/EnvironmentConfiguration.cs b/src/Testcontainers/Configurations/EnvironmentConfiguration.cs
index 471cfc9c8..ebc2dbcad 100644
--- a/src/Testcontainers/Configurations/EnvironmentConfiguration.cs
+++ b/src/Testcontainers/Configurations/EnvironmentConfiguration.cs
@@ -34,6 +34,12 @@ internal class EnvironmentConfiguration : CustomConfiguration, ICustomConfigurat
private const string HubImageNamePrefix = "TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX";
+ private const string WaitStrategyRetries = "TESTCONTAINERS_WAIT_STRATEGY_RETRIES";
+
+ private const string WaitStrategyInterval = "TESTCONTAINERS_WAIT_STRATEGY_INTERVAL";
+
+ private const string WaitStrategyTimeout = "TESTCONTAINERS_WAIT_STRATEGY_TIMEOUT";
+
static EnvironmentConfiguration()
{
}
@@ -56,6 +62,9 @@ public EnvironmentConfiguration()
RyukContainerPrivileged,
RyukContainerImage,
HubImageNamePrefix,
+ WaitStrategyRetries,
+ WaitStrategyInterval,
+ WaitStrategyTimeout,
}
.ToDictionary(key => key, Environment.GetEnvironmentVariable))
{
@@ -138,5 +147,23 @@ public string GetHubImageNamePrefix()
{
return GetHubImageNamePrefix(HubImageNamePrefix);
}
+
+ ///
+ public ushort? GetWaitStrategyRetries()
+ {
+ return GetWaitStrategyRetries(WaitStrategyRetries);
+ }
+
+ ///
+ public TimeSpan? GetWaitStrategyInterval()
+ {
+ return GetWaitStrategyInterval(WaitStrategyInterval);
+ }
+
+ ///
+ public TimeSpan? GetWaitStrategyTimeout()
+ {
+ return GetWaitStrategyTimeout(WaitStrategyTimeout);
+ }
}
}
diff --git a/src/Testcontainers/Configurations/ICustomConfiguration.cs b/src/Testcontainers/Configurations/ICustomConfiguration.cs
index 4a9c31ecd..4cbd51285 100644
--- a/src/Testcontainers/Configurations/ICustomConfiguration.cs
+++ b/src/Testcontainers/Configurations/ICustomConfiguration.cs
@@ -101,5 +101,29 @@ internal interface ICustomConfiguration
/// https://dotnet.testcontainers.org/custom_configuration/.
[CanBeNull]
string GetHubImageNamePrefix();
+
+ ///
+ /// Gets the wait strategy retries custom configuration.
+ ///
+ /// The wait strategy retries custom configuration.
+ /// https://dotnet.testcontainers.org/custom_configuration/.
+ [CanBeNull]
+ ushort? GetWaitStrategyRetries();
+
+ ///
+ /// Gets the wait strategy interval custom configuration.
+ ///
+ /// The wait strategy interval custom configuration.
+ /// https://dotnet.testcontainers.org/custom_configuration/.
+ [CanBeNull]
+ TimeSpan? GetWaitStrategyInterval();
+
+ ///
+ /// Gets the wait strategy timeout custom configuration.
+ ///
+ /// The wait strategy timeout custom configuration.
+ /// https://dotnet.testcontainers.org/custom_configuration/.
+ [CanBeNull]
+ TimeSpan? GetWaitStrategyTimeout();
}
}
diff --git a/src/Testcontainers/Configurations/PropertiesFileConfiguration.cs b/src/Testcontainers/Configurations/PropertiesFileConfiguration.cs
index 6d0367c1f..86d1dc67c 100644
--- a/src/Testcontainers/Configurations/PropertiesFileConfiguration.cs
+++ b/src/Testcontainers/Configurations/PropertiesFileConfiguration.cs
@@ -139,5 +139,26 @@ public string GetHubImageNamePrefix()
const string propertyName = "hub.image.name.prefix";
return GetHubImageNamePrefix(propertyName);
}
+
+ ///
+ public ushort? GetWaitStrategyRetries()
+ {
+ const string propertyName = "wait.strategy.retries";
+ return GetWaitStrategyRetries(propertyName);
+ }
+
+ ///
+ public TimeSpan? GetWaitStrategyInterval()
+ {
+ const string propertyName = "wait.strategy.interval";
+ return GetWaitStrategyInterval(propertyName);
+ }
+
+ ///
+ public TimeSpan? GetWaitStrategyTimeout()
+ {
+ const string propertyName = "wait.strategy.timeout";
+ return GetWaitStrategyTimeout(propertyName);
+ }
}
}
diff --git a/src/Testcontainers/Configurations/TestcontainersSettings.cs b/src/Testcontainers/Configurations/TestcontainersSettings.cs
index 59dbc57d1..c2b5146b8 100644
--- a/src/Testcontainers/Configurations/TestcontainersSettings.cs
+++ b/src/Testcontainers/Configurations/TestcontainersSettings.cs
@@ -100,6 +100,42 @@ static TestcontainersSettings()
public static string HubImageNamePrefix { get; set; }
= EnvironmentConfiguration.Instance.GetHubImageNamePrefix() ?? PropertiesFileConfiguration.Instance.GetHubImageNamePrefix();
+ ///
+ /// Gets or sets the wait strategy retry count.
+ ///
+ ///
+ /// This property represents the default value and applies to all wait strategies.
+ /// Wait strategies can be configured individually using the wait strategy option callback:
+ /// https://dotnet.testcontainers.org/api/wait_strategies/.
+ ///
+ [CanBeNull]
+ public static ushort? WaitStrategyRetries { get; set; }
+ = EnvironmentConfiguration.Instance.GetWaitStrategyRetries() ?? PropertiesFileConfiguration.Instance.GetWaitStrategyRetries();
+
+ ///
+ /// Gets or sets the wait strategy interval.
+ ///
+ ///
+ /// This property represents the default value and applies to all wait strategies.
+ /// Wait strategies can be configured individually using the wait strategy option callback:
+ /// https://dotnet.testcontainers.org/api/wait_strategies/.
+ ///
+ [CanBeNull]
+ public static TimeSpan? WaitStrategyInterval { get; set; }
+ = EnvironmentConfiguration.Instance.GetWaitStrategyInterval() ?? PropertiesFileConfiguration.Instance.GetWaitStrategyInterval();
+
+ ///
+ /// Gets or sets the wait strategy timeout.
+ ///
+ ///
+ /// This property represents the default value and applies to all wait strategies.
+ /// Wait strategies can be configured individually using the wait strategy option callback:
+ /// https://dotnet.testcontainers.org/api/wait_strategies/.
+ ///
+ [CanBeNull]
+ public static TimeSpan? WaitStrategyTimeout { get; set; }
+ = EnvironmentConfiguration.Instance.GetWaitStrategyTimeout() ?? PropertiesFileConfiguration.Instance.GetWaitStrategyTimeout();
+
///
/// Gets or sets the host operating system.
///
diff --git a/src/Testcontainers/Configurations/WaitStrategies/RetryLimitExceededException.cs b/src/Testcontainers/Configurations/WaitStrategies/RetryLimitExceededException.cs
new file mode 100644
index 000000000..17b3621c0
--- /dev/null
+++ b/src/Testcontainers/Configurations/WaitStrategies/RetryLimitExceededException.cs
@@ -0,0 +1,21 @@
+namespace DotNet.Testcontainers.Configurations
+{
+ using System;
+
+ public sealed class RetryLimitExceededException : Exception
+ {
+ public RetryLimitExceededException()
+ {
+ }
+
+ public RetryLimitExceededException(string message)
+ : base(message)
+ {
+ }
+
+ public RetryLimitExceededException(string message, Exception inner)
+ : base(message, inner)
+ {
+ }
+ }
+}
diff --git a/src/Testcontainers/Configurations/WaitStrategies/WaitStrategy.cs b/src/Testcontainers/Configurations/WaitStrategies/WaitStrategy.cs
index 75466647d..48c74d36f 100644
--- a/src/Testcontainers/Configurations/WaitStrategies/WaitStrategy.cs
+++ b/src/Testcontainers/Configurations/WaitStrategies/WaitStrategy.cs
@@ -44,20 +44,20 @@ public WaitStrategy(IWaitUntil waitUntil)
///
/// Gets the number of retries.
///
- public int Retries { get; private set; }
- = -1;
+ public ushort Retries { get; private set; }
+ = TestcontainersSettings.WaitStrategyRetries ?? 0;
///
/// Gets the interval between retries.
///
public TimeSpan Interval { get; private set; }
- = TimeSpan.FromSeconds(1);
+ = TestcontainersSettings.WaitStrategyInterval ?? TimeSpan.FromSeconds(1);
///
/// Gets the timeout.
///
public TimeSpan Timeout { get; private set; }
- = TimeSpan.FromHours(1);
+ = TestcontainersSettings.WaitStrategyTimeout ?? TimeSpan.FromHours(1);
///
public IWaitStrategy WithRetries(ushort retries)
@@ -114,10 +114,10 @@ public virtual Task UntilAsync(IContainer container, CancellationToken ct
/// The number of retries to run for the condition to become false.
/// The optional cancellation token to cancel the waiting operation.
/// Thrown when the timeout expires.
- /// Thrown when the number of retries is exceeded.
+ /// Thrown when the number of retries is exceeded.
/// A task that represents the asynchronous block operation.
[PublicAPI]
- public static async Task WaitWhileAsync(Func> wait, TimeSpan interval, TimeSpan timeout, int retries = -1, CancellationToken ct = default)
+ public static async Task WaitWhileAsync(Func> wait, TimeSpan interval, TimeSpan timeout, int retries = 0, CancellationToken ct = default)
{
ushort actualRetries = 0;
@@ -134,7 +134,7 @@ async Task WhileAsync()
}
_ = Guard.Argument(retries, nameof(retries))
- .ThrowIf(_ => retries > 0 && ++actualRetries > retries, _ => throw new ArgumentException(MaximumRetryExceededException));
+ .ThrowIf(_ => retries > 0 && ++actualRetries > retries, _ => throw new RetryLimitExceededException(MaximumRetryExceededException));
await Task.Delay(interval, ct)
.ConfigureAwait(false);
@@ -170,10 +170,10 @@ await waitTask
/// The number of retries to run for the condition to become true.
/// The optional cancellation token to cancel the waiting operation.
/// Thrown when the timeout expires.
- /// Thrown when the number of retries is exceeded.
+ /// Thrown when the number of retries is exceeded.
/// A task that represents the asynchronous block operation.
[PublicAPI]
- public static async Task WaitUntilAsync(Func> wait, TimeSpan interval, TimeSpan timeout, int retries = -1, CancellationToken ct = default)
+ public static async Task WaitUntilAsync(Func> wait, TimeSpan interval, TimeSpan timeout, int retries = 0, CancellationToken ct = default)
{
ushort actualRetries = 0;
@@ -190,7 +190,7 @@ async Task UntilAsync()
}
_ = Guard.Argument(retries, nameof(retries))
- .ThrowIf(_ => retries > 0 && ++actualRetries > retries, _ => throw new ArgumentException(MaximumRetryExceededException));
+ .ThrowIf(_ => retries > 0 && ++actualRetries > retries, _ => throw new RetryLimitExceededException(MaximumRetryExceededException));
await Task.Delay(interval, ct)
.ConfigureAwait(false);
diff --git a/tests/Testcontainers.Platform.Linux.Tests/WaitStrategyTest.cs b/tests/Testcontainers.Platform.Linux.Tests/WaitStrategyTest.cs
new file mode 100644
index 000000000..0bcddbb26
--- /dev/null
+++ b/tests/Testcontainers.Platform.Linux.Tests/WaitStrategyTest.cs
@@ -0,0 +1,45 @@
+namespace Testcontainers.Tests;
+
+public sealed class WaitStrategyTest
+{
+ [Fact]
+ public Task WithTimeout()
+ {
+ return Assert.ThrowsAsync(() => new ContainerBuilder()
+ .WithImage(CommonImages.Alpine)
+ .WithEntrypoint(CommonCommands.SleepInfinity)
+ .WithWaitStrategy(Wait.ForUnixContainer().AddCustomWaitStrategy(FailingWaitStrategy.Instance, o => o.WithTimeout(TimeSpan.FromSeconds(1))))
+ .Build()
+ .StartAsync());
+ }
+
+ [Fact]
+ public Task WithRetries()
+ {
+ return Assert.ThrowsAsync(() => new ContainerBuilder()
+ .WithImage(CommonImages.Alpine)
+ .WithEntrypoint(CommonCommands.SleepInfinity)
+ .WithWaitStrategy(Wait.ForUnixContainer().AddCustomWaitStrategy(FailingWaitStrategy.Instance, o => o.WithRetries(1)))
+ .Build()
+ .StartAsync());
+ }
+
+ private sealed class FailingWaitStrategy : IWaitUntil
+ {
+ static FailingWaitStrategy()
+ {
+ }
+
+ private FailingWaitStrategy()
+ {
+ }
+
+ public static IWaitUntil Instance { get; }
+ = new FailingWaitStrategy();
+
+ public Task UntilAsync(IContainer container)
+ {
+ return Task.FromResult(false);
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Testcontainers.Tests/Unit/Configurations/CustomConfigurationTest.cs b/tests/Testcontainers.Tests/Unit/Configurations/CustomConfigurationTest.cs
index c30508a30..72c6ecbd9 100644
--- a/tests/Testcontainers.Tests/Unit/Configurations/CustomConfigurationTest.cs
+++ b/tests/Testcontainers.Tests/Unit/Configurations/CustomConfigurationTest.cs
@@ -27,6 +27,9 @@ static EnvironmentConfigurationTest()
EnvironmentVariables.Add("TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED");
EnvironmentVariables.Add("TESTCONTAINERS_RYUK_CONTAINER_IMAGE");
EnvironmentVariables.Add("TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX");
+ EnvironmentVariables.Add("TESTCONTAINERS_WAIT_STRATEGY_RETRIES");
+ EnvironmentVariables.Add("TESTCONTAINERS_WAIT_STRATEGY_INTERVAL");
+ EnvironmentVariables.Add("TESTCONTAINERS_WAIT_STRATEGY_TIMEOUT");
}
[Theory]
@@ -177,6 +180,41 @@ public void GetHubImageNamePrefixCustomConfiguration(string propertyName, string
Assert.Equal(expected, customConfiguration.GetHubImageNamePrefix());
}
+ [Theory]
+ [InlineData("", "", null)]
+ [InlineData("TESTCONTAINERS_WAIT_STRATEGY_RETRIES", "", null)]
+ [InlineData("TESTCONTAINERS_WAIT_STRATEGY_RETRIES", "1", 1)]
+ public void GetWaitStrategyRetriesCustomConfiguration(string propertyName, string propertyValue, int? expected)
+ {
+ SetEnvironmentVariable(propertyName, propertyValue);
+ ICustomConfiguration customConfiguration = new EnvironmentConfiguration();
+ Assert.Equal(expected, customConfiguration.GetWaitStrategyRetries());
+ }
+
+ [Theory]
+ [InlineData("", "", null)]
+ [InlineData("TESTCONTAINERS_WAIT_STRATEGY_INTERVAL", "", null)]
+ [InlineData("TESTCONTAINERS_WAIT_STRATEGY_INTERVAL", "-00:00:00.001", null)]
+ [InlineData("TESTCONTAINERS_WAIT_STRATEGY_INTERVAL", "00:00:01", "00:00:01")]
+ public void GetWaitStrategyIntervalCustomConfiguration(string propertyName, string propertyValue, string expected)
+ {
+ SetEnvironmentVariable(propertyName, propertyValue);
+ ICustomConfiguration customConfiguration = new EnvironmentConfiguration();
+ Assert.Equal(expected, customConfiguration.GetWaitStrategyInterval()?.ToString());
+ }
+
+ [Theory]
+ [InlineData("", "", null)]
+ [InlineData("TESTCONTAINERS_WAIT_STRATEGY_TIMEOUT", "", null)]
+ [InlineData("TESTCONTAINERS_WAIT_STRATEGY_TIMEOUT", "-00:00:00.001", null)]
+ [InlineData("TESTCONTAINERS_WAIT_STRATEGY_TIMEOUT", "00:00:01", "00:00:01")]
+ public void GetWaitStrategyTimeoutCustomConfiguration(string propertyName, string propertyValue, string expected)
+ {
+ SetEnvironmentVariable(propertyName, propertyValue);
+ ICustomConfiguration customConfiguration = new EnvironmentConfiguration();
+ Assert.Equal(expected, customConfiguration.GetWaitStrategyTimeout()?.ToString());
+ }
+
public void Dispose()
{
foreach (var propertyName in EnvironmentVariables)
@@ -331,6 +369,38 @@ public void GetHubImageNamePrefixCustomConfiguration(string configuration, strin
ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { configuration });
Assert.Equal(expected, customConfiguration.GetHubImageNamePrefix());
}
+
+ [Theory]
+ [InlineData("", null)]
+ [InlineData("wait.strategy.retries=", null)]
+ [InlineData("wait.strategy.retries=1", 1)]
+ public void GetWaitStrategyRetriesCustomConfiguration(string configuration, int? expected)
+ {
+ ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { configuration });
+ Assert.Equal(expected, customConfiguration.GetWaitStrategyRetries());
+ }
+
+ [Theory]
+ [InlineData("", null)]
+ [InlineData("wait.strategy.interval=", null)]
+ [InlineData("wait.strategy.interval=-00:00:00.001", null)]
+ [InlineData("wait.strategy.interval=00:00:01", "00:00:01")]
+ public void GetWaitStrategyIntervalCustomConfiguration(string configuration, string expected)
+ {
+ ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { configuration });
+ Assert.Equal(expected, customConfiguration.GetWaitStrategyInterval()?.ToString());
+ }
+
+ [Theory]
+ [InlineData("", null)]
+ [InlineData("wait.strategy.timeout=", null)]
+ [InlineData("wait.strategy.timeout=-00:00:00.001", null)]
+ [InlineData("wait.strategy.timeout=00:00:01", "00:00:01")]
+ public void GetWaitStrategyTimeoutCustomConfiguration(string configuration, string expected)
+ {
+ ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { configuration });
+ Assert.Equal(expected, customConfiguration.GetWaitStrategyTimeout()?.ToString());
+ }
}
}
}