Skip to content
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace DotNet.Testcontainers.Configurations
{
using System.Text.Json;

internal static class DefaultJsonSerializerOptions
{
static DefaultJsonSerializerOptions()
{
Instance.Converters.Add(new JsonOrderedKeysConverter());
}

public static JsonSerializerOptions Instance { get; }
= new JsonSerializerOptions();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@ namespace DotNet.Testcontainers.Configurations

internal sealed class JsonIgnoreRuntimeResourceLabels : JsonOrderedKeysConverter
{
private static readonly ISet<string> IgnoreLabels = new HashSet<string> { ResourceReaper.ResourceReaperSessionLabel, TestcontainersClient.TestcontainersVersionLabel, TestcontainersClient.TestcontainersSessionIdLabel };
private static readonly ISet<string> IgnoreLabels = new HashSet<string>
{
ResourceReaper.ResourceReaperSessionLabel,
TestcontainersClient.TestcontainersVersionLabel,
TestcontainersClient.TestcontainersSessionIdLabel,
};

public override void Write(Utf8JsonWriter writer, IReadOnlyDictionary<string, string> value, JsonSerializerOptions options)
{
var labels = value.Where(label => !IgnoreLabels.Contains(label.Key)).ToDictionary(label => label.Key, label => label.Value);

base.Write(writer, labels, options);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,6 @@ namespace DotNet.Testcontainers.Configurations
[PublicAPI]
public class ResourceConfiguration<TCreateResourceEntity> : IResourceConfiguration<TCreateResourceEntity>
{
private static readonly JsonSerializerOptions JsonSerializerOptions;

static ResourceConfiguration()
{
JsonSerializerOptions = new JsonSerializerOptions { Converters = { new JsonOrderedKeysConverter() } };
}

/// <summary>
/// Initializes a new instance of the <see cref="ResourceConfiguration{TCreateResourceEntity}" /> class.
/// </summary>
Expand Down Expand Up @@ -95,7 +88,7 @@ protected ResourceConfiguration(IResourceConfiguration<TCreateResourceEntity> ol
/// <inheritdoc />
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));
Expand Down
96 changes: 70 additions & 26 deletions tests/Testcontainers.Platform.Linux.Tests/ReusableResourceTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,37 +104,76 @@ public sealed class EqualTest
{
[Fact]
[Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
public void ForSameConfigurationCreatedInDifferentOrder()
public void ForKnownConfiguration()
{
var env1 = new Dictionary<string, string>
{
["keyA"] = "valueA",
["keyB"] = "valueB",
};
var env2 = new Dictionary<string, string>
{
["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<string, string>();
env["keyA"] = "valueA";
env["keyB"] = "valueB";

// When
var hash = new ReuseHashContainerBuilder()
.WithEnvironment(env)
.WithLabel("labelA", "A")
.WithLabel("labelB", "B")
.GetReuseHash();

// Then

// The hash is calculated from the minified JSON. For readability, the JSON
// shown below is formatted. `Dtj7Jx6NVlbDUnA3vmH1nNZw+o8=` is the
// Base64-encoded SHA-1 hash for this JSON (minified):
//
// {
// "Image": null,
// "Name": null,
// "Entrypoint": null,
// "Command": [],
// "Environments": {
// "keyA": "valueA",
// "keyB": "valueB"
// },
// "ExposedPorts": {},
// "PortBindings": {},
// "NetworkAliases": [],
// "Labels": {
// "labelA": "A",
// "labelB": "B",
// "org.testcontainers": "true",
// "org.testcontainers.lang": "dotnet"
// }
// }
Assert.Equal("Dtj7Jx6NVlbDUnA3vmH1nNZw+o8=", hash);
}

[Fact]
[Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
public void ForGivenConfiguration()
public void ForSameConfigurationInDifferentOrder()
{
var env = new Dictionary<string, string>
{
["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<string, string>();
env1["keyA"] = "valueA";
env1["keyB"] = "valueB";

var env2 = new Dictionary<string, string>();
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);
}
}

Expand Down Expand Up @@ -226,7 +265,12 @@ public void EnabledCleanUpThrowsException()
private sealed class ReuseHashContainerBuilder : ContainerBuilder<ReuseHashContainerBuilder, DockerContainer, ContainerConfiguration>
{
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down Expand Up @@ -68,4 +64,12 @@ public async Task StartAsyncShouldSucceedWhenContainerIsNotRunning()
Assert.Null(exception);
}
}

private sealed class WaitUntil : IWaitUntil
{
public Task<bool> UntilAsync(IContainer container)
{
return Task.FromResult(TestcontainersStates.Exited.Equals(container.State));
}
}
}