diff --git a/src/Testcontainers/Configurations/Commons/JsonIgnoreRuntimeResourceLabels.cs b/src/Testcontainers/Configurations/Commons/JsonIgnoreRuntimeResourceLabels.cs index 14badf1c1..aab61b0ce 100644 --- a/src/Testcontainers/Configurations/Commons/JsonIgnoreRuntimeResourceLabels.cs +++ b/src/Testcontainers/Configurations/Commons/JsonIgnoreRuntimeResourceLabels.cs @@ -1,39 +1,20 @@ namespace DotNet.Testcontainers.Configurations { - using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; - using System.Text.Json.Serialization; using DotNet.Testcontainers.Clients; using DotNet.Testcontainers.Containers; - internal sealed class JsonIgnoreRuntimeResourceLabels : JsonConverter> + internal sealed class JsonIgnoreRuntimeResourceLabels : JsonOrderedKeysConverter { private static readonly ISet IgnoreLabels = new HashSet { ResourceReaper.ResourceReaperSessionLabel, TestcontainersClient.TestcontainersVersionLabel, TestcontainersClient.TestcontainersSessionIdLabel }; - public override bool CanConvert(Type typeToConvert) - { - return typeof(IEnumerable>).IsAssignableFrom(typeToConvert); - } - - public override IReadOnlyDictionary Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return JsonSerializer.Deserialize>(ref reader); - } - 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); - writer.WriteStartObject(); - - foreach (var label in labels) - { - writer.WriteString(label.Key, label.Value); - } - - writer.WriteEndObject(); + base.Write(writer, labels, options); } } } diff --git a/src/Testcontainers/Configurations/Commons/JsonOrderedKeysConverter.cs b/src/Testcontainers/Configurations/Commons/JsonOrderedKeysConverter.cs new file mode 100644 index 000000000..5893dd69a --- /dev/null +++ b/src/Testcontainers/Configurations/Commons/JsonOrderedKeysConverter.cs @@ -0,0 +1,33 @@ +namespace DotNet.Testcontainers.Configurations +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text.Json; + using System.Text.Json.Serialization; + + internal class JsonOrderedKeysConverter : JsonConverter> + { + public override bool CanConvert(Type typeToConvert) + { + return typeof(IEnumerable>).IsAssignableFrom(typeToConvert); + } + + public override IReadOnlyDictionary Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return JsonSerializer.Deserialize>(ref reader); + } + + public override void Write(Utf8JsonWriter writer, IReadOnlyDictionary value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + + foreach (var item in value.OrderBy(item => item.Key)) + { + writer.WriteString(item.Key, item.Value); + } + + writer.WriteEndObject(); + } + } +} diff --git a/src/Testcontainers/Configurations/Commons/ResourceConfiguration.cs b/src/Testcontainers/Configurations/Commons/ResourceConfiguration.cs index 10ebbd52d..7b7f7e1f5 100644 --- a/src/Testcontainers/Configurations/Commons/ResourceConfiguration.cs +++ b/src/Testcontainers/Configurations/Commons/ResourceConfiguration.cs @@ -14,6 +14,13 @@ 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. /// @@ -88,7 +95,7 @@ protected ResourceConfiguration(IResourceConfiguration ol /// public virtual string GetReuseHash() { - var jsonUtf8Bytes = JsonSerializer.SerializeToUtf8Bytes(this, GetType()); + var jsonUtf8Bytes = JsonSerializer.SerializeToUtf8Bytes(this, GetType(), JsonSerializerOptions); #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 457761ba5..f0afa32b4 100644 --- a/tests/Testcontainers.Platform.Linux.Tests/ReusableResourceTest.cs +++ b/tests/Testcontainers.Platform.Linux.Tests/ReusableResourceTest.cs @@ -100,6 +100,44 @@ public async Task ShouldReuseExistingResource() public static class ReuseHashTest { + public sealed class EqualTest + { + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public void ForSameConfigurationCreatedInDifferentOrder() + { + 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); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public void ForGivenConfiguration() + { + 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); + } + } + public sealed class NotEqualTest { [Fact]