Skip to content

Commit a668d34

Browse files
authored
feat: Add Task<ExecResult> extension method ThrowOnFailure (#1448)
1 parent 5b2fbc3 commit a668d34

File tree

4 files changed

+195
-0
lines changed

4 files changed

+195
-0
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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 executing a command inside a
10+
/// running container fails.
11+
/// </summary>
12+
[PublicAPI]
13+
public sealed class ExecFailedException : Exception
14+
{
15+
private static readonly string[] LineEndings = new[] { "\r\n", "\n" };
16+
17+
/// <summary>
18+
/// Initializes a new instance of the <see cref="ExecFailedException" /> class.
19+
/// </summary>
20+
/// <param name="execResult">The result of the failed command execution.</param>
21+
public ExecFailedException(ExecResult execResult)
22+
: base(CreateMessage(execResult))
23+
{
24+
ExecResult = execResult;
25+
}
26+
27+
/// <summary>
28+
/// Gets the result of the failed command execution inside the container.
29+
/// </summary>
30+
public ExecResult ExecResult { get; }
31+
32+
private static string CreateMessage(ExecResult execResult)
33+
{
34+
var exceptionInfo = new StringBuilder(256);
35+
exceptionInfo.Append($"Process exited with code {execResult.ExitCode}.");
36+
37+
if (!string.IsNullOrEmpty(execResult.Stdout))
38+
{
39+
var stdoutLines = execResult.Stdout
40+
.Split(LineEndings, StringSplitOptions.RemoveEmptyEntries)
41+
.Select(line => " " + line);
42+
43+
exceptionInfo.AppendLine();
44+
exceptionInfo.AppendLine(" Stdout: ");
45+
exceptionInfo.Append(string.Join(Environment.NewLine, stdoutLines));
46+
}
47+
48+
if (!string.IsNullOrEmpty(execResult.Stderr))
49+
{
50+
var stderrLines = execResult.Stderr
51+
.Split(LineEndings, StringSplitOptions.RemoveEmptyEntries)
52+
.Select(line => " " + line);
53+
54+
exceptionInfo.AppendLine();
55+
exceptionInfo.AppendLine(" Stderr: ");
56+
exceptionInfo.Append(string.Join(Environment.NewLine, stderrLines));
57+
}
58+
59+
return exceptionInfo.ToString();
60+
}
61+
}
62+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
namespace DotNet.Testcontainers.Containers
2+
{
3+
using System;
4+
using System.Threading.Tasks;
5+
6+
/// <summary>
7+
/// Extension methods for working with <see cref="ExecResult" /> instances.
8+
/// </summary>
9+
public static class ExecResultExtensions
10+
{
11+
/// <summary>
12+
/// Awaits the <see cref="Task{ExecResult}" /> and throws an exception if the result's exit code is not successful.
13+
/// </summary>
14+
/// <param name="execTask">The task returning an <see cref="ExecResult" />.</param>
15+
/// <param name="successExitCodes">A list of exit codes that should be treated as successful. If none are provided, only exit code <c>0</c> is treated as successful.</param>
16+
/// <returns>The <see cref="ExecResult" /> if the exit code is in the list of success exit codes.</returns>
17+
/// <exception cref="ExecFailedException">Thrown if the exit code is not in the list of success exit codes.</exception>
18+
public static async Task<ExecResult> ThrowOnFailure(this Task<ExecResult> execTask, params long[] successExitCodes)
19+
{
20+
successExitCodes = successExitCodes == null || successExitCodes.Length == 0 ? new long[] { 0 } : successExitCodes;
21+
22+
var execResult = await execTask
23+
.ConfigureAwait(false);
24+
25+
if (Array.IndexOf(successExitCodes, execResult.ExitCode) < 0)
26+
{
27+
throw new ExecFailedException(execResult);
28+
}
29+
30+
return execResult;
31+
}
32+
}
33+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
namespace Testcontainers.Tests;
2+
3+
public sealed class ExecFailedExceptionTest
4+
{
5+
public static readonly List<TheoryDataRow<ExecResult, string>> ExecResultTestData
6+
= new List<TheoryDataRow<ExecResult, string>>
7+
{
8+
new TheoryDataRow<ExecResult, string>
9+
(
10+
new ExecResult("Stdout\nStdout", "Stderr\nStderr", 1),
11+
"Process exited with code 1." + Environment.NewLine +
12+
" Stdout: " + Environment.NewLine +
13+
" Stdout" + Environment.NewLine +
14+
" Stdout" + Environment.NewLine +
15+
" Stderr: " + Environment.NewLine +
16+
" Stderr" + Environment.NewLine +
17+
" Stderr"
18+
),
19+
new TheoryDataRow<ExecResult, string>
20+
(
21+
new ExecResult("Stdout\nStdout", string.Empty, 1),
22+
"Process exited with code 1." + Environment.NewLine +
23+
" Stdout: " + Environment.NewLine +
24+
" Stdout" + Environment.NewLine +
25+
" Stdout"
26+
),
27+
new TheoryDataRow<ExecResult, string>
28+
(
29+
new ExecResult(string.Empty, "Stderr\nStderr", 1),
30+
"Process exited with code 1." + Environment.NewLine +
31+
" Stderr: " + Environment.NewLine +
32+
" Stderr" + Environment.NewLine +
33+
" Stderr"
34+
),
35+
new TheoryDataRow<ExecResult, string>
36+
(
37+
new ExecResult(string.Empty, string.Empty, 1),
38+
"Process exited with code 1."
39+
),
40+
};
41+
42+
[Theory]
43+
[MemberData(nameof(ExecResultTestData))]
44+
public void ExecFailedExceptionCreatesExpectedMessage(ExecResult execResult, string message)
45+
{
46+
var exception = new ExecFailedException(execResult);
47+
Assert.Equal(execResult, exception.ExecResult);
48+
Assert.Equal(message, exception.Message);
49+
}
50+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
namespace Testcontainers.Tests;
2+
3+
public sealed class ExecResultExtensionsTest : IAsyncLifetime
4+
{
5+
private readonly IContainer _container = new ContainerBuilder()
6+
.WithImage(CommonImages.Alpine)
7+
.WithCommand(CommonCommands.SleepInfinity)
8+
.Build();
9+
10+
public async ValueTask InitializeAsync()
11+
{
12+
await _container.StartAsync()
13+
.ConfigureAwait(false);
14+
}
15+
16+
public ValueTask DisposeAsync()
17+
{
18+
return _container.DisposeAsync();
19+
}
20+
21+
[Fact]
22+
public async Task ExecAsyncShouldSucceedWhenCommandReturnsZeroExitCode()
23+
{
24+
// Given
25+
var command = new[] { "true" };
26+
27+
// When
28+
var exception = await Record.ExceptionAsync(() => _container.ExecAsync(command, TestContext.Current.CancellationToken).ThrowOnFailure())
29+
.ConfigureAwait(true);
30+
31+
// Then
32+
Assert.Null(exception);
33+
}
34+
35+
[Fact]
36+
public async Task ExecAsyncShouldThrowExecFailedExceptionWhenCommandFails()
37+
{
38+
// Given
39+
var command = new[] { "/bin/sh", "-c", "echo out; echo err >&2; exit 1" };
40+
41+
// When
42+
var exception = await Assert.ThrowsAsync<ExecFailedException>(() => _container.ExecAsync(command, TestContext.Current.CancellationToken).ThrowOnFailure())
43+
.ConfigureAwait(true);
44+
45+
// Then
46+
Assert.Equal(1, exception.ExecResult.ExitCode);
47+
Assert.Equal("out", exception.ExecResult.Stdout.Trim());
48+
Assert.Equal("err", exception.ExecResult.Stderr.Trim());
49+
}
50+
}

0 commit comments

Comments
 (0)