Skip to content

Commit c2b86ad

Browse files
authored
feat: Throw if container not running (#1550)
1 parent 80bb3ec commit c2b86ad

File tree

15 files changed

+352
-20
lines changed

15 files changed

+352
-20
lines changed

docs/api/wait_strategies.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,17 @@ _ = Wait.ForUnixContainer()
1919

2020
Besides configuring the wait strategy, cancelling a container start can always be done utilizing a [CancellationToken](create_docker_container.md#canceling-a-container-start).
2121

22+
## Wait strategy modes
23+
24+
Wait strategy modes define how Testcontainers for .NET handles container readiness checks. By default, wait strategies assume the container remains running throughout the startup. If a container exits unexpectedly during startup, Testcontainers for .NET will throw a `ContainerNotRunningException` containing the exit code and logs.
25+
26+
Some containers are intended to stop after completing short-lived tasks like migrations or setup scripts. In these cases, the container exit is expected, not a failure. Use `WaitStrategyMode.OneShot` to treat a normal exit as successful rather than throwing an exception.
27+
28+
```csharp
29+
_ = Wait.ForUnixContainer()
30+
.UntilMessageIsLogged("Migration completed", o => o.WithMode(WaitStrategyMode.OneShot));
31+
```
32+
2233
## Wait until an HTTP(S) endpoint is available
2334

2435
You can choose to wait for an HTTP(S) endpoint to return a particular HTTP response status code or to match a predicate. The default configuration tries to access the HTTP endpoint running inside the container. Chose `ForPort(ushort)` or `ForPath(string)` to adjust the endpoint or `UsingTls()` to switch to HTTPS. When using `UsingTls()` port 443 is used as a default. If your container exposes a different HTTPS port, make sure that the correct waiting port is configured accordingly.

docs/custom_configuration/index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,13 @@ Once configured, Testcontainers will rewrite Docker Hub image names by adding th
8181
For example, the image:
8282

8383
```
84-
testcontainers/helloworld:1.2.0
84+
testcontainers/helloworld:1.3.0
8585
```
8686

8787
will automatically become:
8888

8989
```
90-
registry.mycompany.com/mirror/testcontainers/helloworld:1.2.0
90+
registry.mycompany.com/mirror/testcontainers/helloworld:1.3.0
9191
```
9292

9393
## Enable logging

docs/index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ dotnet add package Testcontainers
77
```csharp title="Run the Hello World container"
88
// Create a new instance of a container.
99
var container = new ContainerBuilder()
10-
// Set the image for the container to "testcontainers/helloworld:1.2.0".
11-
.WithImage("testcontainers/helloworld:1.2.0")
10+
// Set the image for the container to "testcontainers/helloworld:1.3.0".
11+
.WithImage("testcontainers/helloworld:1.3.0")
1212
// Bind port 8080 of the container to a random port on the host.
1313
.WithPortBinding(8080, true)
1414
// Wait until the HTTP endpoint of the container is available.

src/Testcontainers.MongoDb/MongoDbBuilder.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ protected override MongoDbBuilder Merge(MongoDbConfiguration oldValue, MongoDbCo
158158
/// <param name="configuration">The container configuration.</param>
159159
/// <param name="ct">Cancellation token.</param>
160160
/// <returns>Task that completes when the replica set initiation has been executed.</returns>
161-
private async Task InitiateReplicaSetAsync(MongoDbContainer container, MongoDbConfiguration configuration, CancellationToken ct)
161+
private static async Task InitiateReplicaSetAsync(MongoDbContainer container, MongoDbConfiguration configuration, CancellationToken ct)
162162
{
163163
if (string.IsNullOrEmpty(configuration.ReplicaSetName))
164164
{
@@ -224,7 +224,7 @@ public Task<bool> UntilAsync(IContainer container)
224224
}
225225

226226
/// <inheritdoc cref="IWaitUntil.UntilAsync" />
227-
private async Task<bool> UntilAsync(MongoDbContainer container)
227+
private static async Task<bool> UntilAsync(MongoDbContainer container)
228228
{
229229
var execResult = await container.ExecScriptAsync(ScriptContent)
230230
.ConfigureAwait(false);

src/Testcontainers/Configurations/WaitStrategies/IWaitStrategy.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ namespace DotNet.Testcontainers.Configurations
99
[PublicAPI]
1010
public interface IWaitStrategy
1111
{
12+
/// <summary>
13+
/// Sets the wait strategy mode.
14+
/// </summary>
15+
/// <param name="mode">The wait strategy mode.</param>
16+
/// <returns>The updated instance of the wait strategy.</returns>
17+
IWaitStrategy WithMode(WaitStrategyMode mode);
18+
1219
/// <summary>
1320
/// Sets the number of retries for the wait strategy.
1421
/// </summary>

src/Testcontainers/Configurations/WaitStrategies/WaitForContainerOS.cs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ internal abstract class WaitForContainerOS : IWaitForContainerOS
1515
/// </summary>
1616
protected WaitForContainerOS()
1717
{
18-
_waitStrategies.Add(new WaitStrategy(new UntilContainerIsRunning()));
18+
var waitStrategy = new WaitStrategy(new UntilContainerIsRunning());
19+
_ = waitStrategy.WithMode(WaitStrategyMode.OneShot);
20+
21+
_waitStrategies.Add(waitStrategy);
1922
}
2023

2124
/// <inheritdoc />
@@ -31,7 +34,7 @@ protected WaitForContainerOS()
3134
public abstract IWaitForContainerOS UntilInternalTcpPortIsAvailable(int containerPort, Action<IWaitStrategy> waitStrategyModifier = null);
3235

3336
/// <inheritdoc />
34-
public virtual IWaitForContainerOS AddCustomWaitStrategy(IWaitUntil waitUntil, Action<IWaitStrategy> waitStrategyModifier = null)
37+
public IWaitForContainerOS AddCustomWaitStrategy(IWaitUntil waitUntil, Action<IWaitStrategy> waitStrategyModifier = null)
3538
{
3639
var waitStrategy = new WaitStrategy(waitUntil);
3740

@@ -51,7 +54,7 @@ public IWaitForContainerOS UntilExternalTcpPortIsAvailable(int containerPort, Ac
5154
}
5255

5356
/// <inheritdoc />
54-
public virtual IWaitForContainerOS UntilFileExists(string filePath, FileSystem fileSystem = FileSystem.Host, Action<IWaitStrategy> waitStrategyModifier = null)
57+
public IWaitForContainerOS UntilFileExists(string filePath, FileSystem fileSystem = FileSystem.Host, Action<IWaitStrategy> waitStrategyModifier = null)
5558
{
5659
switch (fileSystem)
5760
{
@@ -76,19 +79,19 @@ public IWaitForContainerOS UntilMessageIsLogged(Regex pattern, Action<IWaitStrat
7679
}
7780

7881
/// <inheritdoc />
79-
public virtual IWaitForContainerOS UntilHttpRequestIsSucceeded(Func<HttpWaitStrategy, HttpWaitStrategy> request, Action<IWaitStrategy> waitStrategyModifier = null)
82+
public IWaitForContainerOS UntilHttpRequestIsSucceeded(Func<HttpWaitStrategy, HttpWaitStrategy> request, Action<IWaitStrategy> waitStrategyModifier = null)
8083
{
8184
return AddCustomWaitStrategy(request.Invoke(new HttpWaitStrategy()), waitStrategyModifier);
8285
}
8386

8487
/// <inheritdoc />
85-
public virtual IWaitForContainerOS UntilContainerIsHealthy(long failingStreak = 3, Action<IWaitStrategy> waitStrategyModifier = null)
88+
public IWaitForContainerOS UntilContainerIsHealthy(long failingStreak = 3, Action<IWaitStrategy> waitStrategyModifier = null)
8689
{
8790
return AddCustomWaitStrategy(new UntilContainerIsHealthy(failingStreak), waitStrategyModifier);
8891
}
8992

9093
/// <inheritdoc />
91-
public virtual IWaitForContainerOS UntilDatabaseIsAvailable(DbProviderFactory dbProviderFactory, Action<IWaitStrategy> waitStrategyModifier = null)
94+
public IWaitForContainerOS UntilDatabaseIsAvailable(DbProviderFactory dbProviderFactory, Action<IWaitStrategy> waitStrategyModifier = null)
9295
{
9396
return AddCustomWaitStrategy(new UntilDatabaseIsAvailable(dbProviderFactory), waitStrategyModifier);
9497
}

src/Testcontainers/Configurations/WaitStrategies/WaitStrategy.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ public WaitStrategy(IWaitUntil waitUntil)
4141
_ = WithStrategy(waitUntil);
4242
}
4343

44+
/// <summary>
45+
/// Gets the wait strategy mode.
46+
/// </summary>
47+
public WaitStrategyMode Mode { get; private set; }
48+
= WaitStrategyMode.Running;
49+
4450
/// <summary>
4551
/// Gets the number of retries.
4652
/// </summary>
@@ -59,6 +65,13 @@ public WaitStrategy(IWaitUntil waitUntil)
5965
public TimeSpan Timeout { get; private set; }
6066
= TestcontainersSettings.WaitStrategyTimeout ?? TimeSpan.FromHours(1);
6167

68+
/// <inheritdoc />
69+
public IWaitStrategy WithMode(WaitStrategyMode mode)
70+
{
71+
Mode = mode;
72+
return this;
73+
}
74+
6275
/// <inheritdoc />
6376
public IWaitStrategy WithRetries(ushort retries)
6477
{
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
namespace DotNet.Testcontainers.Configurations
2+
{
3+
using DotNet.Testcontainers.Containers;
4+
using JetBrains.Annotations;
5+
6+
/// <summary>
7+
/// Represents the execution mode for a wait strategy.
8+
/// </summary>
9+
[PublicAPI]
10+
public enum WaitStrategyMode
11+
{
12+
/// <summary>
13+
/// Indicates that the container is expected to be in the <c>Running</c> state.
14+
/// </summary>
15+
/// <remarks>
16+
/// When this mode is used, the library verifies that the container is running. If
17+
/// the container is not running, it collects the container's <c>stdout</c> and
18+
/// <c>stderr</c> logs and throws a <see cref="ContainerNotRunningException" /> exception.
19+
/// </remarks>
20+
Running,
21+
22+
/// <summary>
23+
/// Executes the wait strategy without requiring the container to be running.
24+
/// </summary>
25+
/// <remarks>
26+
/// This mode does not check the container's running state.
27+
/// </remarks>
28+
OneShot,
29+
}
30+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
namespace DotNet.Testcontainers.Containers
2+
{
3+
using System;
4+
using System.Linq;
5+
using System.Text;
6+
using JetBrains.Annotations;
7+
8+
/// <summary>
9+
/// Represents an exception that is thrown when a container is not running anymore,
10+
/// and exited unexpectedly.
11+
/// </summary>
12+
[PublicAPI]
13+
public sealed class ContainerNotRunningException : Exception
14+
{
15+
private static readonly string[] LineEndings = new[] { "\r\n", "\n" };
16+
17+
/// <summary>
18+
/// Initializes a new instance of the <see cref="ContainerNotRunningException" /> class.
19+
/// </summary>
20+
/// <param name="id">The container id.</param>
21+
/// <param name="stdout">The stdout.</param>
22+
/// <param name="stderr">The stderr.</param>
23+
/// <param name="exitCode">The exit code.</param>
24+
/// <param name="exception">The inner exception.</param>
25+
public ContainerNotRunningException(string id, string stdout, string stderr, long exitCode, [CanBeNull] Exception exception)
26+
: base(CreateMessage(id, stdout, stderr, exitCode), exception)
27+
{
28+
}
29+
30+
private static string CreateMessage(string id, string stdout, string stderr, long exitCode)
31+
{
32+
var exceptionInfo = new StringBuilder(256);
33+
exceptionInfo.Append($"Container {id} exited with code {exitCode}.");
34+
35+
if (!string.IsNullOrEmpty(stdout))
36+
{
37+
var stdoutLines = stdout
38+
.Split(LineEndings, StringSplitOptions.RemoveEmptyEntries)
39+
.Select(line => " " + line);
40+
41+
exceptionInfo.AppendLine();
42+
exceptionInfo.AppendLine(" Stdout: ");
43+
exceptionInfo.Append(string.Join(Environment.NewLine, stdoutLines));
44+
}
45+
46+
if (!string.IsNullOrEmpty(stderr))
47+
{
48+
var stderrLines = stderr
49+
.Split(LineEndings, StringSplitOptions.RemoveEmptyEntries)
50+
.Select(line => " " + line);
51+
52+
exceptionInfo.AppendLine();
53+
exceptionInfo.AppendLine(" Stderr: ");
54+
exceptionInfo.Append(string.Join(Environment.NewLine, stderrLines));
55+
}
56+
57+
return exceptionInfo.ToString();
58+
}
59+
}
60+
}

src/Testcontainers/Containers/DockerContainer.cs

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -671,11 +671,27 @@ protected override bool Exists()
671671
/// <returns>A task representing the asynchronous operation, returning true if the wait strategy indicates readiness; otherwise, false.</returns>
672672
private async Task<bool> CheckReadinessAsync(WaitStrategy waitStrategy, CancellationToken ct = default)
673673
{
674+
Exception exception = null;
675+
674676
_container = await _client.Container.ByIdAsync(_container.ID, ct)
675677
.ConfigureAwait(false);
676678

677-
return await waitStrategy.UntilAsync(this, ct)
678-
.ConfigureAwait(false);
679+
try
680+
{
681+
return await waitStrategy.UntilAsync(this, ct)
682+
.ConfigureAwait(false);
683+
}
684+
catch (DockerApiException e)
685+
{
686+
exception = e;
687+
}
688+
finally
689+
{
690+
await ThrowIfContainerNotRunningAsync(waitStrategy.Mode, exception)
691+
.ConfigureAwait(false);
692+
}
693+
694+
return false;
679695
}
680696

681697
/// <summary>
@@ -699,13 +715,36 @@ await WaitStrategy.WaitUntilAsync(() => CheckReadinessAsync(waitStrategy, ct), w
699715
return true;
700716
}
701717

718+
/// <summary>
719+
/// Throws <see cref="ContainerNotRunningException" /> when the container exited unexpectedly.
720+
/// </summary>
721+
/// <param name="waitStrategyMode">The wait strategy mode.</param>
722+
/// <param name="innerException">The inner exception.</param>
723+
/// <exception cref="ContainerNotRunningException">The container exited unexpectedly.</exception>
724+
private async Task ThrowIfContainerNotRunningAsync(WaitStrategyMode waitStrategyMode, [CanBeNull] Exception innerException = null)
725+
{
726+
if (innerException == null && (TestcontainersStates.Exited != State || WaitStrategyMode.Running != waitStrategyMode))
727+
{
728+
return;
729+
}
730+
731+
var (stdout, stderr) = await GetLogsAsync()
732+
.ConfigureAwait(false);
733+
734+
var exitCode = await GetExitCodeAsync()
735+
.ConfigureAwait(false);
736+
737+
throw new ContainerNotRunningException(Id, stdout, stderr, exitCode, innerException);
738+
}
739+
702740
private sealed class WaitUntilPortBindingsMapped : WaitStrategy
703741
{
704742
private readonly DockerContainer _parent;
705743

706744
public WaitUntilPortBindingsMapped(DockerContainer parent)
707745
{
708746
_parent = parent;
747+
_ = WithMode(WaitStrategyMode.OneShot);
709748
_ = WithInterval(TimeSpan.FromSeconds(1));
710749
_ = WithTimeout(TimeSpan.FromSeconds(15));
711750
}

0 commit comments

Comments
 (0)