From 5838d75635b2d8ed2bd0eca61b7b325f49f27584 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Fri, 17 Oct 2025 16:36:42 +0200 Subject: [PATCH 1/6] fix: Don't create a static field in a generic class --- .../Commons/DefaultJsonSerializerOptions.cs | 14 +++ .../JsonIgnoreRuntimeResourceLabels.cs | 8 +- .../Commons/ResourceConfiguration.cs | 9 +- .../ReusableResourceTest.cs | 88 +++++++++++++------ 4 files changed, 84 insertions(+), 35 deletions(-) create mode 100644 src/Testcontainers/Configurations/Commons/DefaultJsonSerializerOptions.cs diff --git a/src/Testcontainers/Configurations/Commons/DefaultJsonSerializerOptions.cs b/src/Testcontainers/Configurations/Commons/DefaultJsonSerializerOptions.cs new file mode 100644 index 000000000..4c3747c38 --- /dev/null +++ b/src/Testcontainers/Configurations/Commons/DefaultJsonSerializerOptions.cs @@ -0,0 +1,14 @@ +namespace DotNet.Testcontainers.Configurations +{ + using System.Text.Json; + + public static class DefaultJsonSerializerOptions + { + static DefaultJsonSerializerOptions() + { + Instance.Converters.Add(new JsonOrderedKeysConverter()); + } + + public static JsonSerializerOptions Instance { get; } = new JsonSerializerOptions(); + } +} diff --git a/src/Testcontainers/Configurations/Commons/JsonIgnoreRuntimeResourceLabels.cs b/src/Testcontainers/Configurations/Commons/JsonIgnoreRuntimeResourceLabels.cs index aab61b0ce..8a87e9543 100644 --- a/src/Testcontainers/Configurations/Commons/JsonIgnoreRuntimeResourceLabels.cs +++ b/src/Testcontainers/Configurations/Commons/JsonIgnoreRuntimeResourceLabels.cs @@ -8,12 +8,16 @@ namespace DotNet.Testcontainers.Configurations internal sealed class JsonIgnoreRuntimeResourceLabels : JsonOrderedKeysConverter { - private static readonly ISet IgnoreLabels = new HashSet { ResourceReaper.ResourceReaperSessionLabel, TestcontainersClient.TestcontainersVersionLabel, TestcontainersClient.TestcontainersSessionIdLabel }; + private static readonly ISet IgnoreLabels = new HashSet + { + ResourceReaper.ResourceReaperSessionLabel, + TestcontainersClient.TestcontainersVersionLabel, + TestcontainersClient.TestcontainersSessionIdLabel, + }; public override void Write(Utf8JsonWriter writer, IReadOnlyDictionary value, JsonSerializerOptions options) { var labels = value.Where(label => !IgnoreLabels.Contains(label.Key)).ToDictionary(label => label.Key, label => label.Value); - base.Write(writer, labels, options); } } diff --git a/src/Testcontainers/Configurations/Commons/ResourceConfiguration.cs b/src/Testcontainers/Configurations/Commons/ResourceConfiguration.cs index 7b7f7e1f5..024b5e6ae 100644 --- a/src/Testcontainers/Configurations/Commons/ResourceConfiguration.cs +++ b/src/Testcontainers/Configurations/Commons/ResourceConfiguration.cs @@ -14,13 +14,6 @@ namespace DotNet.Testcontainers.Configurations [PublicAPI] public class ResourceConfiguration : IResourceConfiguration { - private static readonly JsonSerializerOptions JsonSerializerOptions; - - static ResourceConfiguration() - { - JsonSerializerOptions = new JsonSerializerOptions { Converters = { new JsonOrderedKeysConverter() } }; - } - /// /// Initializes a new instance of the class. /// @@ -95,7 +88,7 @@ protected ResourceConfiguration(IResourceConfiguration ol /// public virtual string GetReuseHash() { - var jsonUtf8Bytes = JsonSerializer.SerializeToUtf8Bytes(this, GetType(), JsonSerializerOptions); + var jsonUtf8Bytes = JsonSerializer.SerializeToUtf8Bytes(this, GetType(), DefaultJsonSerializerOptions.Instance); #if NET6_0_OR_GREATER return Convert.ToBase64String(SHA1.HashData(jsonUtf8Bytes)); diff --git a/tests/Testcontainers.Platform.Linux.Tests/ReusableResourceTest.cs b/tests/Testcontainers.Platform.Linux.Tests/ReusableResourceTest.cs index f0afa32b4..6c42c11a0 100644 --- a/tests/Testcontainers.Platform.Linux.Tests/ReusableResourceTest.cs +++ b/tests/Testcontainers.Platform.Linux.Tests/ReusableResourceTest.cs @@ -104,37 +104,75 @@ public sealed class EqualTest { [Fact] [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] - public void ForSameConfigurationCreatedInDifferentOrder() + public void ForKnownConfiguration() { - var env1 = new Dictionary - { - ["keyA"] = "valueA", - ["keyB"] = "valueB", - }; - var env2 = new Dictionary - { - ["keyB"] = "valueB", - ["keyA"] = "valueA", - }; - var hash1 = new ReuseHashContainerBuilder().WithEnvironment(env1).WithLabel("labelA", "A").WithLabel("labelB", "B").GetReuseHash(); - var hash2 = new ReuseHashContainerBuilder().WithEnvironment(env2).WithLabel("labelB", "B").WithLabel("labelA", "A").GetReuseHash(); - Assert.Equal(hash1, hash2); + // Given + var env = new Dictionary(); + env["keyA"] = "valueA"; + env["keyB"] = "valueB"; + + // When + var hash = new ReuseHashContainerBuilder() + .WithEnvironment(env) + .WithLabel("labelA", "A") + .WithLabel("labelB", "B") + .GetReuseHash(); + + // Then + + // `50MEP+vnxEkQFo5PrndJ7oKOfh8=` is the Base64-encoded SHA-1 hash of this JSON: + // + // { + // "Image": null, + // "Name": null, + // "Entrypoint": null, + // "Command": [], + // "Environments": { + // "keyA": "valueA", + // "keyB": "valueB" + // }, + // "ExposedPorts": {}, + // "PortBindings": {}, + // "NetworkAliases": [], + // "ExtraHosts": [], + // "Labels": { + // "labelA": "A", + // "labelB": "B", + // "org.testcontainers": "true", + // "org.testcontainers.lang": "dotnet" + // } + // } + Assert.Equal("50MEP+vnxEkQFo5PrndJ7oKOfh8=", hash); } [Fact] [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] - public void ForGivenConfiguration() + public void ForSameConfigurationInDifferentOrder() { - var env = new Dictionary - { - ["keyB"] = "valueB", - ["keyA"] = "valueA", - }; - var hash = new ReuseHashContainerBuilder().WithEnvironment(env).WithLabel("labelB", "B").WithLabel("labelA", "A").GetReuseHash(); - - // 50MEP+vnxEkQFo5PrndJ7oKOfh8= is the base64 encoded SHA1 of this JSON: - // {"Image":null,"Name":null,"Entrypoint":null,"Command":[],"Environments":{"keyA":"valueA","keyB":"valueB"},"ExposedPorts":{},"PortBindings":{},"NetworkAliases":[],"ExtraHosts":[],"Labels":{"labelA":"A","labelB":"B","org.testcontainers":"true","org.testcontainers.lang":"dotnet"}} - Assert.Equal("50MEP+vnxEkQFo5PrndJ7oKOfh8=", hash); + // Given + var env1 = new Dictionary(); + env1["keyA"] = "valueA"; + env1["keyB"] = "valueB"; + + var env2 = new Dictionary(); + env2["keyB"] = "valueB"; + env2["keyA"] = "valueA"; + + // When + var hash1 = new ReuseHashContainerBuilder() + .WithEnvironment(env1) + .WithLabel("labelA", "A") + .WithLabel("labelB", "B") + .GetReuseHash(); + + var hash2 = new ReuseHashContainerBuilder() + .WithEnvironment(env2) + .WithLabel("labelB", "B") + .WithLabel("labelA", "A") + .GetReuseHash(); + + // Then + Assert.Equal(hash1, hash2); } } From 6dec98e3e08670f4332868ca99b3fbca0c564cab Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Fri, 17 Oct 2025 17:02:23 +0200 Subject: [PATCH 2/6] Apply suggestion from @HofmeisterAn --- .../Configurations/Commons/DefaultJsonSerializerOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Testcontainers/Configurations/Commons/DefaultJsonSerializerOptions.cs b/src/Testcontainers/Configurations/Commons/DefaultJsonSerializerOptions.cs index 4c3747c38..ae83b6d68 100644 --- a/src/Testcontainers/Configurations/Commons/DefaultJsonSerializerOptions.cs +++ b/src/Testcontainers/Configurations/Commons/DefaultJsonSerializerOptions.cs @@ -2,7 +2,7 @@ namespace DotNet.Testcontainers.Configurations { using System.Text.Json; - public static class DefaultJsonSerializerOptions + internal static class DefaultJsonSerializerOptions { static DefaultJsonSerializerOptions() { From 4e907480129a3bf4a68e30504f538a3b538fa2c5 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Fri, 17 Oct 2025 17:43:26 +0200 Subject: [PATCH 3/6] chore: Add line break --- .../Configurations/Commons/DefaultJsonSerializerOptions.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Testcontainers/Configurations/Commons/DefaultJsonSerializerOptions.cs b/src/Testcontainers/Configurations/Commons/DefaultJsonSerializerOptions.cs index ae83b6d68..b89139092 100644 --- a/src/Testcontainers/Configurations/Commons/DefaultJsonSerializerOptions.cs +++ b/src/Testcontainers/Configurations/Commons/DefaultJsonSerializerOptions.cs @@ -9,6 +9,7 @@ static DefaultJsonSerializerOptions() Instance.Converters.Add(new JsonOrderedKeysConverter()); } - public static JsonSerializerOptions Instance { get; } = new JsonSerializerOptions(); + public static JsonSerializerOptions Instance { get; } + = new JsonSerializerOptions(); } } From a7bcb69689fb64c379d845cd9b49a11afcaa8457 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Fri, 17 Oct 2025 20:34:53 +0200 Subject: [PATCH 4/6] fix: Pin extra host value --- .../ReusableResourceTest.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/Testcontainers.Platform.Linux.Tests/ReusableResourceTest.cs b/tests/Testcontainers.Platform.Linux.Tests/ReusableResourceTest.cs index 6c42c11a0..831e29ab9 100644 --- a/tests/Testcontainers.Platform.Linux.Tests/ReusableResourceTest.cs +++ b/tests/Testcontainers.Platform.Linux.Tests/ReusableResourceTest.cs @@ -116,11 +116,17 @@ public void ForKnownConfiguration() .WithEnvironment(env) .WithLabel("labelA", "A") .WithLabel("labelB", "B") + // When the port forwarding container runs, Testcontainers automatically adds an + // extra host to the builder configuration. Since this extra host changes the + // hash value, we pin it to keep the hash consistent for assertions. + .WithExtraHost("host.testcontainers.internal", "127.0.0.1") .GetReuseHash(); // Then - // `50MEP+vnxEkQFo5PrndJ7oKOfh8=` is the Base64-encoded SHA-1 hash of this JSON: + // The hash is calculated from the minified JSON. For readability, the JSON + // shown below is formatted. `d0r7YnGzcm1QQyS0KIVT9DvcayA=` is the + // Base64-encoded SHA-1 hash for this JSON (minified): // // { // "Image": null, @@ -134,7 +140,9 @@ public void ForKnownConfiguration() // "ExposedPorts": {}, // "PortBindings": {}, // "NetworkAliases": [], - // "ExtraHosts": [], + // "ExtraHosts": [ + // "host.testcontainers.internal:127.0.0.1" + // ], // "Labels": { // "labelA": "A", // "labelB": "B", @@ -142,7 +150,7 @@ public void ForKnownConfiguration() // "org.testcontainers.lang": "dotnet" // } // } - Assert.Equal("50MEP+vnxEkQFo5PrndJ7oKOfh8=", hash); + Assert.Equal("d0r7YnGzcm1QQyS0KIVT9DvcayA=", hash); } [Fact] From aa603b3bca0f9d9f577804a9292cf286494a8fe7 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Sat, 18 Oct 2025 07:44:01 +0200 Subject: [PATCH 5/6] fix: Don't use default ctor Init --- .../ReusableResourceTest.cs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/tests/Testcontainers.Platform.Linux.Tests/ReusableResourceTest.cs b/tests/Testcontainers.Platform.Linux.Tests/ReusableResourceTest.cs index 831e29ab9..f3db0da7d 100644 --- a/tests/Testcontainers.Platform.Linux.Tests/ReusableResourceTest.cs +++ b/tests/Testcontainers.Platform.Linux.Tests/ReusableResourceTest.cs @@ -116,16 +116,12 @@ public void ForKnownConfiguration() .WithEnvironment(env) .WithLabel("labelA", "A") .WithLabel("labelB", "B") - // When the port forwarding container runs, Testcontainers automatically adds an - // extra host to the builder configuration. Since this extra host changes the - // hash value, we pin it to keep the hash consistent for assertions. - .WithExtraHost("host.testcontainers.internal", "127.0.0.1") .GetReuseHash(); // Then // The hash is calculated from the minified JSON. For readability, the JSON - // shown below is formatted. `d0r7YnGzcm1QQyS0KIVT9DvcayA=` is the + // shown below is formatted. `Dtj7Jx6NVlbDUnA3vmH1nNZw+o8=` is the // Base64-encoded SHA-1 hash for this JSON (minified): // // { @@ -140,9 +136,6 @@ public void ForKnownConfiguration() // "ExposedPorts": {}, // "PortBindings": {}, // "NetworkAliases": [], - // "ExtraHosts": [ - // "host.testcontainers.internal:127.0.0.1" - // ], // "Labels": { // "labelA": "A", // "labelB": "B", @@ -150,7 +143,7 @@ public void ForKnownConfiguration() // "org.testcontainers.lang": "dotnet" // } // } - Assert.Equal("d0r7YnGzcm1QQyS0KIVT9DvcayA=", hash); + Assert.Equal("Dtj7Jx6NVlbDUnA3vmH1nNZw+o8=", hash); } [Fact] @@ -272,7 +265,12 @@ public void EnabledCleanUpThrowsException() private sealed class ReuseHashContainerBuilder : ContainerBuilder { public ReuseHashContainerBuilder() : this(new ContainerConfiguration()) - => DockerResourceConfiguration = Init().DockerResourceConfiguration; + { + // By default, the constructor calls `Init()`, which sets up the default builder + // configurations, including ones for the port forwarding container if it's running. + // To avoid applying those settings during tests, this class intentionally doesn't + // call `Init()`. + } private ReuseHashContainerBuilder(ContainerConfiguration configuration) : base(configuration) => DockerResourceConfiguration = configuration; From 9cf35439612223b093f7da7a1463c6007179d5af Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Sat, 18 Oct 2025 11:49:10 +0200 Subject: [PATCH 6/6] chore: Simplify test --- .../WaitStrategyModeTest.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/Testcontainers.Platform.Linux.Tests/WaitStrategyModeTest.cs b/tests/Testcontainers.Platform.Linux.Tests/WaitStrategyModeTest.cs index ea1fdd2b3..9e65d1828 100644 --- a/tests/Testcontainers.Platform.Linux.Tests/WaitStrategyModeTest.cs +++ b/tests/Testcontainers.Platform.Linux.Tests/WaitStrategyModeTest.cs @@ -2,17 +2,13 @@ namespace Testcontainers.Tests; public abstract class WaitStrategyModeTest : IAsyncLifetime { - private const string Message = "Hello, World!"; - private readonly IContainer _container; private WaitStrategyModeTest(WaitStrategyMode waitStrategyMode) { _container = new ContainerBuilder() .WithImage(CommonImages.Alpine) - .WithEntrypoint("/bin/sh", "-c") - .WithCommand("echo " + Message) - .WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged(Message, o => o.WithMode(waitStrategyMode))) + .WithWaitStrategy(Wait.ForUnixContainer().AddCustomWaitStrategy(new WaitUntil(), o => o.WithMode(waitStrategyMode))) .Build(); } @@ -68,4 +64,12 @@ public async Task StartAsyncShouldSucceedWhenContainerIsNotRunning() Assert.Null(exception); } } + + private sealed class WaitUntil : IWaitUntil + { + public Task UntilAsync(IContainer container) + { + return Task.FromResult(TestcontainersStates.Exited.Equals(container.State)); + } + } } \ No newline at end of file