Skip to content

Commit 444958d

Browse files
authored
feat: Allow devs to override the reuse hash calculation (#1688)
1 parent c9db4a9 commit 444958d

9 files changed

Lines changed: 79 additions & 7 deletions

File tree

Directory.Packages.props

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
<PackageVersion Include="Confluent.SchemaRegistry.Serdes.Json" Version="2.8.0"/>
5353
<PackageVersion Include="Confluent.SchemaRegistry" Version="2.8.0"/>
5454
<PackageVersion Include="Consul" Version="1.6.10.9"/>
55-
<PackageVersion Include="CouchbaseNetClient" Version="3.7.2"/>
55+
<PackageVersion Include="CouchbaseNetClient" Version="3.9.2"/>
5656
<PackageVersion Include="DotPulsar" Version="3.6.0"/>
5757
<PackageVersion Include="Elastic.Clients.Elasticsearch" Version="8.19.15"/>
5858
<PackageVersion Include="EventStore.Client.Grpc.Streams" Version="23.3.9"/>
@@ -66,13 +66,13 @@
6666
<PackageVersion Include="JanusGraph.Net" Version="1.1.0"/>
6767
<PackageVersion Include="Keycloak.Net.Core" Version="1.0.20"/>
6868
<PackageVersion Include="KubernetesClient" Version="17.0.14"/>
69-
<PackageVersion Include="KurrentDB.Client" Version="1.2.0"/>
69+
<PackageVersion Include="KurrentDB.Client" Version="1.4.0"/>
7070
<PackageVersion Include="Microsoft.Azure.Cosmos" Version="3.32.1"/>
7171
<PackageVersion Include="Microsoft.Azure.Kusto.Data" Version="12.2.8"/>
7272
<PackageVersion Include="Microsoft.Data.SqlClient" Version="5.2.2"/>
7373
<PackageVersion Include="Microsoft.Playwright" Version="1.55.0"/>
7474
<PackageVersion Include="Milvus.Client" Version="2.3.0-preview.1"/>
75-
<PackageVersion Include="MongoDB.Driver" Version="3.2.0"/>
75+
<PackageVersion Include="MongoDB.Driver" Version="3.8.0"/>
7676
<PackageVersion Include="MQTTnet" Version="5.0.1.1416"/>
7777
<PackageVersion Include="MyCouch" Version="7.6.0"/>
7878
<PackageVersion Include="MySqlConnector" Version="2.2.5"/>

docs/api/resource_reuse.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,17 @@ _ = new ContainerBuilder("alpine:3.20.0")
1515
.WithLabel("reuse-id", "WeatherForecast");
1616
```
1717

18+
To take full control over the hash value, pass a custom function to the `WithReuse` overload. The function receives the resource configuration and returns the hash string used to identify and match the resource.
19+
20+
```csharp title="Override the reuse hash calculation"
21+
_ = new ContainerBuilder("alpine:3.20.0")
22+
.WithReuse(_ => "custom-hash");
23+
```
24+
25+
!!! warning
26+
27+
A matching custom hash does not guarantee that the resource is compatible. The default implementation derives the hash from the builder configuration, so a match implies an identical configuration. With a custom hash, it is the caller's responsibility to ensure that the hash uniquely and accurately reflects the intended configuration.
28+
1829
The current implementation considers the following resource configurations and their corresponding builder APIs when calculating the hash value.
1930

2031
!!! note

src/Testcontainers.WebDriver/WebDriverContainer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ static FFmpegContainer()
114114
/// Initializes a new instance of the <see cref="FFmpegContainer" /> class.
115115
/// </summary>
116116
private FFmpegContainer()
117-
: base(new ContainerConfiguration(new ResourceConfiguration<CreateContainerParameters>(new DockerEndpointAuthenticationConfiguration(new Uri("tcp://ffmpeg")), null, null, false, NullLogger.Instance)))
117+
: base(new ContainerConfiguration(new ResourceConfiguration<CreateContainerParameters>(new DockerEndpointAuthenticationConfiguration(new Uri("tcp://ffmpeg")), null, null, false, null, NullLogger.Instance)))
118118
{
119119
}
120120

src/Testcontainers/Builders/AbstractBuilder`4.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,13 @@ public TBuilderEntity WithReuse(bool reuse)
6565
return Clone(new ResourceConfiguration<TCreateResourceEntity>(reuse: reuse)).WithCleanUp(!reuse);
6666
}
6767

