diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 3bf0cb3a9..9a90fb792 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -13,20 +13,22 @@ "ghcr.io/devcontainers/features/docker-in-docker:2": { "moby": true }, - "ghcr.io/devcontainers/features/dotnet:2.0.5": { - "version": "8.0.200", + "ghcr.io/devcontainers/features/dotnet:2.1.3": { + "version": "8.0", "installUsingApt": false } }, "customizations": { - "extensions": [ - "ms-azuretools.vscode-docker", - "ms-dotnettools.csdevkit" - ], - "settings": { - "omnisharp.path": "latest" // https://github.com/OmniSharp/omnisharp-vscode/issues/5410#issuecomment-1284531542. + "vscode": { + "extensions": [ + "ms-azuretools.vscode-docker", + "ms-dotnettools.csdevkit" + ], + "settings": { + "dotnet.defaultSolution": "${containerWorkspaceFolder}/Testcontainers.sln" + } } }, "postCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder} && git lfs checkout", - "postStartCommand": ["dotnet", "build"] + "postStartCommand": ["dotnet", "build", "${containerWorkspaceFolder}/Testcontainers.sln", "/consoleLoggerParameters:NoSummary", "/property:GenerateFullPaths=true", "/property:Configuration=Debug", "/property:Platform=Any CPU"] } diff --git a/Directory.Packages.props b/Directory.Packages.props index bae22338e..011ede47d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -15,11 +15,11 @@ - - - - - + + + + + diff --git a/build.cake b/build.cake index 7ce01ac0c..159e045df 100644 --- a/build.cake +++ b/build.cake @@ -84,6 +84,7 @@ Task("Tests") Filter = param.TestFilter, ResultsDirectory = param.Paths.Directories.TestResultsDirectoryPath, ArgumentCustomization = args => args + .AppendSwitchQuoted("--blame-hang-timeout", "5m") }); }); diff --git a/src/Testcontainers/Clients/DockerApiClient.cs b/src/Testcontainers/Clients/DockerApiClient.cs index 122039994..f0436a287 100644 --- a/src/Testcontainers/Clients/DockerApiClient.cs +++ b/src/Testcontainers/Clients/DockerApiClient.cs @@ -4,6 +4,7 @@ namespace DotNet.Testcontainers.Clients using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; + using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -107,18 +108,14 @@ await RuntimeInitialized.WaitAsync(ct) runtimeInfo.AppendLine(dockerInfo.OperatingSystem); runtimeInfo.Append(" Total Memory: "); - runtimeInfo.AppendLine(string.Format(CultureInfo.InvariantCulture, "{0:F} {1}", dockerInfo.MemTotal / Math.Pow(1024, byteUnits.Length), byteUnits[byteUnits.Length - 1])); + runtimeInfo.AppendFormat(CultureInfo.InvariantCulture, "{0:F} {1}", dockerInfo.MemTotal / Math.Pow(1024, byteUnits.Length), byteUnits[byteUnits.Length - 1]); var labels = dockerInfo.Labels; if (labels != null && labels.Count > 0) { + runtimeInfo.AppendLine(); runtimeInfo.AppendLine(" Labels: "); - - foreach (var label in labels) - { - runtimeInfo.Append(" "); - runtimeInfo.AppendLine(label); - } + runtimeInfo.Append(string.Join(Environment.NewLine, labels.Select(label => " " + label))); } Logger.LogInformation("{RuntimeInfo}", runtimeInfo); } diff --git a/src/Testcontainers/Logger.cs b/src/Testcontainers/Logger.cs index e7028ec32..1b908c5d5 100644 --- a/src/Testcontainers/Logger.cs +++ b/src/Testcontainers/Logger.cs @@ -106,7 +106,8 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except { if (IsEnabled(logLevel)) { - Console.Out.WriteLine("[testcontainers.org {0:hh\\:mm\\:ss\\.ff}] {1}", _stopwatch.Elapsed, formatter.Invoke(state, exception)); + var message = exception == null ? formatter.Invoke(state, null) : string.Join(Environment.NewLine, formatter.Invoke(state, exception), exception); + Console.Out.WriteLine("[testcontainers.org {0:hh\\:mm\\:ss\\.ff}] {1}", _stopwatch.Elapsed, message); } } diff --git a/tests/Testcontainers.Commons/CommonImages.cs b/tests/Testcontainers.Commons/CommonImages.cs index 84e1090e2..5374858c8 100644 --- a/tests/Testcontainers.Commons/CommonImages.cs +++ b/tests/Testcontainers.Commons/CommonImages.cs @@ -3,7 +3,7 @@ namespace DotNet.Testcontainers.Commons; [PublicAPI] public static class CommonImages { - public static readonly IImage Ryuk = new DockerImage("testcontainers/ryuk:0.6.0"); + public static readonly IImage Ryuk = new DockerImage("testcontainers/ryuk:0.9.0"); public static readonly IImage Alpine = new DockerImage("alpine:3.17"); diff --git a/tests/Testcontainers.Platform.Linux.Tests/DependsOnTest.cs b/tests/Testcontainers.Platform.Linux.Tests/DependsOnTest.cs index 55a1ab3df..b3f05e8eb 100644 --- a/tests/Testcontainers.Platform.Linux.Tests/DependsOnTest.cs +++ b/tests/Testcontainers.Platform.Linux.Tests/DependsOnTest.cs @@ -2,37 +2,61 @@ namespace Testcontainers.Tests; public sealed class DependsOnTest : IAsyncLifetime { - private const string DependsOnKey = "org.testcontainers.depends-on"; + private readonly FilterByProperty _filters = new FilterByProperty(); - private const string DependsOnValue = "true"; + private readonly IList _disposables = new List(); - private readonly IContainer _container = new ContainerBuilder() - .DependsOn(new ContainerBuilder() + private readonly string _labelKey = Guid.NewGuid().ToString("D"); + + private readonly string _labelValue = Guid.NewGuid().ToString("D"); + + public DependsOnTest() + { + _filters.Add("label", string.Join("=", _labelKey, _labelValue)); + } + + public async Task InitializeAsync() + { + var childContainer1 = new ContainerBuilder() .WithImage(CommonImages.Alpine) - .WithLabel(DependsOnKey, DependsOnValue) - .Build()) - .DependsOn(new ContainerBuilder() + .WithLabel(_labelKey, _labelValue) + .Build(); + + var childContainer2 = new ContainerBuilder() .WithImage(CommonImages.Alpine) - .WithLabel(DependsOnKey, DependsOnValue) - .Build()) - .DependsOn(new NetworkBuilder() - .WithLabel(DependsOnKey, DependsOnValue) - .Build()) - .DependsOn(new VolumeBuilder() - .WithLabel(DependsOnKey, DependsOnValue) - .Build(), "/workdir") - .WithImage(CommonImages.Alpine) - .WithLabel(DependsOnKey, DependsOnValue) - .Build(); - - public Task InitializeAsync() - { - return _container.StartAsync(); + .WithLabel(_labelKey, _labelValue) + .Build(); + + var network = new NetworkBuilder() + .WithLabel(_labelKey, _labelValue) + .Build(); + + var volume = new VolumeBuilder() + .WithLabel(_labelKey, _labelValue) + .Build(); + + var parentContainer = new ContainerBuilder() + .DependsOn(childContainer1) + .DependsOn(childContainer2) + .DependsOn(network) + .DependsOn(volume, "/workdir") + .WithImage(CommonImages.Alpine) + .WithLabel(_labelKey, _labelValue) + .Build(); + + await parentContainer.StartAsync() + .ConfigureAwait(false); + + _disposables.Add(parentContainer); + _disposables.Add(childContainer1); + _disposables.Add(childContainer2); + _disposables.Add(network); + _disposables.Add(volume); } public Task DisposeAsync() { - return _container.DisposeAsync().AsTask(); + return Task.WhenAll(_disposables.Select(disposable => disposable.DisposeAsync().AsTask())); } [Fact] @@ -40,19 +64,15 @@ public Task DisposeAsync() public async Task DependsOnCreatesDependentResources() { // Given - using var clientConfiguration = TestcontainersSettings.OS.DockerEndpointAuthConfig.GetDockerClientConfiguration(ResourceReaper.DefaultSessionId); + using var clientConfiguration = TestcontainersSettings.OS.DockerEndpointAuthConfig.GetDockerClientConfiguration(Guid.NewGuid()); using var client = clientConfiguration.CreateClient(); - var labelFilter = new Dictionary { { string.Join("=", DependsOnKey, DependsOnValue), true } }; - - var filters = new Dictionary> { { "label", labelFilter } }; - - var containersListParameters = new ContainersListParameters { All = true, Filters = filters }; + var containersListParameters = new ContainersListParameters { All = true, Filters = _filters }; - var networksListParameters = new NetworksListParameters { Filters = filters }; + var networksListParameters = new NetworksListParameters { Filters = _filters }; - var volumesListParameters = new VolumesListParameters { Filters = filters }; + var volumesListParameters = new VolumesListParameters { Filters = _filters }; // When var containers = await client.Containers.ListContainersAsync(containersListParameters) @@ -61,12 +81,12 @@ public async Task DependsOnCreatesDependentResources() var networks = await client.Networks.ListNetworksAsync(networksListParameters) .ConfigureAwait(true); - var volumesListResponse = await client.Volumes.ListAsync(volumesListParameters) + var response = await client.Volumes.ListAsync(volumesListParameters) .ConfigureAwait(true); // Then Assert.Equal(3, containers.Count); Assert.Single(networks); - Assert.Single(volumesListResponse.Volumes); + Assert.Single(response.Volumes); } } \ No newline at end of file diff --git a/tests/Testcontainers.Platform.Linux.Tests/ReusableResourceTest.cs b/tests/Testcontainers.Platform.Linux.Tests/ReusableResourceTest.cs index 747f85d6d..dad62493a 100644 --- a/tests/Testcontainers.Platform.Linux.Tests/ReusableResourceTest.cs +++ b/tests/Testcontainers.Platform.Linux.Tests/ReusableResourceTest.cs @@ -1,9 +1,16 @@ namespace Testcontainers.Tests; -public sealed class ReusableResourceTest : IAsyncLifetime, IDisposable +// We cannot run these tests in parallel because they interfere with the port +// forwarding tests. When the port forwarding container is running, Testcontainers +// automatically inject the necessary extra hosts into the builder configuration +// using `WithPortForwarding()` internally. Depending on when the test framework +// starts the port forwarding container, these extra hosts can lead to flakiness. +// This happens because the reuse hash changes, resulting in two containers with +// the same labels running instead of one. +[CollectionDefinition(nameof(ReusableResourceTest), DisableParallelization = true)] +[Collection(nameof(ReusableResourceTest))] +public sealed class ReusableResourceTest : IAsyncLifetime { - private readonly DockerClient _dockerClient = TestcontainersSettings.OS.DockerEndpointAuthConfig.GetDockerClientConfiguration(Guid.NewGuid()).CreateClient(); - private readonly FilterByProperty _filters = new FilterByProperty(); private readonly IList _disposables = new List(); @@ -63,21 +70,26 @@ public Task DisposeAsync() })); } - public void Dispose() - { - _dockerClient.Dispose(); - } - [Fact] public async Task ShouldReuseExistingResource() { - var containers = await _dockerClient.Containers.ListContainersAsync(new ContainersListParameters { Filters = _filters }) + using var clientConfiguration = TestcontainersSettings.OS.DockerEndpointAuthConfig.GetDockerClientConfiguration(Guid.NewGuid()); + + using var client = clientConfiguration.CreateClient(); + + var containersListParameters = new ContainersListParameters { All = true, Filters = _filters }; + + var networksListParameters = new NetworksListParameters { Filters = _filters }; + + var volumesListParameters = new VolumesListParameters { Filters = _filters }; + + var containers = await client.Containers.ListContainersAsync(containersListParameters) .ConfigureAwait(true); - var networks = await _dockerClient.Networks.ListNetworksAsync(new NetworksListParameters { Filters = _filters }) + var networks = await client.Networks.ListNetworksAsync(networksListParameters) .ConfigureAwait(true); - var response = await _dockerClient.Volumes.ListAsync(new VolumesListParameters { Filters = _filters }) + var response = await client.Volumes.ListAsync(volumesListParameters) .ConfigureAwait(true); Assert.Single(containers); @@ -85,9 +97,9 @@ public async Task ShouldReuseExistingResource() Assert.Single(response.Volumes); } - public static class ReuseHash + public static class ReuseHashTest { - public sealed class NotEqual + public sealed class NotEqualTest { [Fact] public void ForDifferentNames()