Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add custom configuration for retries, interval and timeout #1169

Merged
merged 4 commits into from
May 4, 2024
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
5 changes: 3 additions & 2 deletions docs/api/resource_reuse.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions docs/custom_configuration/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. | - |
<!-- | `wait.strategy.retries` | `TESTCONTAINERS_WAIT_STRATEGY_RETRIES` | The wait strategy retry count. | `infinite` |
| `wait.strategy.interval` | `TESTCONTAINERS_WAIT_STRATEGY_INTERVAL` | The wait strategy interval<sup>1</sup>. | `00:00:01` |
| `wait.strategy.timeout` | `TESTCONTAINERS_WAIT_STRATEGY_TIMEOUT` | The wait strategy timeout<sup>1</sup>. | `01:00:00` |

1) The value represent the string representation of a [TimeSpan](https://learn.microsoft.com/en-us/dotnet/api/system.timespan), for example, `00:00:01` for 1 second. -->

## Configure remote container runtime

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,5 +101,23 @@ public string GetHubImageNamePrefix()
{
return null;
}

/// <inheritdoc />
public ushort? GetWaitStrategyRetries()
{
return null;
}

/// <inheritdoc />
public TimeSpan? GetWaitStrategyInterval()
{
return null;
}

/// <inheritdoc />
public TimeSpan? GetWaitStrategyTimeout()
{
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,24 @@ public string GetHubImageNamePrefix()
return _customConfiguration.GetHubImageNamePrefix();
}

/// <inheritdoc />
public ushort? GetWaitStrategyRetries()
{
return _customConfiguration.GetWaitStrategyRetries();
}

/// <inheritdoc />
public TimeSpan? GetWaitStrategyInterval()
{
return _customConfiguration.GetWaitStrategyInterval();
}

/// <inheritdoc />
public TimeSpan? GetWaitStrategyTimeout()
{
return _customConfiguration.GetWaitStrategyTimeout();
}

private sealed class TestcontainersConfiguration : PropertiesFileConfiguration
{
public TestcontainersConfiguration()
Expand Down
24 changes: 23 additions & 1 deletion src/Testcontainers/Configurations/CustomConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,15 +102,37 @@ protected virtual string GetHubImageNamePrefix(string propertyName)
return GetPropertyValue<string>(propertyName);
}

protected virtual ushort? GetWaitStrategyRetries(string propertyName)
{
return GetPropertyValue<ushort?>(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<T>(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);
Expand Down
27 changes: 27 additions & 0 deletions src/Testcontainers/Configurations/EnvironmentConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
}
Expand All @@ -56,6 +62,9 @@ public EnvironmentConfiguration()
RyukContainerPrivileged,
RyukContainerImage,
HubImageNamePrefix,
WaitStrategyRetries,
WaitStrategyInterval,
WaitStrategyTimeout,
}
.ToDictionary(key => key, Environment.GetEnvironmentVariable))
{
Expand Down Expand Up @@ -138,5 +147,23 @@ public string GetHubImageNamePrefix()
{
return GetHubImageNamePrefix(HubImageNamePrefix);
}

/// <inheritdoc />
public ushort? GetWaitStrategyRetries()
{
return GetWaitStrategyRetries(WaitStrategyRetries);
}

/// <inheritdoc />
public TimeSpan? GetWaitStrategyInterval()
{
return GetWaitStrategyInterval(WaitStrategyInterval);
}

/// <inheritdoc />
public TimeSpan? GetWaitStrategyTimeout()
{
return GetWaitStrategyTimeout(WaitStrategyTimeout);
}
}
}
24 changes: 24 additions & 0 deletions src/Testcontainers/Configurations/ICustomConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,5 +101,29 @@ internal interface ICustomConfiguration
/// <remarks>https://dotnet.testcontainers.org/custom_configuration/.</remarks>
[CanBeNull]
string GetHubImageNamePrefix();

/// <summary>
/// Gets the wait strategy retries custom configuration.
/// </summary>
/// <returns>The wait strategy retries custom configuration.</returns>
/// <remarks>https://dotnet.testcontainers.org/custom_configuration/.</remarks>
[CanBeNull]
ushort? GetWaitStrategyRetries();

/// <summary>
/// Gets the wait strategy interval custom configuration.
/// </summary>
/// <returns>The wait strategy interval custom configuration.</returns>
/// <remarks>https://dotnet.testcontainers.org/custom_configuration/.</remarks>
[CanBeNull]
TimeSpan? GetWaitStrategyInterval();

/// <summary>
/// Gets the wait strategy timeout custom configuration.
/// </summary>
/// <returns>The wait strategy timeout custom configuration.</returns>
/// <remarks>https://dotnet.testcontainers.org/custom_configuration/.</remarks>
[CanBeNull]
TimeSpan? GetWaitStrategyTimeout();
}
}
21 changes: 21 additions & 0 deletions src/Testcontainers/Configurations/PropertiesFileConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,5 +139,26 @@ public string GetHubImageNamePrefix()
const string propertyName = "hub.image.name.prefix";
return GetHubImageNamePrefix(propertyName);
}

/// <inheritdoc />
public ushort? GetWaitStrategyRetries()
{
const string propertyName = "wait.strategy.retries";
return GetWaitStrategyRetries(propertyName);
}

/// <inheritdoc />
public TimeSpan? GetWaitStrategyInterval()
{
const string propertyName = "wait.strategy.interval";
return GetWaitStrategyInterval(propertyName);
}

