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