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()); + } } } }