68+
/// <inheritdoc />
69+
public TBuilderEntity WithReuse(Func<IResourceConfiguration<TCreateResourceEntity>, string> reuseHashProvider)
70+
{
71+
var reuse = reuseHashProvider != null;
72+
return Clone(new ResourceConfiguration<TCreateResourceEntity>(reuse: reuse, reuseHashProvider: reuseHashProvider)).WithCleanUp(!reuse);
73+
}
74+
6875
/// <inheritdoc />
6976
public TBuilderEntity WithLabel(string name, string value)
7077
{

src/Testcontainers/Builders/IAbstractBuilder`3.cs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ namespace DotNet.Testcontainers.Builders
1414
/// <typeparam name="TResourceEntity">The resource entity.</typeparam>
1515
/// <typeparam name="TCreateResourceEntity">The underlying Docker.DotNet resource entity.</typeparam>
1616
[PublicAPI]
17-
public interface IAbstractBuilder<out TBuilderEntity, out TResourceEntity, out TCreateResourceEntity>
17+
public interface IAbstractBuilder<out TBuilderEntity, out TResourceEntity, TCreateResourceEntity>
1818
{
1919
/// <summary>
2020
/// Sets the Docker API endpoint.
@@ -80,6 +80,26 @@ public interface IAbstractBuilder<out TBuilderEntity, out TResourceEntity, out T
8080
[PublicAPI]
8181
TBuilderEntity WithReuse(bool reuse);
8282

83+
/// <summary>
84+
/// Reuses an existing Docker resource with a custom reuse hash.
85+
/// </summary>
86+
/// <remarks>
87+
/// If reuse is enabled, Testcontainers will label the resource with a hash value
88+
/// according to the respective build/resource configuration. When Testcontainers finds a
89+
/// matching resource, it will reuse this resource instead of creating a new one. Enabling
90+
/// reuse will disable the resource reaper, meaning the resource will not be cleaned up
91+
/// after the tests are finished.
92+
///
93+
/// This is an <b>experimental</b> feature. Reuse does not take all builder
94+
/// configurations into account when calculating the hash value. There might be configurations
95+
/// where Testcontainers is not, or not yet, able to find a matching resource and
96+
/// recreate the resource.
97+
/// </remarks>
98+
/// <param name="reuseHashProvider">A function that receives the resource configuration and returns the reuse hash, replacing the default SHA-1-based implementation.</param>
99+
/// <returns>A configured instance of <typeparamref name="TBuilderEntity" />.</returns>
100+
[PublicAPI]
101+
TBuilderEntity WithReuse(Func<IResourceConfiguration<TCreateResourceEntity>, string> reuseHashProvider);
102+
83103
/// <summary>
84104
/// Adds user-defined metadata to the Docker resource.
85105
/// </summary>

src/Testcontainers/Configurations/Commons/IResourceConfiguration.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace DotNet.Testcontainers.Configurations
1010
/// </summary>
1111
/// <typeparam name="TCreateResourceEntity">The underlying Docker.DotNet resource entity.</typeparam>
1212
[PublicAPI]
13-
public interface IResourceConfiguration<in TCreateResourceEntity>
13+
public interface IResourceConfiguration<TCreateResourceEntity>
1414
{
1515
/// <summary>
1616
/// Gets the test session id.
@@ -37,6 +37,11 @@ public interface IResourceConfiguration<in TCreateResourceEntity>
3737
/// </summary>
3838
bool? Reuse { get; }
3939

40+
/// <summary>
41+
/// Gets a function to override the reuse hash calculation, or <c>null</c> to use the default SHA-1-based implementation.
42+
/// </summary>
43+
Func<IResourceConfiguration<TCreateResourceEntity>, string> ReuseHashProvider { get; }
44+
4045
/// <summary>
4146
/// Gets the logger.
4247
/// </summary>

src/Testcontainers/Configurations/Commons/ResourceConfiguration.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,22 @@ public class ResourceConfiguration<TCreateResourceEntity> : IResourceConfigurati
2121
/// <param name="labels">The test session id.</param>
2222
/// <param name="parameterModifiers">A list of low level modifications of the Docker.DotNet entity.</param>
2323
/// <param name="reuse">A value indicating whether to reuse an existing resource configuration or not.</param>
24+
/// <param name="reuseHashProvider">A function to override the reuse hash calculation, or <c>null</c> to use the default SHA-1-based implementation.</param>
2425
/// <param name="logger">The logger.</param>
2526
public ResourceConfiguration(
2627
IDockerEndpointAuthenticationConfiguration dockerEndpointAuthenticationConfiguration = null,
2728
IReadOnlyDictionary<string, string> labels = null,
2829
IReadOnlyList<Action<TCreateResourceEntity>> parameterModifiers = null,
2930
bool? reuse = null,
31+
Func<IResourceConfiguration<TCreateResourceEntity>, string> reuseHashProvider = null,
3032
ILogger logger = null)
3133
{
3234
SessionId = labels != null && labels.TryGetValue(ResourceReaper.ResourceReaperSessionLabel, out var resourceReaperSessionId) && Guid.TryParseExact(resourceReaperSessionId, "D", out var sessionId) ? sessionId : Guid.Empty;
3335
DockerEndpointAuthConfig = dockerEndpointAuthenticationConfiguration;
3436
Labels = labels;
3537
ParameterModifiers = parameterModifiers;
3638
Reuse = reuse;
39+
ReuseHashProvider = reuseHashProvider;
3740
Logger = logger;
3841
}
3942

@@ -57,6 +60,7 @@ protected ResourceConfiguration(IResourceConfiguration<TCreateResourceEntity> ol
5760
labels: BuildConfiguration.Combine(oldValue.Labels, newValue.Labels),
5861
parameterModifiers: BuildConfiguration.Combine(oldValue.ParameterModifiers, newValue.ParameterModifiers),
5962
reuse: (oldValue.Reuse.HasValue && oldValue.Reuse.Value) || (newValue.Reuse.HasValue && newValue.Reuse.Value),
63+
reuseHashProvider: BuildConfiguration.Combine(oldValue.ReuseHashProvider, newValue.ReuseHashProvider),
6064
logger: BuildConfiguration.Combine(oldValue.Logger, newValue.Logger))
6165
{
6266
}
@@ -81,13 +85,22 @@ protected ResourceConfiguration(IResourceConfiguration<TCreateResourceEntity> ol
8185
[JsonIgnore]
8286
public bool? Reuse { get; }
8387

88+
/// <inheritdoc />
89+
[JsonIgnore]
90+
public Func<IResourceConfiguration<TCreateResourceEntity>, string> ReuseHashProvider { get; }
91+
8492
/// <inheritdoc />
8593
[JsonIgnore]
8694
public ILogger Logger { get; }
8795

8896
/// <inheritdoc />
8997
public virtual string GetReuseHash()
9098
{
99+
if (ReuseHashProvider != null)
100+
{
101+
return ReuseHashProvider(this);
102+
}
103+
91104
var jsonUtf8Bytes = JsonSerializer.SerializeToUtf8Bytes(this, GetType(), DefaultJsonSerializerOptions.Instance);
92105

93106
#if NET6_0_OR_GREATER

tests/Testcontainers.Couchbase.Tests/CouchbaseContainerTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public async Task GetBucketReturnsDefaultBucket()
2525
clusterOptions.UserName = CouchbaseBuilder.DefaultUsername;
2626
clusterOptions.Password = CouchbaseBuilder.DefaultPassword;
2727

28-
var cluster = await Cluster.ConnectAsync(clusterOptions)
28+
var cluster = await Cluster.ConnectAsync(clusterOptions, TestContext.Current.CancellationToken)
2929
.ConfigureAwait(true);
3030

3131
// When

tests/Testcontainers.Platform.Linux.Tests/ReusableResourceTest.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,22 @@ public void ForSameConfigurationInDifferentOrder()
172172
// Then
173173
Assert.Equal(hash1, hash2);
174174
}
175+
176+
[Fact]
177+
[Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
178+
public void ForCustomReuseHashProvider()
179+
{
180+
// Given
181+
const string customHash = "custom-hash";
182+
183+
// When
184+
var hash = new ReuseHashContainerBuilder()
185+
.WithReuse(_ => customHash)
186+
.GetReuseHash();
187+
188+
// Then
189+
Assert.Equal(customHash, hash);
190+
}
175191
}
176192

177193
public sealed class NotEqualTest

0 commit comments

Comments
 (0)