From a33a3ac09ef37044c18961aa449cd7fa96def55d Mon Sep 17 00:00:00 2001
From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com>
Date: Sat, 10 Feb 2024 13:26:04 +0100
Subject: [PATCH] fix: Consider the stopped timestamp in the log message wait
strategy while starting an existing container
---
.../ElasticsearchBuilder.cs | 2 +-
src/Testcontainers.MongoDb/MongoDbBuilder.cs | 2 +-
.../PostgreSqlBuilder.cs | 13 +++---
.../Clients/DockerContainerOperations.cs | 4 +-
.../WaitStrategies/UntilMessageIsLogged.cs | 2 +-
.../Containers/DockerContainer.cs | 16 ++++++++
src/Testcontainers/Containers/IContainer.cs | 15 +++++++
.../SharedContainerInstance.cs | 23 +++++++++++
.../Testcontainers.Commons.csproj | 1 +
tests/Testcontainers.Commons/Usings.cs | 7 +++-
.../PostgreSqlContainerTest.cs | 41 +++++++++++++++++++
.../Testcontainers.PostgreSql.Tests/Usings.cs | 3 ++
12 files changed, 117 insertions(+), 12 deletions(-)
create mode 100644 tests/Testcontainers.Commons/SharedContainerInstance.cs
diff --git a/src/Testcontainers.Elasticsearch/ElasticsearchBuilder.cs b/src/Testcontainers.Elasticsearch/ElasticsearchBuilder.cs
index f05e9d83d..6dc19adff 100644
--- a/src/Testcontainers.Elasticsearch/ElasticsearchBuilder.cs
+++ b/src/Testcontainers.Elasticsearch/ElasticsearchBuilder.cs
@@ -126,7 +126,7 @@ private sealed class WaitUntil : IWaitUntil
///
public async Task UntilAsync(IContainer container)
{
- var (stdout, _) = await container.GetLogsAsync(timestampsEnabled: false)
+ var (stdout, _) = await container.GetLogsAsync(since: container.StoppedTime, timestampsEnabled: false)
.ConfigureAwait(false);
return Pattern.Any(stdout.Contains);
diff --git a/src/Testcontainers.MongoDb/MongoDbBuilder.cs b/src/Testcontainers.MongoDb/MongoDbBuilder.cs
index e34666b95..033cd9fdd 100644
--- a/src/Testcontainers.MongoDb/MongoDbBuilder.cs
+++ b/src/Testcontainers.MongoDb/MongoDbBuilder.cs
@@ -136,7 +136,7 @@ public WaitUntil(MongoDbConfiguration configuration)
///
public async Task UntilAsync(IContainer container)
{
- var (stdout, stderr) = await container.GetLogsAsync(timestampsEnabled: false)
+ var (stdout, stderr) = await container.GetLogsAsync(since: container.StoppedTime, timestampsEnabled: false)
.ConfigureAwait(false);
return _count.Equals(Array.Empty()
diff --git a/src/Testcontainers.PostgreSql/PostgreSqlBuilder.cs b/src/Testcontainers.PostgreSql/PostgreSqlBuilder.cs
index 53a3844b6..e115d9100 100644
--- a/src/Testcontainers.PostgreSql/PostgreSqlBuilder.cs
+++ b/src/Testcontainers.PostgreSql/PostgreSqlBuilder.cs
@@ -123,18 +123,19 @@ protected override PostgreSqlBuilder Merge(PostgreSqlConfiguration oldValue, Pos
///
private sealed class WaitUntil : IWaitUntil
{
- private static readonly string[] LineEndings = ["\r\n", "\n"];
+ private const string IPv4Listening = "listening on IPv4";
+
+ private const string IPv6Listening = "listening on IPv6";
+
+ private const string DatabaseSystemReady = "database system is ready to accept connections";
///
public async Task UntilAsync(IContainer container)
{
- var (stdout, stderr) = await container.GetLogsAsync(timestampsEnabled: false)
+ var (_, stderr) = await container.GetLogsAsync(since: container.StoppedTime, timestampsEnabled: false)
.ConfigureAwait(false);
- return 2.Equals(Array.Empty()
- .Concat(stdout.Split(LineEndings, StringSplitOptions.RemoveEmptyEntries))
- .Concat(stderr.Split(LineEndings, StringSplitOptions.RemoveEmptyEntries))
- .Count(line => line.Contains("database system is ready to accept connections")));
+ return new[] { IPv4Listening, IPv6Listening, DatabaseSystemReady }.All(stderr.Contains);
}
}
}
\ No newline at end of file
diff --git a/src/Testcontainers/Clients/DockerContainerOperations.cs b/src/Testcontainers/Clients/DockerContainerOperations.cs
index beb30d2d1..f30d9feb0 100644
--- a/src/Testcontainers/Clients/DockerContainerOperations.cs
+++ b/src/Testcontainers/Clients/DockerContainerOperations.cs
@@ -69,8 +69,8 @@ public async Task GetExitCodeAsync(string id, CancellationToken ct = defau
{
ShowStdout = true,
ShowStderr = true,
- Since = Math.Max(0, since.TotalSeconds).ToString("0", CultureInfo.InvariantCulture),
- Until = Math.Max(0, until.TotalSeconds).ToString("0", CultureInfo.InvariantCulture),
+ Since = Math.Max(0, Math.Floor(since.TotalSeconds)).ToString("0", CultureInfo.InvariantCulture),
+ Until = Math.Max(0, Math.Floor(until.TotalSeconds)).ToString("0", CultureInfo.InvariantCulture),
Timestamps = timestampsEnabled,
};
diff --git a/src/Testcontainers/Configurations/WaitStrategies/UntilMessageIsLogged.cs b/src/Testcontainers/Configurations/WaitStrategies/UntilMessageIsLogged.cs
index 909f06ff3..4d988bb71 100644
--- a/src/Testcontainers/Configurations/WaitStrategies/UntilMessageIsLogged.cs
+++ b/src/Testcontainers/Configurations/WaitStrategies/UntilMessageIsLogged.cs
@@ -21,7 +21,7 @@ public UntilMessageIsLogged(Regex pattern)
public async Task UntilAsync(IContainer container)
{
- var (stdout, stderr) = await container.GetLogsAsync(timestampsEnabled: false)
+ var (stdout, stderr) = await container.GetLogsAsync(since: container.StoppedTime, timestampsEnabled: false)
.ConfigureAwait(false);
return _pattern.IsMatch(stdout) || _pattern.IsMatch(stderr);
diff --git a/src/Testcontainers/Containers/DockerContainer.cs b/src/Testcontainers/Containers/DockerContainer.cs
index 55f058fb4..3afaffc82 100644
--- a/src/Testcontainers/Containers/DockerContainer.cs
+++ b/src/Testcontainers/Containers/DockerContainer.cs
@@ -61,6 +61,15 @@ public DockerContainer(IContainerConfiguration configuration, ILogger logger)
///
public ILogger Logger { get; }
+ ///
+ public DateTime CreatedTime { get; private set; }
+
+ ///
+ public DateTime StartedTime { get; private set; }
+
+ ///
+ public DateTime StoppedTime { get; private set; }
+
///
public string Id
{
@@ -400,6 +409,7 @@ protected override async Task UnsafeCreateAsync(CancellationToken ct = default)
_container = await _client.Container.ByIdAsync(id, ct)
.ConfigureAwait(false);
+ CreatedTime = DateTime.UtcNow;
Created?.Invoke(this, EventArgs.Empty);
}
@@ -420,6 +430,10 @@ await _client.RemoveAsync(_container.ID, ct)
.ConfigureAwait(false);
_container = new ContainerInspectResponse();
+
+ CreatedTime = default;
+ StartedTime = default;
+ StoppedTime = default;
}
///
@@ -476,6 +490,7 @@ await WaitStrategy.WaitUntilAsync(() => CheckWaitStrategyAsync(waitStrategy), Ti
Logger.CompleteReadinessCheck(_container.ID);
+ StartedTime = DateTime.UtcNow;
Started?.Invoke(this, EventArgs.Empty);
}
@@ -504,6 +519,7 @@ await _client.StopAsync(_container.ID, ct)
_container = await _client.Container.ByIdAsync(_container.ID, ct)
.ConfigureAwait(false);
+ StoppedTime = DateTime.UtcNow;
Stopped?.Invoke(this, EventArgs.Empty);
}
diff --git a/src/Testcontainers/Containers/IContainer.cs b/src/Testcontainers/Containers/IContainer.cs
index 211a2dac2..11b784bfe 100644
--- a/src/Testcontainers/Containers/IContainer.cs
+++ b/src/Testcontainers/Containers/IContainer.cs
@@ -58,6 +58,21 @@ public interface IContainer : IAsyncDisposable
[NotNull]
ILogger Logger { get; }
+ ///
+ /// Gets the created timestamp.
+ ///
+ DateTime CreatedTime { get; }
+
+ ///
+ /// Gets the started timestamp.
+ ///
+ DateTime StartedTime { get; }
+
+ ///
+ /// Gets the stopped timestamp.
+ ///
+ DateTime StoppedTime { get; }
+
///
/// Gets the container id.
///
diff --git a/tests/Testcontainers.Commons/SharedContainerInstance.cs b/tests/Testcontainers.Commons/SharedContainerInstance.cs
new file mode 100644
index 000000000..6159f30fa
--- /dev/null
+++ b/tests/Testcontainers.Commons/SharedContainerInstance.cs
@@ -0,0 +1,23 @@
+namespace DotNet.Testcontainers.Commons;
+
+[PublicAPI]
+public abstract class SharedContainerInstance : IAsyncLifetime
+ where TContainer : IContainer
+{
+ public SharedContainerInstance(TContainer container)
+ {
+ Container = container;
+ }
+
+ public TContainer Container { get; }
+
+ public Task InitializeAsync()
+ {
+ return Container.StartAsync();
+ }
+
+ public Task DisposeAsync()
+ {
+ return Container.DisposeAsync().AsTask();
+ }
+}
\ No newline at end of file
diff --git a/tests/Testcontainers.Commons/Testcontainers.Commons.csproj b/tests/Testcontainers.Commons/Testcontainers.Commons.csproj
index efc535979..3915c15cd 100644
--- a/tests/Testcontainers.Commons/Testcontainers.Commons.csproj
+++ b/tests/Testcontainers.Commons/Testcontainers.Commons.csproj
@@ -8,6 +8,7 @@
+
diff --git a/tests/Testcontainers.Commons/Usings.cs b/tests/Testcontainers.Commons/Usings.cs
index 904e633f5..74399d9ac 100644
--- a/tests/Testcontainers.Commons/Usings.cs
+++ b/tests/Testcontainers.Commons/Usings.cs
@@ -1,6 +1,11 @@
global using System;
+global using System;
global using System.Diagnostics;
global using System.IO;
global using System.Text;
+global using System.Threading.Tasks;
+global using DotNet.Testcontainers.Containers;
global using DotNet.Testcontainers.Images;
-global using JetBrains.Annotations;
\ No newline at end of file
+global using JetBrains.Annotations;
+global using JetBrains.Annotations;
+global using Xunit;
\ No newline at end of file
diff --git a/tests/Testcontainers.PostgreSql.Tests/PostgreSqlContainerTest.cs b/tests/Testcontainers.PostgreSql.Tests/PostgreSqlContainerTest.cs
index 5433e8c81..5bf15725e 100644
--- a/tests/Testcontainers.PostgreSql.Tests/PostgreSqlContainerTest.cs
+++ b/tests/Testcontainers.PostgreSql.Tests/PostgreSqlContainerTest.cs
@@ -42,4 +42,45 @@ public async Task ExecScriptReturnsSuccessful()
// When
Assert.True(0L.Equals(execResult.ExitCode), execResult.Stderr);
}
+
+ public sealed class ReuseContainerTest : IClassFixture, IDisposable
+ {
+ private readonly CancellationTokenSource _cts = new CancellationTokenSource(TimeSpan.FromMinutes(5));
+
+ private readonly SharedContainerInstance _fixture;
+
+ public ReuseContainerTest(SharedPostgreSqlInstance fixture)
+ {
+ _fixture = fixture;
+ }
+
+ public void Dispose()
+ {
+ _cts.Dispose();
+ }
+
+ [Theory]
+ [InlineData]
+ [InlineData]
+ [InlineData]
+ public async Task StopsAndStartsContainerSuccessful()
+ {
+ await _fixture.Container.StopAsync(_cts.Token)
+ .ConfigureAwait(true);
+
+ await _fixture.Container.StartAsync(_cts.Token)
+ .ConfigureAwait(true);
+
+ Assert.False(_cts.IsCancellationRequested);
+ }
+ }
+
+ [UsedImplicitly]
+ public sealed class SharedPostgreSqlInstance : SharedContainerInstance
+ {
+ public SharedPostgreSqlInstance()
+ : base(new PostgreSqlBuilder().Build())
+ {
+ }
+ }
}
\ No newline at end of file
diff --git a/tests/Testcontainers.PostgreSql.Tests/Usings.cs b/tests/Testcontainers.PostgreSql.Tests/Usings.cs
index 576c631bf..a7c5d950c 100644
--- a/tests/Testcontainers.PostgreSql.Tests/Usings.cs
+++ b/tests/Testcontainers.PostgreSql.Tests/Usings.cs
@@ -1,6 +1,9 @@
+global using System;
global using System.Data;
global using System.Data.Common;
+global using System.Threading;
global using System.Threading.Tasks;
global using DotNet.Testcontainers.Commons;
+global using JetBrains.Annotations;
global using Npgsql;
global using Xunit;
\ No newline at end of file