/// <inheritdoc />
public TimeSpan? GetWaitStrategyTimeout()
{
const string propertyName = "wait.strategy.timeout";
return GetWaitStrategyTimeout(propertyName);
}
}
}
36 changes: 36 additions & 0 deletions src/Testcontainers/Configurations/TestcontainersSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,42 @@ static TestcontainersSettings()
public static string HubImageNamePrefix { get; set; }
= EnvironmentConfiguration.Instance.GetHubImageNamePrefix() ?? PropertiesFileConfiguration.Instance.GetHubImageNamePrefix();

/// <summary>
/// Gets or sets the wait strategy retry count.
/// </summary>
/// <remarks>
/// 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/.
/// </remarks>
[CanBeNull]
public static ushort? WaitStrategyRetries { get; set; }
= EnvironmentConfiguration.Instance.GetWaitStrategyRetries() ?? PropertiesFileConfiguration.Instance.GetWaitStrategyRetries();

/// <summary>
/// Gets or sets the wait strategy interval.
/// </summary>
/// <remarks>
/// 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/.
/// </remarks>
[CanBeNull]
public static TimeSpan? WaitStrategyInterval { get; set; }
= EnvironmentConfiguration.Instance.GetWaitStrategyInterval() ?? PropertiesFileConfiguration.Instance.GetWaitStrategyInterval();

/// <summary>
/// Gets or sets the wait strategy timeout.
/// </summary>
/// <remarks>
/// 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/.
/// </remarks>
[CanBeNull]
public static TimeSpan? WaitStrategyTimeout { get; set; }
= EnvironmentConfiguration.Instance.GetWaitStrategyTimeout() ?? PropertiesFileConfiguration.Instance.GetWaitStrategyTimeout();

/// <summary>
/// Gets or sets the host operating system.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
{
}
}
}
20 changes: 10 additions & 10 deletions src/Testcontainers/Configurations/WaitStrategies/WaitStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,20 +44,20 @@ public WaitStrategy(IWaitUntil waitUntil)
/// <summary>
/// Gets the number of retries.
/// </summary>
public int Retries { get; private set; }
= -1;
public ushort Retries { get; private set; }
= TestcontainersSettings.WaitStrategyRetries ?? 0;

/// <summary>
/// Gets the interval between retries.
/// </summary>
public TimeSpan Interval { get; private set; }
= TimeSpan.FromSeconds(1);
= TestcontainersSettings.WaitStrategyInterval ?? TimeSpan.FromSeconds(1);

/// <summary>
/// Gets the timeout.
/// </summary>
public TimeSpan Timeout { get; private set; }
= TimeSpan.FromHours(1);
= TestcontainersSettings.WaitStrategyTimeout ?? TimeSpan.FromHours(1);

/// <inheritdoc />
public IWaitStrategy WithRetries(ushort retries)
Expand Down Expand Up @@ -114,10 +114,10 @@ public virtual Task<bool> UntilAsync(IContainer container, CancellationToken ct
/// <param name="retries">The number of retries to run for the condition to become false.</param>
/// <param name="ct">The optional cancellation token to cancel the waiting operation.</param>
/// <exception cref="TimeoutException">Thrown when the timeout expires.</exception>
/// <exception cref="ArgumentException">Thrown when the number of retries is exceeded.</exception>
/// <exception cref="RetryLimitExceededException">Thrown when the number of retries is exceeded.</exception>
/// <returns>A task that represents the asynchronous block operation.</returns>
[PublicAPI]
public static async Task WaitWhileAsync(Func<Task<bool>> wait, TimeSpan interval, TimeSpan timeout, int retries = -1, CancellationToken ct = default)
public static async Task WaitWhileAsync(Func<Task<bool>> wait, TimeSpan interval, TimeSpan timeout, int retries = 0, CancellationToken ct = default)
{
ushort actualRetries = 0;

Expand All @@ -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);
Expand Down Expand Up @@ -170,10 +170,10 @@ await waitTask
/// <param name="retries">The number of retries to run for the condition to become true.</param>
/// <param name="ct">The optional cancellation token to cancel the waiting operation.</param>
/// <exception cref="TimeoutException">Thrown when the timeout expires.</exception>
/// <exception cref="ArgumentException">Thrown when the number of retries is exceeded.</exception>
/// <exception cref="RetryLimitExceededException">Thrown when the number of retries is exceeded.</exception>
/// <returns>A task that represents the asynchronous block operation.</returns>
[PublicAPI]
public static async Task WaitUntilAsync(Func<Task<bool>> wait, TimeSpan interval, TimeSpan timeout, int retries = -1, CancellationToken ct = default)
public static async Task WaitUntilAsync(Func<Task<bool>> wait, TimeSpan interval, TimeSpan timeout, int retries = 0, CancellationToken ct = default)
{
ushort actualRetries = 0;

Expand All @@ -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);
Expand Down
45 changes: 45 additions & 0 deletions tests/Testcontainers.Platform.Linux.Tests/WaitStrategyTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
namespace Testcontainers.Tests;

public sealed class WaitStrategyTest
{
[Fact]
public Task WithTimeout()
{
return Assert.ThrowsAsync<TimeoutException>(() => 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<RetryLimitExceededException>(() => 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<bool> UntilAsync(IContainer container)
{
return Task.FromResult(false);
}
}
}
Loading
Loading